Add a headless CMS with live preview to Svelte and Sapper in 5 minutes

Contents

In this short tutorial we will show how to you integrate the Storyblok API into a Svelte app and Sapper. Step by step we will build the components and develop the integration using Storyblok's SDK storyblok-js-client. The final result will be following:

You can clone this tutorial at https://github.com/storyblok/storyblok-svelte-boilerplate

Environment Setup

Requirements

To follow this tutorial there are the following requirements:

  • Understanding of Svelte and Sapper

  • Node, yarn (or npm) and npx installed

  • An account on Storyblok to manage content

Setup the project

As you can see, we will use Sapper to build our app. Sapper provides routing, SSR and export of our application in static files. To install the Sapper boilerplate execute the following command in your terminal:

npx degit "sveltejs/sapper-template#webpack" storyblok-svelte # you can use rollup too

After this step enter the folder created and install the package storyblok-js-client:

cd storyblok-svelte
yarn add storyblok-js-client # you can use npm install storyblok-js-client

This command will install all dependencies.

Run the Sapper dev command with:

yarn dev # or npm run dev

Open your browser in http://localhost:3000. The result will be following:

Create the file storyblokClient.js in src that will work as service to connect with the Storyblok API:

import StoryblokClient from 'storyblok-js-client'

const client = new StoryblokClient({
  accessToken: '<YOUR_TOKEN>' // replace with your accessToken
})

export const defaultRequestConfig = {
  version: 'draft'
}

export default client

Now edit the file _layout.svelte in the src/routes folder to get all stories from Storyblok and pass them to the Nav.svelte component:

<script context="module">
  import client, { defaultRequestConfig as reqConfig } from '../storyblokClient'
  
  export async function preload(page, session) {
    const response = await client.getAll('cdn/stories', reqConfig)
  
    return { stories: response || [] }
  }
</script>

<script>
  import Nav from '../components/Nav.svelte'
  
  export let stories = []
  export let segment
</script>

<style>
  main {
    position: relative;
    max-width: 56em;
    background-color: white;
    padding: 2em;
    margin: 0 auto;
    box-sizing: border-box;
  }
</style>

<Nav {segment} {stories} />
<main>
  <slot></slot>
</main>

Edit the Nav.svelte component to receive the stories as a property and iterate over them to create a the navigation bar:

<script>
  export let segment
  export let stories = []
</script>

<style>
  /* nothing changes here */
</style>

<nav>
  <ul>
    <li class={segment === undefined ? 'selected' : ''}>
      <a href='.'> Index </a>
    </li>
    {#each stories as story}
      <li class={story.full_slug === segment ? 'selected' : ''}>
        <a href={story.full_slug}> {story.name} </a>
      </li>
    {/each}
  </ul>
</nav>

Now create a file called [slug].svelte in src/routes. This file will be necessary to dynamically generate the routes and get the data from the Storyblok API:

<script context="module">
  import client, { defaultRequestConfig as reqConfig } from '../storyblokClient'

  export async function preload(page, session) {
    const { slug } = page.params

    const response = await client.get('cdn/stories/' + slug, reqConfig)

    return { story: response.data.story || {} }
  }
</script>

<script>
  export let story = {}
</script>

<svelte:head>
  <title>{story.name}</title>
</svelte:head>

<h1> Page: {story.name} </h1>

Open your browser again and you will see the navigation bar:

Click in the Home link in navigation bar to test if it works:

Setup components

Now we will create some components to render the content blocks from Storyblok. All the components will be create in the folder src/components.

Page component

Create a file called Page.svelte in src/components:

<script>
  import getComponent from './index'
  export let blok
</script>

<div>
  {#each blok.body as blok}
    <svelte:component
      blok={blok}
      this={getComponent(blok.component)}
    />
  {/each}
</div>

Grid component

Create a file called Grid.svelte in src/components:

<script>
  import getComponent from './index'

  export let blok
</script>

<div class="grid">
  {#each blok.columns as blok}
    <svelte:component blok={blok} this={getComponent(blok.component)} />
  {/each}
</div>
Teaser component

Create a file called Teaser.svelte in src/components:

<script>
  export let blok
</script>

<div class="teaser">
  {blok.headline}
</div>

Feature component

Create a file called Feature.svelte in src/components:

<script>
  export let blok
</script>

<div class="column feature">
  { blok.name }
</div>

404 component

Create a file called 404.svelte in src/components:

<script>
  export let block
</script>

<div>The component {block.component} has not been created yet.</div>

The getComponent function

What does the getComponent function do? Svelte has a special tag called svelte:component. This tag is able to render components dynamically. But the this parameter will must be a Svelte component or null and not a string like you get it from the Storyblok API. Because of this you need to create the file index.js in src/components that gets a Svelte component from Storyblok's component name. If it does not a find a component it returns a NotFound component.

import Grid from './Grid.svelte'
import Teaser from './Teaser.svelte'
import Feature from './Feature.svelte'
import Page from './Page.svelte'
import NotFound from './404.svelte'

const Components = {
  grid: Grid,
  teaser: Teaser,
  feature: Feature,
  page: Page
}

export default component => {
  // component does exist
  if (typeof Components[component] !== "undefined") {
    return Components[component]
  }

  return NotFound
}

Now we will edit the file [slug].svelte in src/routes:

<script context="module">
  import client, { defaultRequestConfig as reqConfig } from '../storyblokClient'
  import getComponent from '../components'

  export async function preload(page, session) {
    const { slug } = page.params

    const response = await client.get('cdn/stories/' + slug, reqConfig)

    return { story: response.data.story || {} }
  }
</script>

<script>
  export let story = {}
</script>

<svelte:head>
  <title>{story.name}</title>
</svelte:head>

<h1> Page: {story.name} </h1>

{#if story.content.component}
  <svelte:component
    blok={story.content}
    this={getComponent(story.content.component)}
  />
{/if}

Refresh your browser to test if it is working:

Live Preview Setup

To use Storyblok's live preview feature it is necessary to do the following changes:

  1. Add a page for the Editor

  2. Create a directive called editable and add it in your components

Add the Editor Page

The Editor page will be a page that syncs the changes from Storyblok. Create the file editor.svelte in src/routes folder with the following content:

<script>
  import { onMount } from 'svelte'
  import getComponent from '../components'
  import client, { defaultRequestConfig as reqConfig } from '../storyblokClient'

  async function loadStory() {
    const slug = window.storyblok.getParam('path')
    const response = await client.get('cdn/stories/' + slug, reqConfig)

    story = response.data.story || {}
  }

  export let story = {
    content: {
      component: null
    }
  }

  const loadStoryblokBridge = function(cb) {
    let script = document.createElement('script')
    script.type = 'text/javascript'
    script.src = `//app.storyblok.com/f/storyblok-latest.js?t=kWq6R3vdEgig4HX2bFtDQAtt`
    script.onload = cb
    document.getElementsByTagName('head')[0].appendChild(script)
  }

  const initStoryblokEvents = () => {
    loadStory()

    let sb = window.storyblok

    sb.on(['change', 'published'], (payload) => {
      loadStory()
    })

    sb.on('input', (payload) => {
      if (story && payload.story.id === story.id) {
        payload.story.content = sb.addComments(payload.story.content, payload.story.id)
        story = payload.story || {}
      }
    })

    sb.pingEditor(() => {
      if (sb.inEditor) {
        sb.enterEditmode()
      }
    })
  }

  onMount(() => {
    loadStoryblokBridge(() => initStoryblokEvents())
  })
</script>

<svelte:head>
  <title>{story.name}</title>
</svelte:head>

{#if story.content.component}
  <svelte:component
    blok={story.content}
    this={getComponent(story.content.component)}
  />
{/if}

Now we will create the file directives.js in the folder src with the following content:

const addClass = function(el, className) {
  if (el.classList) {
    el.classList.add(className)
  } else if (!new RegExp('\\b'+ className+'\\b').test(el.className)) {
    el.className += ' ' + className
  }
}

export const editable = (el, content) => {
  if (typeof content._editable === 'undefined') {
    return
  }

  var options = JSON.parse(content._editable.replace('<!--#storyblok#', '').replace('-->', ''))

  el.setAttribute('data-blok-c', JSON.stringify(options))
  el.setAttribute('data-blok-uid', options.id + '-' + options.uid)

  addClass(el, 'storyblok__outline')
}

Add the created directive in the Teaser and Feature components like following:

<script>
  import { editable } from '../directives'

  export let blok
</script>

<div use:editable={blok} class="teaser">
  {blok.headline}
</div>

In the Feature component:

<script>
  import { editable } from '../directives'

  export let blok
</script>

<div use:editable={blok} class="column feature">
  { blok.name }
</div>

Now you need to open your Storyblok space and adjust the Location setting to http://localhost:3000/editor?path=:

Before accessing the Home page we will add some styles to our application. Add following code in head section of the file template.html in the folder src :

<!-- inside head html tag -->
<link rel="stylesheet" href="https://rawgit.com/DominikAngerer/486c4f34c35d514e64e3891b737770f4/raw/db3b490ee3eec14a2171ee175b2ee24aede8bea5/sample-stylings.css">
<!-- head -->

Open the home content item and you will see your project inside an iframe with clickable content elements:

Now you are able to edit components, change the text and add other components and the live preview will be automatically sync the changes that you made.

Conclusion

In this tutorial we learned how to integrated Storyblok API in a Sapper project. We created some components and configured the Live Preview functionality using directives in Svelte. You can clone this tutorial at https://github.com/storyblok/storyblok-svelte-boilerplate

ResourceLink
Github repository of this tutorialhttps://github.com/storyblok/storyblok-svelte-boilerplate
SvelteSvelte
SapperSapper
Storyblok AppStoryblok

About the author

Emanuel Gonçalves

Emanuel Gonçalves

Working as frontend engineer at Storyblok. He loves contributing to the open source community and studies artificial intelligence in his spare time. His motto is "To go fast, go alone; to go far, go together".


More to read...