Add a headless CMS with live preview to Svelte and Sapper in 5 minutes
Storyblok is the first headless CMS that works for developers & marketers alike.
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 (opens in a new window) . 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 (opens in a new window) and Sapper (opens in a new window)
- Node, yarn (or npm) and npx installed
- An account on Storyblok (opens in a new window) 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 (opens in a new window) :
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 (opens in a new window) . 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 blok
</script>
<div>The component {blok.component} has not been created yet.</div>
The getComponent function
What does the getComponent
function do? Svelte has a special tag called svelte:component (opens in a new window) . 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:
- Add a page for the Editor
- 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 (opens in a new window) 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
Resource | Link |
---|---|
Github repository of this tutorial | https://github.com/storyblok/storyblok-svelte-boilerplate |
Svelte | Svelte |
Sapper | Sapper |
Storyblok App | Storyblok |