Setup a Developer Blog with Social Images using SvelteKit

Matías Hernández

Banner Image for Jam

The end goal for this article is to implement a blog site that includes the use of open graph images for social sharing. It will consist of a:

  • A Home page with a list of some articles.
  • An article page to allow the user to read the articles.

Each of these pages will have its own social image. To accomplish this, you'll set the corresponding meta tag into the <head> of the current page using svelte features.

You can check out this repository that will get you to the end result of this article, clone it, run it and play around to grasp the way of work with SvelteKit, or you can just play around with this codesandbox.

¿What is Sveltekit?

SvelteKit is Svelte's take on the web application framework. This is the new (still in beta) official way to develop applications with Svelte. It is packed with features like routing, layouts, stage management, API routes, SSG, and SSR. If you come from React or Vue world, SvelteKit is the counterpart of Nextjs or Nuxt.

SvelteKit helps you to make static sites, server-rendered sites, and even hybrid static/server-rendered apps. It delivers an outstanding developer experience and fast user experience like the Svelte philosophy describes.

In general words, SvelteKit is a tool to take your Svelte code and transform it into a node app or static files. You can consider Svelte as the underlying language and SvelteKit the set that brings the server-side and some options about how an application should be architected.

What are Social Images

One big part of content creation is to share your content on social media, but just tweeting about your newest post is not enough to stand out in a never-ending stream of content.

One way to stand out on all of that noise is by sharing images or "Open Graph Images" for your content.

These images are used by different social network applications to create a "content card" and show some kind of preview of the link you're sharing.

You can manually create these images for each post you publish in your post, but it is a significant burden that can quickly slow down your publishing workflow. But, we are developers, and we love to craft solutions based on automation.

By the end of this article, you'll be able to generate social media images by using Cloudinary transformation API as part of your SvelteKit site. You'll also learn how to get up to speed with SvelteKit by creating your first blog.

Develop your blog

This section will drive you through the steps required to create the blog described at the top of the article. This particular implementation uses:

  • Prismic as headless CMS to write and deliver the articles
  • tailwindcss for styling

To start, we just need to run the SvelteKit scaffold script.

1npm init svelte@next my-blog

This will show you a message saying that this is beta software and also a prompt asking what template you want to use. Use the arrow keys to choose SvelteKit

Then you'll be asked if you want to use Typescript. For this demo, you can go with plain old javascript. Choose No. Finally, select yes to the next questions: Add ESLint and Add Prettier.

Then to run the base skeleton just go into the new folder, install dependencies and run the app

1cd my-blog
2npm install
3npm run dev

Now go ahead and visit http://localhost:3000 in your browser, and you'll see the base SvelteKit project running.

Now let's set up tailwindcss to implement the base layout. For this purpose, you can use svelte-add so just run in console

1npx svelte-add@latest tailwindcss

With that, you are ready to go.

After the scaffold process a few files were created. You will get rid of some of this but let's check them out first.

From the files there you'll focus will be under the lib and routes folder. In SvelteKit every page is an Svelte component, to create a page, just add it to under src/routes. In this case the scaffold created two pages for you. about.svelte and index.svelte.

You can get rid of the about.svelte file since you'll not use it in this project.

There is also another important file: __layout.svelte. This file enables you to create the base layout structure that will be shared across all your pages.

Finally, there is another default folder: src/lib this folder is where you'll store or save the components or utilities you need in the application. SvelteKit makes the content of this folder accessible with an alias: $lib so if you need to import something from this folder you just use import Component from '$lib/Componente.svelte since is an absolute path. By default there are some files under this folder like an Svelte component: Counter.svelte and an empty javascript file form.js.

Is important to recall that Svelte manage the components in a single file that have 3 different sections. The HTML where you structure your component, the script tag to define the javascript logic and the style tag to define the scoped css. Check more information about Svelte components here.

Your focus will be in the file under the src folder, there you'll find a few other files:

  • app.html: The base HTML structure were the application will be mounted.
  • app.postcss: The main postcss configuration. Here you can see tailwind configuration. You can edit this file if you need some extra utilities for your css.
  • hooks.js: This is an optional file to define some functions that will run in the server. You can read more about the hooks in the official documentation. You'll not be working with hooks in this particular project.

To start, let's get rid of some of this default files:

  • Delete the content of the src/lib folder
  • Delete the file about.svelte
  • Delete the content of __layout.svelte and the content of index.svelte.

Now, let's start by defining the layout of the blog and think about the components,

In the layout file you will use the <slot /> component to define where the content of the pages will be rendered.

If you come from React world, the <slot /> component is very similar to the children prop. Read more about <slot /> in the official docs.

Add the content of this gist to the layout file. That code defines the structure of the page including a header with a navigation section, a footer, and the main container.

Now you can add the first page under src/routes/index.svelte Check the source code of this example implementation directly here.

The site will list the articles retrieved from Prismic on this home page. Here is where you'll use the new features of SvelteKit.

Prismic is a headless CMS that easily integrates with your choose technology to manage and deliver the content. In this case, Prismic is used in a very contrived way to store and provide the website's articles.

To start, SvelteKit has the concept of endpoints These are "special" modules that live under src/route are unique because instead of exporting a Svelte component, these routes export a function that corresponds with the HTTP methods.

In this example, the home page will render a list of articles. The page will hit an internal endpoint under /API/blog/articles to get those. This endpoint will return a JSON structure with the data from Prismic.

These are just standard javascript functions, as you can see in this example.

1// /api/blog/articles.json
2import Client, { Predicates } from '$lib/PrismicClient';
4export async function get() {
5 const response = await Client.query('document.type', 'article'));
6 const result = => {
7 return {
9 id:,
10 href: r.href,
11 uid: r.uid,
12 featured:
13 };
14 });
15 const { featured, articles } = result?.reduce(
16 (acc, current) => {
17 if (current.featured) {
18 acc.featured = current;
19 } else {
20 acc.articles.push(current);
21 }
22 return acc;
23 },
24 { featured: null, articles: [] }
25 );
27 return {
28 body: {
29 featured,
30 articles
31 }
32 };

This exported function is mapped to the get method. It uses the PrismicClient methods that you can see here and just fetch the data, parse it and then return the new object in the body of the request.

Now, how the home page will get this data?. A page, that is, in fact, a Svelte component, can define an export a function called load that will run before the component is created. The trick here is that this function runs server-side and also client-side, allowing you to retrieve data on build time if you want to get an SSG page.

You can just add a new script tag to the home page with the load function to accomplish this feat. Why a new script tag? Since this function runs before the component is rendered, it lives in a different context <script context="module">.

So, open your src/routes/index.svelte component and add the load function that perform the fetch request to /api/blog/articles

1<script context="module" lang="ts">
2 export async function load({ fetch }) {
3 const url = `/api/blog/articles.json`;
4 const res = await fetch(url);
5 const { articles, featured } = await res.json();
7 if (res.ok) {
8 return {
9 status: res.status,
10 props: {
11 articles,
12 featured
13 }
14 };
15 }
17 return {
18 status: res.status,
19 error: new Error(`Could not load ${url}, status: ${res.status}`)
20 };
21 }

As you can see, retrieve data for your page is really straightforward. Now that the props object is returned, it will be available for your component, which means that in the Svelte component under this page, you can access articles and featured objects.

1<script lang="ts">
2 import PrismicDOM from 'prismic-dom';
3 import ArticleCard from '$lib/components/ArticleCard.svelte';
4 export let articles: Article[];
5 export let featured: Article;
6 const list = [...articles].splice(1, articles.length);
7 const listHeader = articles[0];

Since the data comes from Prismic, you need a helper to parse and render the content of the rich text field. This page also uses a component to render a card. The component can be found in src/lib/components/ArticleCard.svelte

Let's check how to render the article.

To render an article, the site uses another SvelteKit feature called dynamic parameters. This allows you to create routes that accept a parameter like the slug or uid of the article.

The dynamic parameter let you load a particular element based on some identifier. To do this, let's create the dynamic page under src/route/blog/[uid].svelte

This page will get the uid from the URL and pass it down to the API to retrieve the corresponding data. This is done again in the load function of the page.

1<script context="module" lang="ts">
2 export async function load({ fetch, page }) {
3 const { uid } = page.params;
4 const url = `/api/blog/${uid}.json`;
5 const res = await fetch(url);
6 const article = await res.json();
7 if (res.ok) {
8 return {
9 status: res.status,
10 props: {
11 article
12 }
13 };
14 }
16 return {
17 status: res.status,
18 error: new Error(`Could not load ${url}, status: ${res.status}`)
19 };
20 }

This load function will get the uid and use it to run the endpoint get function defined in API/blog/[uid].json.ts that perform the query to Prismic based on the uid parameter.

Then, this page will have access to the corresponding article instance to render it as is shown here.

Here is where the social images come into play.

Setup Social Images

Some of the most shared content of a site or blog is the articles, so each piece should have its own social images (and SEO data) based on the content that is actually present. This can be easily accomplished by a neat Svelte feature: the <svelte:head> component.

The svelte:head component allows you to easily insert elements into the <head> tag of the document, and since the Sveltekit pages are a document by itself, by using svelte:head, you'll end up defining the meta tags for each article in just one file :D

Now you have a few options to generate or retrieve the social image for your article. You can directly use the same image you added to the article and put it into the corresponding meta tag or use a neat trick provided by Cloudinary. Add overlays to the image so you can share more information in it.

With Cloudinary

If you decided to use Cloudinary, then you have options too:

  • Upload the header image of the article to Cloudinary and apply transformations to it like adding text overlay, your logo etc
  • or, use a pre-defined template with the corresponding text overlay for the article title.

Personally, I prefer the second option, have a template image that shows my personal brand where I can include the post title and keywords or subtitle. To do this, you first need to define the template image.

What this template image should have?

To create a reusable template for your social image, need you need an image that can work for any post you make. This image will include

  • Your logo or profile picture
  • The post title: This is the dynamic part of the template, so in the template, you need to reserve space for it. There are some guidelines for the length of the title. You can use some tools like the SEO snippet generator to check the size of your post titles
  • Subtitle, keywords, or tagline: Just an additional text space to show some extra data.

You can find a base template in this Figma board

Or just use this image for testing purposes

With your base image done, you need to use Cloudinary transformation capabilities to add the corresponding text overlay. This can be simple done in a function to be reused across your project.

Let's use the `Cloudinary nodejs package that can be found in npm

1npm install cloudinary --save

You can check in deep documentation in cloudinary site

For this function, you'll use the cloudinary.url method to define transformations using javascript objects and return the corresponding URL for it.

Let's start by creating the function for this job. It will lib under src/lib . SvelteKit comes with some default configurations for some folders like this one allowing you to access the modules from that library by using $lib.

So, how this function looks like?

1import cloudinary from 'cloudinary';
4 cloud_name: import.meta.env.VITE_CLOUDINARY_CLOUD_NAME,
8async function getOgImage({ title, subTitle }) {
9 const url = cloudinary.v2.url('example_og_image.jpg', {
10 transformation: [
11 { width: 1200, height: 627, crop: 'fill', quality: 'auto', format: 'auto' },
12 {
13 crop: 'fit',
14 width: 700,
15 x: 480,
16 y: 254,
17 gravity: 'south_west',
18 color: 'white',
19 effect: 'shadow:40',
20 overlay: {
21 font_family: 'roboto',
22 font_size: 54,
23 font_weight: 'bold',
24 text: encodeURIComponent(title)
25 }
26 },
27 {
28 crop: 'fit',
29 width: 700,
30 x: 480,
31 y: 154,
32 gravity: 'south_west',
33 color: 'white',
34 overlay: {
35 font_family: 'roboto',
36 font_size: 34,
37 font_weight: 'bold',
38 text: encodeURIComponent(subTitle)
39 }
40 }
41 ]
42 });
44 return URL;
47export default getOgImage;

Let's break down that code.

First, you'll find the configuration for using cloudinary. This config method is using some environment variables. If you need to use custom environment variables inside your code, you need to name it with the prefix VITE_. The use of the prefix will make the variable immediately available under the import.meta object.

Sveltekit uses Vite for building the application, and Vite uses dotenv to load the variables from the .env file.

Vite enables the import.meta object to expose context-specific data to a module.

You can find more information about environment variables in Sveltekit site and Vite docs

Then, you have the function definition that is receiving an object as an argument that has 2 attributes: title and subTitle. These are the overlays you want to create

Now is the time to use cloudinary transformations.

The cloudinary.url method accepts 2 arguments, the name of the base image and an object with options. This object is where the magic happens. One of the properties of the options object is the transformation array that accepts multiple transformation object descriptors. For this case, you'll use 3 transformations.

  1. First, define the "canvas" to work on. Will set format, quality, and size for the base image.
  2. Second will define the first text overlay for the title
  3. Third one defines the transformation to create another text overlay for the subTitle

You can find more in deep documentation about transformation in Cloudinary documentation site

Now is time to use this function and really use your newly created social image.

Let's go back to the get function in our endpoint route for an article src/routes/api/blog/[uid].json.ts

1import Client from '$lib/PrismicClient';
2import PrismicDOM from 'prismic-dom';
3import getOgImage from '$lib/getOgImage';
6export async function get({ params }) {
7 const { uid } = params;
8 const response = await Client.getByUID('article', uid);
9 const { title, subTitle } =;
11 return {
12 body: {
14 id:,
15 href: response.href,
16 uid: response.uid,
17 ogImage: getOgImage({
18 text: PrismicDOM.RichText.asText(title),
19 subTitle: PrismicDOM.RichText.asText(subTitle)
20 })
21 }
22 };

Here you can just use your getOgImage function passing down the text you want to show in the social image card, then the URL generated by the function will be available in the svelte component/page found in src/routes/blog/[uid].svelte

Let's show the social image card

Now that the svelte component has the URL, we can just use <svelte:head> and add the corresponding meta tags, but it will be way better to have a reusable component for future pages, right?.

Let's build an SEO component under src/lib/components/Seo.svelte

And write down the meta tags we need for a basic Seo data definition.

1<script lang="ts">
2 export let title: string;
3 export let description: string = undefined;
4 export let keywords: string;
5 export let canonical: string = undefined;
6 export let type: string;
7 export let image: string;
11 <title>{title}</title>
12 <meta name="robots" content="index, follow" />
13 <meta name="googlebot" content="index,follow" />
14 {#if description}
15 <meta name="description" content={description} />
16 {/if}
17 {#if canonical}
18 <link rel="canonial" href={canonical} />
19 {/if}
20 <meta name="keywords" content={keywords} />
22 <meta property="og:title" content={title} />
23 {#if description}
24 <meta property="og:description" content={description} />
25 {/if}
26 {#if canonical}
27 <meta property="og:url" content={canonical} />
28 {/if}
29 <meta property="og:type" content={type ? type : 'site'} />
30 <meta property="og:image" content={image} />
32 <meta name="twitter:card" content="summary_large_image" />
33 <meta name="twitter:title" content={title} />
34 {#if description}
35 <meta name="twitter:description" content={description} />
36 {/if}
37 <meta name="twitter:image" content={image} />

This component exposes a few props. You can found them defined in the <script> tag as an exported variable.

Then we have the component definition that creates some meta tags. The ones that use the social image you created are og:image and twitter:image

Let's go back to the article pagesrc/routes/blog/[uid].svelte and use the new Seo component

1<script lang="ts">
2 import PrismicDOM from 'prismic-dom';
3 import Seo from '$lib/components/Seo.svelte';
4 export let article;
5 const title = PrismicDOM.RichText.asText(article.title);
6 const subTitle = PrismicDOM.RichText.asText(article.subTitle);
10<Seo {title} {subTitle} keywords="" image={article.ogImage} type="article" />

Here the code import the Seo component from $lib/components/Seo.svelte, define the article prop with the data from the load function, and create a new variable with the title and subtitle

Then the code just uses the Seo component bypassing the required props. Note here that neat syntax {title} that is actually a shortcut to title={title}.

Now, if you run your project and go to an article, you'll find the `og:image meta tag with the corresponding cloudinary URL of your social image.

Without Cloudinary

If you don't want to use Cloudinary transformation API and prefer to just use the same image you defined for your article, you can use the same Seo component. Still, instead of passing the article.image as value to image prop, you'll need to give the URL of the image. Since this project is using Prismic, you'll access article.image.url

1<Seo {title} {subTitle} keywords="" image={article.image.url} type="article" />


In this article, you were able to review the power of the new toolset for Svelte and create a basic website to publish your content using SvelteKit. At the same time, you learned the importance o

Matías Hernández

Senior Frontend Engineer

Matías is a Chilean Software Engineer, father, host of two podcasts and egghead, and Escuela Frontend, instructor.

He focuses on front-end development and shares what he knows and learns with the community through articles and video lessons tailored to the Spanish community.

Matías host two podcasts: Café con Tech and Control Remoto, and write for different tech publications.

You can always reach him on twitter