Add a headless CMS to Next.js in 5 minutes

Contents
    Try Storyblok

    Storyblok is the first headless CMS that works for developers & marketers alike.

    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.

    You can find all the code for this tutorial and commits in our react-next-boilerplate repo.

    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.2.0
    • 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
    
    npm install storyblok-js-client storyblok-react axios
    # or
    yarn add storyblok-js-client storyblok-react axios

    Then, let's start the development server

    npm run dev
    # or
    yarn dev

    Open your browser in http://localhost:3000. You should see the following screen.

    NextJs Landing Page
    Hint:

    You can check the commit feat: init Next.js for the changes.

    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 two main files to integrate with Storyblok: DynamicComponent.js inside a components folder and storyblok.js inside a lib folder.

    import DynamicComponent from 'src/components/DynamicComponent'
    import Storyblok from 'src/lib/storyblok'

    DynamicComponent is a wrapper around our components to load the correct components and enable live editing, when we click a component. The storyblok.js file will contain a React hook to enable live updates in the Visual Editor and 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.

    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

    Create a new space and retrieve your preview token {3} from your Space Settings {1} under API-Keys {2}. Add the token to your Storyblok client in storyblok.js as the accessToken directly or from an .env file.

    Storyblok Preview Token

    Fetching Data

    To fetch data, we will make use of Next.js getStaticProps function. Add the following code to the pages/index.js file. This will load our home story using the client we just created and display the name of the story.

    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, 
      }
    }

    Let's open our Home Story now by clicking on Content {1} and then the Home Story {2}.

    Storyblok Content

    Setting the Preview Url

    Inside the home story, we need to set the preview URL to our development server http://localhost:3000/ {2}.

    Preview Url

    Setting the Real Path

    We need to set the Real Path to / {2} because we want to display the story with the slug home under our base path / and not /home. Once you set the preview URL and the real path, you should be able to see your development server inside Storyblok showing the name of the story Home.

    Next.js Storyblok Landing Page

    Creating the components

    In the next step, we have to create the components that already exist in the Home story: Page, Teaser, Grid and Feature. Create a new folder components with the following files:

    components/Page.js
    import DynamicComponent from "./DynamicComponent";
    
    const Page = ({ blok }) => (
      <main>
        {blok.body
          ? blok.body.map((blok) => (
              <DynamicComponent blok={blok} key={blok._uid} />
            ))
          : null}
      </main>
    );
    
    export default Page;
    
    components/Teaser.js
    import React from 'react'
    
    const Teaser = ({blok}) => {
      return (
        <h2>{blok.headline}</h2>
      )
    }
    
    export default Teaser
    components/Grid.js
    import React from 'react'
    import DynamicComponent from '../components/DynamicComponent'
    
    const Grid = ({ blok }) => {
      return (
        <div className="grid">
          {blok.columns.map((blok) =>
            (<DynamicComponent blok={blok} key={blok._uid}/>)
          )}
        </div>
      )
    }
     
    export default Grid
    components/Feature.js
    import React from 'react'
    
    const Feature = ({ blok }) => (
        <div className="column feature">
          {blok.name}
        </div>
    )
    
    export default Feature

    Loading Components dynamically

    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 file DynamicComponent.js inside our components folder with the following code:

    components/DynamicComponent.js
    import SbEditable from 'storyblok-react'
    import Teaser from './Teaser'
    import Grid from './Grid'
    import Feature from './Feature'
    import Page from './Page'
     
    // resolve Storyblok components to Next.js components
    const Components = {
      'teaser': Teaser,
      'grid': Grid,
      'feature': Feature,
      'page': Page,
    }
     
    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>)
      }
      
      // fallback if the component doesn't exist
      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.

    Showing the dynamic components

    To display the components, we will need to load them in our return function in the pages/index.js file (see Line 24):

    pages/index.js
    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>
     
           <DynamicComponent blok={story.content} />
        </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. It should show the Grid component {1} and the Teaser component {2}. If you change their order in Storyblok and click Save, they should dynamically switch their order on the page.

    Storyblok Components
    Hint:

    You can check the commit feat: add components for the changes.

    Enabling the Visual Editor & Live Preview

    So far we loaded our content from Storyblok, but we aren't able to directly select the different components. To enable Storyblok's Visual Editor, we need to connect the Storyblok Bridge. For this tutorial, we already use the new Storyblok Bridge Version 2. After loading the bridge, we will need to add a React hook to enable 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, whenever we want to enable it. This is mostly the case when you're inside the Storyblok editor.

    <script src="//app.storyblok.com/f/storyblok-v2-latest.js" type="text/javascript" id="storyblokBridge">
    </script>

    Inside our lib/storyblok.js file, add the following code after the client. In Line 12, we're creating a custom React hook called useStoryblok.

    Inside this hook, we have a function addBridge (Line 39), which basically just adds the script tag, if it's not already present. Once the loading of the bridge is completed (Line 58), it will call the initEventListeners function (Line 17) to enable input (Line 25) and save events (Line 22) inside Storyblok.

    lib/storyblok.js
    import { useEffect, useState } from "react";
    import StoryblokClient from "storyblok-js-client";
    
    const Storyblok = new StoryblokClient({
      accessToken: "your-preview-token",
      cache: {
        clear: "auto",
        type: "memory",
      },
    });
    
    export function useStoryblok(originalStory, preview) {
      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() {
        const { StoryblokBridge } = window
        if (typeof StoryblokBridge !== "undefined") {
          // initialize the bridge with your token
          const storyblokInstance = new StoryblokBridge();
    
          // reload on Next.js page on save or publish event in the Visual Editor
          storyblokInstance.on(["change", "published"], () =>
            location.reload(true)
          );
    
          // live update the story on input events
          storyblokInstance.on("input", (event) => {
            if (story && event.story.content._uid === story.content._uid) {
              setStory(event.story);
            }
          });
    
          storyblokInstance.on('enterEditmode', (event) => {
            // loading the draft version on initial enter of editor
            Storyblok
              .get(`cdn/stories/${event.storyId}`, {
                version: 'draft',
              })
              .then(({ data }) => {
                if(data.story) {
                  setStory(data.story)
                }
              })
              .catch((error) => {
                console.log(error);
              }) 
          }) 
        }
      }
    
      // 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 = "//app.storyblok.com/f/storyblok-v2-latest.js";
          script.id = "storyblokBridge";
          document.body.appendChild(script);
          script.onload = () => {
            // once the scrip is loaded, init the event listeners
            callback();
          };
        } else {
          callback();
        }
      }
    
      useEffect(() => {
          // only load inside preview mode
          if(preview) {
            // first load the bridge, then initialize the event listeners
            addBridge(initEventListeners);
          } 
      }, []);
    
      return story;
    }
    
    export default Storyblok;
    

    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 3600 seconds (1 hour) with Next.js Incremental Static Regeneration feature.

    pages/index.js
    import Head from "next/head"
    import styles from "../styles/Home.module.css"
     
    // The Storyblok Client & hook
    import Storyblok, { useStoryblok } from "../lib/storyblok"
    import DynamicComponent from '../components/DynamicComponent'
     
    export default function Home({ story, preview }) {
      story = useStoryblok(story, preview)
      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>
            <DynamicComponent blok={story.content} />
          </main>
        </div>
      )
    }
     
    export async function getStaticProps(context) {
      let slug = "home"
      let params = {
        version: "published", // or 'draft'
      }
     
      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: 3600, // revalidate every hour
      }
    }

    Hint:

    You can check the commit feat: add storyblok bridge & live updates for the changes.

    Once we added the Storyblok Bridge and the Storyblok hook, you should be able to click the Teaser and Feature components and see the live editing updates {1}.

    Storyblok Live Preview
    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.

    Dynamic route generation 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. Let's create a few different pages, by creating a folder pages {1} with two stories about {2} and contact similar to the home story.

    New Folder
    New Page

    Catch all routes in Next.js: [...slug].js

    To create all routes from Storyblok programmatically, we need to create a file pages/[...slug].js to dynamically create routes for all stories inside Storyblok. Add the following code to the file.

    On this dynamic page generation file we have a function getStaticPaths on Line 48, which reads all links from Storyblok. You can check what is returned from the links endpoint by appending your preview token to the following link: https://api.storyblok.com/v1/cdn/links?&starts_with=pages&version=draft&token=preview-token

    We then check each link that is returned from Storyblok and don't create a page for any folder or the home story itself, since that is already created in our index.js file (Line 53).

    Finally, we can read the slug of the page in getStaticProps with params.slug.join('/) (Line 28). Here the slug is an array because of the catch all rule in Next.js. Using that slug, we're requesting the correct entry from Storyblok on Line 37.

    pages/[...slug].js
    import React from "react";
    import DynamicComponent from "../components/DynamicComponent";
    import Head from "next/head";
    import styles from "../styles/Home.module.css";
    
    import Storyblok, { useStoryblok } from "../lib/storyblok";
    
    export default function Page({ story, preview }) {
      const enableBridge = true; // load the storyblok bridge everywhere
      // const enableBridge = preview; // enable bridge only in prevew mode
      story = useStoryblok(story, enableBridge);
    
      return (
        <div className={styles.container}>
          <Head>
            <title>{story ? story.name : "My Site"}</title>
            <link rel="icon" href="/favicon.ico" />
          </Head>
    
          <DynamicComponent blok={story.content} />
        </div>
      );
    }
    
    export async function getStaticProps({ params, preview = false }) {
      let slug = params.slug ? params.slug.join("/") : "home";
    
      let sbParams = {
        version: "draft", // or published
      };
    
      if (preview) {
        sbParams.version = "draft";
        sbParams.cv = Date.now();
      }
    
      let { data } = await Storyblok.get(`cdn/stories/${slug}`, sbParams);
    
      return {
        props: {
          story: data ? data.story : null,
          preview,
        },
        revalidate: 3600, // revalidate every hour
      };
    }
    
    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 || data.links[linkKey].slug === "home") {
          return;
        }
    
        // get array for slug because of catch all
        const slug = data.links[linkKey].slug;
        let splittedSlug = slug.split("/");
    
        paths.push({ params: { slug: splittedSlug } });
      });
    
      return {
        paths: paths,
        fallback: false,
      };
    }
    

    Learn:

    It’s also possible to make the catch-all routes optional by putting the ...slug in double brackets: [[...slug]].js. The main difference between catch-all and optional catch-all routes is that with optional, the route without the parameter is also matched.

    Hint:

    In order not to have to change the real path & slugs constantly, it makes sense to already create the correct folder structure in Storyblok and use the catch all routes.

    Custom routes in Next.js: [...slug].js

    Sometimes you only want to generate pages for certain folders with your own slugs. To create custom routes from a folder in Storyblok we adapt our [...slug].js file. Add the following code to the file.

    On Line 51, we're only reading links inside the pages folder. On Line 64 we're removing the pages prefix from the story, so we generate http://localhost:3000/about instead of http://localhost:3000/pages/about.

    Finally, we can read the slug of the page in getStaticProps withparams.slug (Line 26). Using that slug, we're requesting the correct entry from Storyblok on Line 38 from inside the pages folder.

    pages/[slug].js
    import React from "react";
    import DynamicComponent from "../components/DynamicComponent";
    import Head from "next/head";
    import styles from "../styles/Home.module.css";
    
    import Storyblok, { useStoryblok } from "../lib/storyblok";
    
    export default function Page({ story, preview }) {
      const enableBridge = true; // load the storyblok bridge everywhere
      // const enableBridge = preview; // enable bridge only in prevew mode
      story = useStoryblok(story, enableBridge, locale);
    
      return (
        <div className={styles.container}>
          <Head>
            <title>{story ? story.name : "My Site"}</title>
            <link rel="icon" href="/favicon.ico" />
          </Head>
    
          <DynamicComponent blok={story.content} />
        </div>
      );
    }
    
    export async function getStaticProps({ params, preview = false }) {
      let slug = params.slug ? params.slug.join("/") : "home";
    
      let sbParams = {
        version: "draft", // or published
      };
    
      if (preview) {
        sbParams.version = "draft";
        sbParams.cv = Date.now();
      }
    
      // load the stories insides the pages folder
      let { data } = await Storyblok.get(`cdn/stories/pages/${slug}`, sbParams);
    
      return {
        props: {
          story: data ? data.story : null,
          preview,
        },
        revalidate: 3600, // revalidate every hour
      };
    }
    
    export async function getStaticPaths() {
      let { data } = await Storyblok.get('cdn/links/', {
          starts_with: 'pages'
      })
    
      let paths = [];
      Object.keys(data.links).forEach((linkKey) => {
        // don't create routes for folders and the index page
        if (data.links[linkKey].is_folder || data.links[linkKey].slug === "home") {
          return;
        }
    
        // get array for slug because of catch all
        const slug = data.links[linkKey].slug;
        // remove the pages part from the slug
        let newSlug = slug.replace('pages', '')
        let splittedSlug = newSlug.split("/");
    
        paths.push({ params: { slug: splittedSlug } });
      });
    
      return {
        paths: paths,
        fallback: false,
      };
    }
    

    Once you created the pages/[slug].js file, you can open one of the stories inside the pages folder. Since Storyblok will automatically use the full slug including the folder name for the story, but we removed the pages/ part in Next.js, we will have to change the real path to use the same slug that is generated in Next.js {2}. If you have a lot of real paths to replace, the Advanced paths plugin can be helpful to do this for many stories.

    Dynamic Next.js routes
    Hint:

    You can check the commit feat: add catch all & custom routes for the changes.

    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.

    Warn:

    Next.js preview mode needs to set a cookie to work. Some browsers block setting of cookies on http, which is a problem on the local development server http://localhost:3000. You can find more details about this in our FAQ.

    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) {
        const { slug = '' } = req.query
        // get the storyblok params for the bridge to work
        const params = req.url.split('?')
        // Check the secret and next parameters
        // This secret should only be known to this API route and the CMS
        if (req.query.secret !== 'MY_SECRET_TOKEN') {
          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;Secure')))
     
      
        // Redirect to the path from entry
        res.redirect(`/${slug}?${params[1]}`)
      }
    

    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(req, res) {
        const { slug = '' } = req.query
        // 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;Secure')))
     
        // Redirect the user back to the index page.
        res.redirect(`/${slug}`)
      }

    Adding a preview URL

    Once you created these files, you need to add a new preview URL for the http://localhost:3000/api/preview path. Click on the URL {1} in the editor and then on the Add or change preview URLs {2} button

    Storyblok editing capabilities

    Inside the settings we will add two new preview URLs: Preview and Exit Preview. The first preview URL {1} will be http://localhost:3000/api/preview?secret=MY_SECRET_TOKEN&slug=

    The secret token needs to be the same as the token you set in your preview function in Next.js.

    Preview Url

    Once you open this route with the correct secret token, you should be redirected to the current draft version of the story and the Next.js preview cookie will be set. Storyblok will automatically append the slug for the current story for you. To exit preview mode just add another preview URL for http://localhost:3000/api/exit-preview?slug=. Once you open this exit preview path, the cookie for the preview will be reset and you should see the published version if you're loading the published version outside of the preview mode or if you're using the public token.

    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 (Line 10).

    pages/index.js
    import Head from "next/head"
    import styles from "../styles/Home.module.css"
     
    // The Storyblok Client & hook
    import Storyblok, { useStoryblok } from "../lib/storyblok"
    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 'draft'
      }
    
      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 storyblook.js file, by passing a the preview context to the useStoryblok function (Line 6) and then adding an if statement (Line 19), if we're inside the preview mode:

    lib/storyblok-hook.js
    import { useEffect, useState } from "react";
    import StoryblokClient from "storyblok-js-client";
    
    const Storyblok = new StoryblokClient({ ... });
    
    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;
    }
    

    Make sure you publish your stories once to make them available in the published versions of the Storyblok client. Now if you enter the preview mode {1} and change some content, you should see the live updates {2}.

    Inside Preview Mode

    Save, but don't publish the changes. Then enter the Exit Preview Url {1}. Now you should see the published content {2} instead of the draft version.

    Published Version
    Hint:

    You can check the commit feat: add preview mode for the changes.

    Using Server Side Rendering

    If you don't want to use static rendering and the Preview Mode, which are the most performant choice, you can also use Next.js server side rendering. This allows you to check for the _storyblok paramter that is passed to the visual editor iframe and the preview in new window. Checking for this parameter, allows you to load your draft content only if you're inside Storyblok without the preview mode. Since getStaticProps doesn't have access to the request parameters, because it's building the site statically, we have to change to getServerSideProps (Line 32) in our index.js file. This means instead of having pre-built pages, the pages are generated on the server side on every request.

    Inside the context object of getServerSideProps we have access to the query and can check if the _storyblok parameter is present (Line 34). Then we do the same as we would do with the preview mode and load the draft version only if the parameter is present.

    pages/index.js
    import Head from "next/head"
    import styles from "../styles/Home.module.css"
     
    // The Storyblok Client & hook
    import Storyblok, { useStoryblok } from "../lib/storyblok"
    import DynamicComponent from '../components/DynamicComponent'
     
    export default function Home({ story, preview }) {
      story = useStoryblok(story, preview)
      return (
        <div className={styles.container}>
          <Head>
            <title>{ story ? story.name : 'My Site' }</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 getServerSideProps(context) {
      // get the query object
      const insideStoryblok = context.query._storyblok
      const shouldLoadDraft = context.preview || insideStoryblok
      let slug = "home"
      let sbParams = {
        version: "published", // or 'draft'
      }
     
      if (shouldLoadDraft) {
        sbParams.version = "draft"
        sbParams.cv = Date.now()
      }
     
      let { data } = await Storyblok.get(`cdn/stories/${slug}`, sbParams)
     
      return {
        props: {
          story: data ? data.story : false,
          preview: shouldLoadDraft || 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.

    Resource Link
    Github Example Repo storyblok/react-next-boilerplate
    Next.js Documentation Next.js Docs
    Preview Mode Documentation Preview Mode

    Developer Newsletter

    Want to stay on top of the latest news and updates in Storyblok?
    Subscribe to Code & Bloks - our headless newsletter.

    An error occurred. Please get in touch with support@storyblok.com

    Please select at least one option.

    Please enter a valid email address.

    This email address is already registered.

    Please Check Your Email!

    Almost there! To confirm your subscription, please click on the link in the email we’ve just sent you. If you didn’t receive the email check your ’junk folder’ or