Add a headless CMS to Next.js in 5 minutes
In this short tutorial, we will explore how to integrate the Storyblok API into a Next.js application and enable the live-preview in the Visual Editor. We will use Storyblok's SDK storyblok-js-client to load our data from Storyblok and enable Next.js preview mode to preview our changes.
The project in this article was developed using the following versions of these technologies:
- Next.js v10.0.5
- Nodejs v12.18.0
- Npm v6.14.9
Keep in mind that these versions may be slightly behind the latest ones.
Setup the project
Let's start by creating a new Next.js application.
npx create-next-app basic-nextjs
# or
yarn create next-app basic-nextjs
Next, we need to install the following packages storyblok-js-client and storyblok-react:
cd basic-nextjs
yarn add storyblok-js-client storyblok-react axios # or npm install
Then let's start the development server
yarn dev # or npm run dev
Open your browser in http://localhost:3000. You should see the following screen.

Connecting to Storyblok
Now that we kickstarted our project, we need to create a connection to Storyblok and enable the Visual Editor. We will create three main files to integrate with Storyblok: DynamicComponent
, useStoryblok
and Storyblok
.
import DynamicComponent from '../components/DynamicComponent'
import useStoryblok from "../lib/storyblok-hook"
import Storyblok from "../lib/storyblok"
DynamicComponent
is a wrapper around our components to load the correct components and enable live editing, when we click a component. The useStoryblok
hook is necessary to enable live updates in the Visual Editor and finally, we need a Storyblok
client to request content from the API.
Creating the Storyblok Client
We will need to create a new client to access our Storyblok API. Create a new folder lib
with a file storyblok.js
with the following code. You will need to add your preview token from your Space Settings under API-Keys.
lib/storyblok.js
import StoryblokClient from 'storyblok-js-client'
const Storyblok = new StoryblokClient({
accessToken: 'your-preview-token',
cache: {
clear: 'auto',
type: 'memory'
}
})
export default Storyblok
Fetching Data
To fetch data, we will make use of Next.js getStaticProps function. Add the following code to the pages/index.js
file.
pages/index.js
import Head from "next/head"
import styles from "../styles/Home.module.css"
// The Storyblok Client
import Storyblok from "../lib/storyblok"
export default function Home(props) {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<header>
<h1>
{ props.story ? props.story.name : 'My Site' }
</h1>
</header>
<main>
</main>
</div>
)
}
export async function getStaticProps(context) {
// the slug of the story
let slug = "home"
// the storyblok params
let params = {
version: "draft", // or 'published'
}
// checks if Next.js is in preview mode
if (context.preview) {
// loads the draft version
params.version = "draft"
// appends the cache version to get the latest content
params.cv = Date.now()
}
// loads the story from the Storyblok API
let { data } = await Storyblok.get(`cdn/stories/${slug}`, params)
// return the story from Storyblok and whether preview mode is active
return {
props: {
story: data ? data.story : false,
preview: context.preview || false
},
revalidate: 10,
}
}
If we open our Home Story now and set up the preview URL with our development server http://localhost:3000/ {1} and set the Real Path to '/' {2}, we should see the name of our story, which means we're already loading data from Storyblok.

Loading Components
To load the right components in Next.js, we will need a dynamic component, that can resolve the component names we get from Storyblok to actual components in our Next.js application. Let's create a new folder components
with a file DynamicComponent.js
with the following code:
components/DynamicComponent.js
import SbEditable from 'storyblok-react'
import Teaser from './Teaser'
// resolve Storyblok components to Next.js components
const Components = {
'teaser': Teaser,
}
const DynamicComponent = ({blok}) => {
// check if component is defined above
if (typeof Components[blok.component] !== 'undefined') {
const Component = Components[blok.component]
// wrap with SbEditable for visual editing
return (<SbEditable content={blok}><Component blok={blok} /></SbEditable>)
}
return (<p>The component <strong>{blok.component}</strong> has not been created yet.</p>)
}
export default DynamicComponent
By wrapping any component with the SbEditable
component, we can make all components loaded here clickable in Storyblok. If you want to control, which components are clickable, you can move the SbEditable
directly into the components. Finally, we have to also create the Teaser.js
component that was imported in the DynamicComponent
:
components/Teaser.js
import React from 'react'
const Teaser = ({blok}) => {
return (
<h2>{blok.headline}</h2>
)
}
export default Teaser
To display the components, we will need to load them in our <main>
element in the pages/index.js
file:
import Head from "next/head"
import styles from "../styles/Home.module.css"
// The Storyblok Client
import Storyblok from "../lib/storyblok"
import DynamicComponent from '../components/DynamicComponent'
export default function Home(props) {
const story = props.story
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<header>
<h1>
{ story ? story.name : 'My Site' }
</h1>
</header>
<main>
{ story ? story.content.body.map((blok) => (
<DynamicComponent blok={blok} key={blok._uid}/>
)) : null }
</main>
</div>
)
}
export async function getStaticProps(context) {
let slug = "home"
let params = {
version: "draft", // or 'published'
}
if (context.preview) {
params.version = "draft"
params.cv = Date.now()
}
let { data } = await Storyblok.get(`cdn/stories/${slug}`, params)
return {
props: {
story: data ? data.story : false,
preview: context.preview || false
},
revalidate: 10,
}
}
Once you loaded the components you should be able to see the available components in your Storyblok Live Preview:

Enabling the Visual Editor & Live Preview
To enable the Visual Editor, we need to connect the Storyblok Bridge. Then we will need to add a React hook to create live updating of the story content.
Adding the Storyblok Bridge
To do that we have to add a specific <script>
tag to the end of our document.
<script src="https://app.storyblok.com/f/storyblok-latest.js?t=YOUR_PREVIEW_TOKEN" id="storyblokBridge"></script>
Create a new file lib/storyblok-hook.js
with the following code:
import { useEffect, useState } from "react";
import Storyblok from "../lib/storyblok";
export default function useStoryblok(originalStory) {
let [story, setStory] = useState(originalStory);
// adds the events for updating the visual editor
// see https://www.storyblok.com/docs/guide/essentials/visual-editor#initializing-the-storyblok-js-bridge
function initEventListeners() {
if (window.storyblok) {
window.storyblok.init();
// reload on Next.js page on save or publish event in the Visual Editor
window.storyblok.on(["change", "published"], () => location.reload(true));
// live update the story on input events
window.storyblok.on("input", (event) => {
if (event.story.content._uid === story.content._uid) {
event.story.content = window.storyblok.addComments(
event.story.content,
event.story.id
);
setStory(event.story);
}
});
}
}
// appends the bridge script tag to our document
// see https://www.storyblok.com/docs/guide/essentials/visual-editor#installing-the-storyblok-js-bridge
function addBridge(callback) {
// check if the script is already present
const existingScript = document.getElementById("storyblokBridge");
if (!existingScript) {
const script = document.createElement("script");
script.src = `https://app.storyblok.com/f/storyblok-latest.js?t=${Storyblok.accessToken}`;
script.id = "storyblokBridge";
document.body.appendChild(script);
script.onload = () => {
// once the scrip is loaded, init the event listeners
callback();
};
} else {
callback();
}
}
useEffect(() => {
// first load the bridge, then initialize the event listeners
addBridge(initEventListeners);
});
return story;
}
Finally, we need to load this hook in our pages/index.js
file. By returning the revalidate
value in the getStaticProps
function, we enable our static content to be updated dynamically every 10s econds with Next.js Incremental Static Regeneration feature.
import Head from "next/head"
import styles from "../styles/Home.module.css"
// The Storyblok Client
import Storyblok from "../lib/storyblok"
import useStoryblok from "../lib/storyblok-hook"
import DynamicComponent from '../components/DynamicComponent'
export default function Home(props) {
// the Storyblok hook to enable live updates
const story = useStoryblok(props.story)
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<header>
<h1>
{ story ? story.name : 'My Site' }
</h1>
</header>
<main>
{ story ? story.content.body.map((blok) => (
<DynamicComponent blok={blok} key={blok._uid}/>
)) : null }
</main>
</div>
)
}
export async function getStaticProps(context) {
let slug = "home"
let params = {
version: "draft", // or 'published'
}
if (context.preview) {
params.version = "draft"
params.cv = Date.now()
}
let { data } = await Storyblok.get(`cdn/stories/${slug}`, params)
return {
props: {
story: data ? data.story : false,
preview: context.preview || false
},
revalidate: 10,
}
}
Once we added the Storyblok Bridge and the Storyblok hook, you should be able to click the Teaser component and see the live editing updates.
Since getStaticProps
creates your pages statically with an option for incremental static generation, it is highly recommended to combine it with preview mode in the next step, which allows you to see your live changes inside Storyblok.

Adding Preview Mode
Next.js offers a preview mode feature, to allow you to load either a draft version or the published version, depending on what you need. To add Next.js preview mode we will need to create two files pages/api/preview.js
and pages/api/exit-preview.js
.
pages/api/preview.js
export default async function preview(req, res) {
// Check the secret and next parameters
// This secret should only be known to this API route and the CMS
if (
req.query.secret !== process.env.MY_SECRET_TOKEN ||
!req.query.slug
) {
return res.status(401).json({ message: 'Invalid token' })
}
// Enable Preview Mode by setting the cookies
res.setPreviewData({})
// Set cookie to None, so it can be read in the Storyblok iframe
const cookies = res.getHeader('Set-Cookie')
res.setHeader('Set-Cookie', cookies.map((cookie) => cookie.replace('SameSite=Lax', 'SameSite=None')))
// Redirect to the entry location
let slug = req.query.slug
// Handle home slug
if(slug === 'home') {
slug = ''
}
// Redirect to the path from entry
res.redirect(`/${slug}`)
}
The preview.js
file will check for a secret token, that needs to be set on the server-side, and if the token matches, it will set the Next.js preview cookie. Since Storyblok loads your site inside an iframe, we need to change the SameSite
policy of this cookie to None
, so the preview cookie can also be read inside the iframe.
To exit the preview mode, we will need a api/exit-preview.js
file with the following code:
pages/api/exit-preview.js
export default async function exit(_, res) {
// Exit the current user from "Preview Mode". This function accepts no args.
res.clearPreviewData()
// set the cookies to None
const cookies = res.getHeader('Set-Cookie')
res.setHeader('Set-Cookie', cookies.map((cookie) => cookie.replace('SameSite=Lax', 'SameSite=None')))
// Redirect the user back to the index page.
res.redirect('/')
}
Once you created these files, you need to add a new preview URL for the http://localhost:3000/api/preview
path {1} with a secret
parameter {2} containing the token you set on the preview function and the slug of the story you want to preview {3}. Once you open this route with the correct secret token, you will be redirected to the current draft version of the story and the Next.js preview cookie will be set. To exit preview mode just add another preview URL for http://localhost:3000/api/exit-preview
. Once you open this exit preview path, the cookie for the preview will be reset and you should see the published version.

Loading the Draft Version only in Preview Mode
If we want to see the published version and then only see the editable draft version once we're inside this preview mode, we need to adapt our pages/index.js
file at the point where we're loading the bridge. We will only initialize the bridge if we're in preview mode.
import Head from "next/head"
import styles from "../styles/Home.module.css"
// The Storyblok Client
import Storyblok from "../lib/storyblok"
import useStoryblok from "../lib/storyblok-hook"
import DynamicComponent from '../components/DynamicComponent'
export default function Home({ story, preview }) {
// we only initialize the visual editor if we're in preview mode
story = useStoryblok(story, preview)
return (
<div className={styles.container}>
...
</div>
)
}
export async function getStaticProps(context) {
let slug = "home"
let params = {
version: "published", // or 'published'
}
if (context.preview) {
params.version = "draft"
params.cv = Date.now()
}
let { data } = await Storyblok.get(`cdn/stories/${slug}`, params)
return {
props: {
story: data ? data.story : false,
preview: context.preview || false
}
}
}
Since we're passing the preview variable to the useStoryblok
hook, we'll need to adapt the code in the use-storyblook.js
file:
lib/storyblok-hook.js
import { useEffect, useState } from "react";
import Storyblok from "../lib/storyblok";
export default function useStoryblok(originalStory, preview) {
let [story, setStory] = useState(originalStory);
function initEventListeners() {
..
}
function addBridge(callback) {
...
}
useEffect(() => {
// load the bridge if we're in preview mode
if(preview) {
addBridge(initEventListeners);
}
});
return story;
}
Pre-render Routes with getStaticPaths
Next.js offers the getStaticPaths functionality to pre-render dynamic routes. If you export an async
function called getStaticPaths
from a page that uses dynamic routes, Next.js will statically pre-render all the paths specified by getStaticPaths
. Add the following code to any page with dynamic routes like pages/[slug].js
to enable this feature.
pages/[slug].js
export async function getStaticPaths() {
let { data } = await Storyblok.get('cdn/links/', {})
let paths = []
Object.keys(data.links).forEach(linkKey => {
if (!data.links[linkKey].is_folder) {
if (data.links[linkKey].slug !== 'home') {
paths.push({ params: { slug: data.links[linkKey].slug } })
}
}
})
return {
paths: paths,
fallback: false
}
}
Conclusion
In this tutorial, we learned how to integrate Storyblok in a Next.js project. We created some components and configured the Live Preview functionality and added the Next.js Preview mode.