Add a headless CMS to Next.js in 5 minutes

Contents

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.

Environment Setup

Requirements

To follow this tutorial there are the following requirements:

  • Basic understanding of Next.js and React

  • Node, yarn (or npm) and npx installed

  • An account on Storyblok to manage content

  • A new Storyblok space

WARN::

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.

NextJs Landing Page

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.

Next.js Storyblok Landing Page

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:

Storyblok Components

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.

Warn::

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.

Storyblok Live Preview

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.

Preview Mode in Storyblok

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.