DEV Community

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

Posted on • Edited on

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.

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
import { apiPlugin, storyblokInit } from '@storyblok/svelte';
// 001 - Access token
import { PUBLIC_ACCESS_TOKEN, PUBLIC_REGION } from '$env/static/public';

export async function useStoryblok(accessToken = '') {
// 002 - Load access token
  accessToken = accessToken === '' ? PUBLIC_ACCESS_TOKEN : accessToken;

  storyblokInit({
// 003 - Using Access Token
    accessToken: accessToken,
// 004 -  // use apiPlugin provided by Storyblok SDK
    use: [apiPlugin],
    components: {
// 005 - List 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: load access token and region from environment variables;
  • 002: fallback to empty string if no token is provided
  • 003: use the access token to authenticate API requests
  • 004: add the Storyblok API plugin for fetching content
  • 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:

// 001 - Import Storyblok API helper
import { useStoryblokApi } from '@storyblok/svelte';
// 002 - Initialize Storyblok configuration
import { useStoryblok } from '$lib/storyblok';

/** @type {import('./$types').PageLoad} */
export async function load({ params }) {
// 003 - Get the slug from the route parameters, defaulting to 'home' if undefined
  let slug = params.slug ?? 'home';
// 004 - Initialize Storyblok with the provided configuration (await)
  await useStoryblok();
// 005 - Access the Storyblok API instance (await)
  let storyblokApi = await useStoryblokApi();

// 006 - Fetch the story data from Storyblok using the slug
  return storyblokApi
    .get(`cdn/stories/${slug}`, {
// 007 - Specify the draft version for previewing unpublished changes
      version: 'draft'
    })
    .then((dataStory) => {
// 008 - Return the fetched story data and indicate no errors
      return {
        story: dataStory.data.story,
        error: false
      };
    })
    .catch((error) => {
// 009 - Handle errors by returning an empty story and the error details
      return {
        story: {},
        error: error
      };
    });
}
Enter fullscreen mode Exit fullscreen mode

Creating the +page.svelte

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

<script lang="ts">
// 001 - Import onMount lifecycle hook
  import { onMount } from 'svelte';
// 002 - Import Storyblok utilities for real-time updates and rendering
  import { useStoryblokBridge, StoryblokComponent } from '@storyblok/svelte';
// 003 - Import type definitions for props
  import type { PageData } from './$types';
// 004 - Import custom Storyblok initialization logic
  import { useStoryblok } from '$lib/storyblok';
// 005 - Load data passed as props to the svelte file (returned from +page.ts file, load method)
  let { data }: { data: PageData } = $props();
// 006 - Initialize the story as state to handle live updates from the Visual Editor
  let story = $state(data.story);
// 007 - Track whether the Storyblok setup is complete
  let loaded = $state(false);

  onMount(async () => {
// 008 - Initialize Storyblok for API interaction and the Visual Editor
    await useStoryblok();
// 009 - Set the loaded flag to true to allow rendering after initialization
    loaded = true;
    if (story) {
// 010 - Attach the Storyblok Bridge to listen for real-time changes from the Visual Editor
    useStoryblokBridge(
// 011 - The story ID for which changes should be tracked
        data.story.id,
// 012 - Update the story state dynamically when changes are made
        (newStory) => (story = newStory), {
// 013 - Uncomment resolveRelations if you need to resolve specific relations like nested content
      // resolveRelations: ["popular-articles.articles"],
// 014 - Prevent default click behavior inside the Visual Editor
      preventClicks: true,
// 015 - Automatically resolve Storyblok links into proper URLs
      resolveLinks: 'url'
    });
    }
  });
</script>

<div>
  {#key story}
// 016 - Display an error message if there is an issue with fetching the data
    {#if data.error}
      ERROR {data.error.message}
    {/if}
// 017 - Render the story's content dynamically using the StoryblokComponent
    {#if loaded && story && story.content}
      <StoryblokComponent blok={story.content} />
    {/if}
  {/key}
</div>

Enter fullscreen mode Exit fullscreen mode

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 { storyblokEditable, StoryblokComponent } from '@storyblok/svelte';

  let { 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';

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

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

The Grid.svelte

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

  let { 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';

  let { 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

Top comments (0)