DEV Community

Cover image for Integrating SvelteKit with Storyblok (Using Svelte 5)
Roberto B.
Roberto B.

Posted on • Edited on

3 2

Integrating SvelteKit with Storyblok (Using Svelte 5)

This guide will show you how to integrate SvelteKit with Storyblok CMS using the latest Svelte 5 and Bun as the runtime and package manager. By the end, you'll have a fully functional, dynamic web app ready to manage and render content easily.

Key takeaways from this guide

  • Setting up a SvelteKit project with Bun for performance and simplicity.
  • Installing and configuring the Storyblok Svelte SDK for seamless CMS integration.
  • Loading and rendering dynamic data from Storyblok.
  • Creating reusable frontend components.

The tools

  • Bun: a lightning-fast JavaScript runtime, bundler, and package manager.
  • SvelteKit (Svelte 5): a modern framework for building high-performance web applications.
  • Storyblok: a headless CMS that offers an intuitive Visual Editor and API-driven content delivery.

The source code

An open-source repository is available for a quick and easy start: https://github.com/roberto-butti/svelte5-storyblok-example

Set up a new SvelteKit project

Start by creating a new SvelteKit project using Bun:

bunx sv create svelte5-sveltekit-storyblok
Enter fullscreen mode Exit fullscreen mode

The command execution will raise some questions. You can set some defaults:

  • Which template would you like? SvelteKit minimal
  • Add type checking with Typescript? prettier
  • Which package manager do you want to install dependencies with? bun

The command will also install the dependencies so you can jump into the new directory and run the development server to ensure everything is set up correctly:

cd svelte5-sveltekit-storyblok
bun run dev --open
Enter fullscreen mode Exit fullscreen mode

By default, the new server will run on http://localhost:5173/ (HTTP protocol and port 5173)

Enabling HTTPS

For the Storyblok Visual Editor to work locally, you’ll need HTTPS. There are different ways to enable HTTPS. Probably the easiest one is to use the @vitejs/plugin-basic-ssl Vite plugin.
Add it:

bun add -d @vitejs/plugin-basic-ssl
Enter fullscreen mode Exit fullscreen mode

And then, in the vite.config.ts add basicSsl as a plugin:

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import basicSsl from '@vitejs/plugin-basic-ssl';

export default defineConfig({
    plugins: [sveltekit(), basicSsl()]
});
Enter fullscreen mode Exit fullscreen mode

Now, if you run the bun dev command, you should see HTTPS enabled via https://localhost:5173/.

Note: because a self-signed certificate is used, you must accept it the first time you visit https://localhost:5173/ via your Browser.

The bun dev command starts a secure local server, ensuring compatibility with Storyblok's Visual Editor.

Install the Storyblok Svelte SDK

Add the Storyblok SDK to your project:

bun add @storyblok/svelte
Enter fullscreen mode Exit fullscreen mode

Configure Storyblok

  • Log in to your Storyblok account.
  • Create a new space. The Community plan is free. No credit card is needed, and the plan can be used to start.
  • Navigate to Settings > Access Tokens and copy the "Preview" access token.
  • Navigate to Settings > Visual Editor and set https://localhost:5173/ in the Location field.

We will set the Access Token in the environment variables and load it when we need to initialize the Storyblok API connection in our Svelte code.

If you need more instructions to create a new Storyblok space, you can read this short tutorial: https://dev.to/robertobutti/how-to-set-up-a-storyblok-space-with-the-community-plan-for-local-development-1i37

Initialize Storyblok in the SvelteKit project

Create a new file src/lib/storyblok.js to initialize Storyblok:

// @ts-nocheck
// 001 - Import Access token and region from env variables
import { PUBLIC_ACCESS_TOKEN, PUBLIC_REGION } from "$env/static/public";
import { apiPlugin, storyblokInit } from "@storyblok/svelte";

export async function useStoryblok(accessToken = "") {
  // 002 - Calling the storyblokInit from storyblok/svelte package
  storyblokInit({
    // 003 - Loading and using the access token
    accessToken: accessToken === "" ? PUBLIC_ACCESS_TOKEN : accessToken,
    // 004 - Using `apiPlugin` provided by Storyblok SDK to connect to API
    use: [apiPlugin],
    bridge: true,
    // 005 - Listing all the components
    components: {
      feature: (await import("$lib/../components/Feature.svelte")).default,
      grid: (await import("$lib/../components/Grid.svelte")).default,
      page: (await import("$lib/../components/Page.svelte")).default,
      teaser: (await import("$lib/../components/Teaser.svelte")).default,
    },
    apiOptions: {
      https: true,
      cache: {
        type: "memory",
      },
      region: PUBLIC_REGION, // "us" if your space is in US region
    },
  });
}


Enter fullscreen mode Exit fullscreen mode
  • 001: Import Access token and region from env variables;
  • 002: Calling the storyblokInit from storyblok/svelte package
  • 003: Loading and using the access token
  • 004: Using apiPlugin provided by Storyblok SDK to connect to API
  • 005: Dynamically import and list components for Storyblok

Now that you've created the storyblok.js file, you can add your API key to the .env file. If the file doesn't exist, you can create a new one:

PUBLIC_ACCESS_TOKEN=yourpreviewaccesstoken
PUBLIC_REGION=eu
Enter fullscreen mode Exit fullscreen mode

Setup route [slug]

Create a dynamic route by adding a file: src/routes/[slug]/+page.ts and src/routes/[slug]/+page.svelte.

Loading Storyblok data in +page.ts

In src/routes/[slug]/+page.ts, fetch data from Storyblok:

import { useStoryblok } from "$lib/storyblok";
import { useStoryblokApi } from "@storyblok/svelte";
export const prerender = true;

/** @type {import('./$types').PageLoad} */
export async function load({ params }) {
  const slug = params.slug ?? "home";
  await useStoryblok();
  const storyblokApi = await useStoryblokApi();

  return storyblokApi
    .get(`cdn/stories/${slug}`, {
      version: "draft",
    })
    .then((dataStory) => {
      return {
        story: dataStory.data.story,
        error: false,
      };
    })
    .catch((error) => {
      return {
        story: {},
        error: error,
      };
    });
}
Enter fullscreen mode Exit fullscreen mode

This SvelteKit load function (in the src/routes/[slug]/+page.ts file) fetches a Storyblok story based on the URL slug, defaulting to "home" if no slug is provided.
It initializes Storyblok, retrieves the story in "draft" mode, and returns it. If the fetch fails, it returns an empty story and an error object.
The prerender = true setting ensures static generation when possible

Creating the +page.svelte

In src/routes/[slug]/+page.svelte, render the loaded data:

<script lang="ts">
import { useStoryblok } from "$lib/storyblok";
import { StoryblokComponent, useStoryblokBridge } from "@storyblok/svelte";
import { onMount } from "svelte";
import type { PageData } from "./$types";

const { data }: { data: PageData } = $props();

const story = $state(data.story);
let loaded = $state(false);
const datetime = new Date();

onMount(async () => {
  await useStoryblok();
  loaded = true;

  useStoryblokBridge(
    data.story.id,
    (newStory) => {
      story.content = newStory.content;
    },
    {
      // resolveRelations: ["popular-articles.articles"],
      preventClicks: true,
      resolveLinks: "url",
    },
  );
});
</script>

<div>

    {#if data.error}
      ERROR {data.error.message}
    {/if}

    {#if ! loaded }
    <div>Loading...</div>
    {:else if story && story.content}
      <StoryblokComponent blok={story.content} />
    {:else}
    <div>Getting Story</div>
    {/if}
</div>
Enter fullscreen mode Exit fullscreen mode

The +page.svelte file integrates with Storyblok for real-time content updates. It initializes Storyblok on mount, tracks the story state, and listens for live edits using useStoryblokBridge.
If an error occurs, it displays a message; otherwise, it dynamically renders the story's content with StoryblokComponent.
The loaded flag ensures the UI updates only after Storyblok is ready.

Creating the frontend components

To dynamically render Storyblok components, map them to Svelte components. For example, in the src/components directory, create a Page.svelte file for rendering the Storyblok content type page and then the Feature.svelte for rendering the Storyblok component feature. Do the same for the grid and the teaser components.

The Page.svelte

The Page.svelte is the Content type that wraps all the components of the page

<script lang="ts">
import { StoryblokComponent, storyblokEditable } from "@storyblok/svelte";

const { blok } = $props();
</script>

<div use:storyblokEditable={blok} class="container">
  {#each blok.body as item}
    <div>
      <StoryblokComponent blok={item} />
    </div>
  {/each}
</div>
Enter fullscreen mode Exit fullscreen mode

The use:storyblokEditable directive is a feature the Storyblok Svelte SDK provides to enable seamless integration with the Storyblok Visual Editor.

By adding use:storyblokEditable={blok} to an element, you mark it as an editable block. This allows Storyblok to inject metadata for the editor to recognize the block, enabling a smooth content-editing experience where changes made in the editor are instantly reflected in the preview without requiring page refreshes or additional setup.

The Feature.svelte

<script lang="ts">
import { storyblokEditable } from "@storyblok/svelte";

const { blok } = $props();
const datetime = new Date();
</script>

<article use:storyblokEditable={blok}>
  {#if blok.image && blok.image.filename}
    <figure class="">
      <img src="{blok.image.filename}/m/800x600" alt={blok.image.alt} />
    </figure>
  {/if}
  {blok.name}
  <p>{ datetime }</p>
</article>
Enter fullscreen mode Exit fullscreen mode

The Grid.svelte

<script lang="ts">
import { StoryblokComponent, storyblokEditable } from "@storyblok/svelte";

const { blok } = $props();
</script>

<div use:storyblokEditable={blok} class="grid">
  {#each blok.columns as item (item._uid)}
    <div>
      <!-- Component UID: {item._uid} -->
      <StoryblokComponent blok={item} />
    </div>
  {/each}
</div>
Enter fullscreen mode Exit fullscreen mode

The Teaser.svelte

<script lang="ts">
import { storyblokEditable } from "@storyblok/svelte";

const { blok } = $props();
</script>

<article use:storyblokEditable={blok}>
  {blok.headline}
</article>
Enter fullscreen mode Exit fullscreen mode

Quick recap of what we did

  • Created a new SvelteKit project: initialized a new SvelteKit project using Bun for a fast and modern development experience.
  • Added the Storyblok Svelte SDK: installed the SDK to enable integration with Storyblok's CMS features.
  • Added environment variables: set up environment variables to manage the Storyblok Preview Access Token.
  • Created the storyblok.js file: configured a central file to initialize the connection to Storyblok and handle component mapping logic.
  • Loaded Storyblok content in `src/routes/[slug]/+page.ts file: retrieved dynamic content from Storyblok using its API and made it available to the page.
  • Rendered the route in src/routes/[slug]/+page.svelte file: used the data from +page.ts to display content and implemented the Storyblok Bridge in the onMount callback for Visual Editor preview.
  • Created Svelte components: built reusable components like Page.svelte, Feature.svelte, etc., to render Storyblok content dynamically.

Now you can run your local server via bun dev

Your new SvelteKit project in the Storyblok Visual Editor

Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (1)

Collapse
 
mal_clairvoyant_0fce7f89a profile image
Mal Clairvoyant • Edited

Hi,

Will this work if I load the data in a +page.server.ts instead of a +page.ts file when setting up the route?

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay