Skip to main content

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.

    Hint:

    Please note that this article has already been updated to match Storyblok V2. If you haven’t already started using it, you can find out how to make the switch here.

    Important:

    Our Next.js Ultimate Tutorial series is using Next.js 12. If you are using Next.js 13, there may be some breaking changes.

    In this short tutorial, we will explore how to integrate Storyblok into a Next.js application, and enable the live preview in the Visual Editor. We will use Storyblok's React SDK to load our data using the Storyblok API and enable the live editing experience. We will also see how to enable Next.js preview mode to preview our changes when working with static site generation.

    Hint:

    If you’re in a hurry, have a look at our live demo in Stackblitz!

    Alternatively, you can explore or fork the code from the Next Ultimate Tutorial GitHub Repository.

    Environment Setup

    Requirements

    To follow this tutorial there are the following requirements:

    • Basic understanding of JavaScript, React and Next.js

    • Node.js LTS version

    • An account in the Storyblok App

    Important:

    The project in this tutorial and its subsequent parts were developed using the following versions:

    • next@12.1.0

    • react@17.0.2

    • @storyblok/react@1.0.3

    Please 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

    Let's also install Storyblok's React SDK. This package allows us to interact with the Storyblok API and will help us to enable the real-time editing experience inside the Visual Editor.

    cd basic-nextjs
    
    npm install @storyblok/react
    # or
    yarn add @storyblok/react

    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

    Connecting to Storyblok

    Now that we kickstarted our project, we need to create a connection to Storyblok and enable the Visual Editor.

    To initialize the connection, go to pages/_app.js and add the following code.

    pages/_app.js
    ...
    
    import { storyblokInit, apiPlugin } from "@storyblok/react";
    
    storyblokInit({
      accessToken: "your-preview-token",
      use: [apiPlugin]
    });
    
    ...
    hint:

    For spaces created in the United States, you have to set the region parameter accordingly: { apiOptions: { region: 'us' } }.

    This function will do mainly two things: Initialize the connection with Storyblok (enabling the Visual Editor) and provide an API client that we can use to retrieve content from the platform, to be used in our application.

    In the Storyblok app, Create a new space and retrieve your Preview token {3} from your Space Settings {1} under Access Tokens {2}. Add the token as the accessToken directly, or from an .env file.

    Hint:

    If you want to use an env variable, you should follow this official Next.js tutorial. You should have a next.config.js file in your project, and add the env config storyblokApiToken: process.env.STORYBLOK_API_TOKEN, in order to set accessToken: process.env.storyblokApiToken in your storyblokInit function.

    app.storyblok.com
    Storyblok Preview Token
    1
    2
    3

    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 initialized (getStoryblokApi) and display the name of the story.

    pages/index.js
    import Head from "next/head"
    import styles from "../styles/Home.module.css"
    
    import { getStoryblokApi } from "@storyblok/react"
    
    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() {
      // home is the default slug for the homepage in Storyblok
      let slug = "home";
    
      // load the draft version
      let sbParams = {
        version: "draft", // or 'published'
      };
    
      const storyblokApi = getStoryblokApi();
      let { data } = await storyblokApi.get(`cdn/stories/${slug}`, sbParams);
    
      return {
        props: {
          story: data ? data.story : false,
          key: data ? data.story.id : false,
        },
        revalidate: 3600, // revalidate every hour
      };
    }

    Setting the Preview Url

    In the Storyblok app, go to Settings {1} > Visual Editor {2}, and set the Location (default environment) {3} to https://localhost:3010/.

    app.storyblok.com
    Preview URL
    1
    2
    3

    Preview URL

    For this tutorial, we will set up our dev server with an HTTPS proxy, to use a secure connection with the application. We'll use port 3010, so the URL to access our website will end up being https://localhost:3010/.

    HINT:

    If you don’t know how to setup an HTTPS proxy, you can read this guide to configure it on macOS, or this guide if you are a Windows user.

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

    app.storyblok.com
    Storyblok Content
    1
    2

    Storyblok Content

    Setting the Real Path

    We need to set the Real Path to / {1} 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.

    app.storyblok.com
    Set the Real Path
    1

    Set the Real Path

    Creating and loading 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 { storyblokEditable, StoryblokComponent } from "@storyblok/react";
    
    const Page = ({ blok }) => (
      <main {...storyblokEditable(blok)}>
        {blok.body.map((nestedBlok) => (
          <StoryblokComponent blok={nestedBlok} key={nestedBlok._uid} />
        ))}
      </main>
    );
    
    export default Page;
    components/Teaser.js
    import { storyblokEditable } from "@storyblok/react";
    
    const Teaser = ({ blok }) => {
      return <h2 {...storyblokEditable(blok)}>{blok.headline}</h2>;
    };
    
    export default Teaser;
    components/Grid.js
    import { storyblokEditable, StoryblokComponent } from "@storyblok/react";
    
    const Grid = ({ blok }) => {
      return (
        <div className="grid" {...storyblokEditable(blok)}>
          {blok.columns.map((nestedBlok) => (
            <StoryblokComponent blok={nestedBlok} key={nestedBlok._uid} />
          ))}
        </div>
      );
    };
    
    export default Grid;
    components/Feature.js
    import { storyblokEditable } from "@storyblok/react";
    
    const Feature = ({ blok }) => (
      <div className="column feature" {...storyblokEditable(blok)}>
        {blok.name}
      </div>
    );
    
    export default Feature;

    By using storyblokEditable with any component, we can make them loaded and clickable in the Storyblok Visual Editor, and we can edit its properties in real-time.

    To load the right content in Next.js, we will need a dynamic element that can resolve the component names we get from Storyblok API to the actual components in our Next.js application. For this purpose, we use the StoryblokComponent feature included in @storyblok/react. You can see how it works in the Page and Grid components, where we have the body and columns properties that can load any type of component.

    Finally, we need to configure the components identified by StoryblokComponent, and link them to their representation in the Storyblok space. To do that, let's go back to pages/_app.js and add a new parameter to storyblokInit call.

    pages/_app.js
    ...
    
    import { storyblokInit, apiPlugin } from "@storyblok/react";
    import Feature from "../components/Feature";
    import Grid from "../components/Grid";
    import Page from "../components/Page";
    import Teaser from "../components/Teaser";
    
    const components = {
      feature: Feature,
      grid: Grid,
      teaser: Teaser,
      page: Page,
    };
    
    storyblokInit({
      accessToken: "your-preview-token",
      use: [apiPlugin],
      components,
    });
    
    ...

    In order to display the components, let's include StoryblokComponent in our return function in the pages/index.js file:

    pages/index.js
    ...
    
    import { getStoryblokApi, StoryblokComponent } from "@storyblok/react"
    
    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>
       
             <StoryblokComponent blok={story.content} />
          </div>
        )
      }
    
    ...

    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.

    app.storyblok.com
    Storyblok Components
    1
    2

    Storyblok Components

    Optional: Changing Styles

    Let’s add TailwindCSS to our project for the ease of adding styles. You can refer to this guide for adding TailwindCSS to Next.js.

    After adding TailwindCSS, let’s add a couple of classes to the Teaser and Grid components.

    The Teaser.js file should look something like this -

    components/Teaser.js
    ...
    
    return 
      <h2 className="text-2xl mb-10" {...storyblokEditable(blok)}>
        {blok.headline}
      </h2>;
    
    ...

    Change the Grid.js code to the following -

    components/Grid.js
    ...
    
       <div className="grid grid-cols-3" {...storyblokEditable(blok)}>
          {blok.columns.map((nestedBlok) => (
            <StoryblokComponent blok={nestedBlok} key={nestedBlok._uid} />
          ))}
      </div>
    
    ...

    Change the Page.js code to the following -

    components/Page.js
    ...
    
      <main className="text-center mt-4" {...storyblokEditable(blok)}>
        {blok.body.map((nestedBlok) => (
          <StoryblokComponent blok={nestedBlok} key={nestedBlok._uid} />
        ))}
      </main>
    
    ...

    After removing the header and styles from Home.module.css in the index.js file, the home story now should look something like this-

    app.storyblok.com
    Home Story

    Home Story

    Enabling the Visual Editor & Live Preview

    So far we loaded our content from Storyblok, but we aren't able to directly select and edit the different components. To enable Storyblok's Visual Editor, we need to connect the Storyblok Bridge. In order to do that, we will use the useStoryblokState React hook provided by @storyblok/react, so we enable live updating for the story content.

    Let's load this hook in our pages/index.js file.

    pages/index.js
    ...
    
    import {
      useStoryblokState,
      getStoryblokApi,
      StoryblokComponent,
    } from "@storyblok/react";
    
    export default function Home({ story }) {
      story = useStoryblokState(story);
    
      return (
        <div>
          <Head>
            <title>Create Next App</title>
            <link rel="icon" href="/favicon.ico" />
          </Head>
    
          <header>
            <h1>{story ? story.name : "My Site"}</h1>
          </header>
    
          <StoryblokComponent blok={story.content} />
        </div>
      );
    }
    
    ...

    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.

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

    app.storyblok.com
    Storyblok Live Preview
    1

    Storyblok Live Preview

    Using Server Side Rendering

    If you don't want to use static rendering, you can also use Next.js server-side rendering. This also allows you to check for the _storyblok parameter that is passed to the visual editor iframe. Checking for this parameter, helps you to load your draft content only if you're inside Storyblok.

    pages/index.js
    ...
    
    export async function getServerSideProps(context) {
      // get the query object
      const insideStoryblok = context.query._storyblok;
    
      let slug = "home";
    
      let sbParams = {
        version: "published", // or 'draft'
      };
    
      if (insideStoryblok) {
        sbParams.version = "draft";
      }
    
      const storyblokApi = getStoryblokApi();
      let { data } = await storyblokApi.get(`cdn/stories/${slug}`, sbParams);
    
      return {
        props: {
          story: data ? data.story : false,
          key: data ? data.story.id : false,
        },
      };
    }
    
    ...

    Since getStaticProps doesn't have access to the request parameters, because it's building the site statically, we have to change to getServerSideProps in our index.js file. This means that instead of having pre-built pages, the pages are generated on the server on every request.

    HINT:

    We will look at how to conditionally load different data versions for Static Site in the next tutorials of this series.

    Inside the context object of getServerSideProps we have access to the query and we can check if the _storyblok parameter is present. Then, we load the draft version only if the parameter is present.

    Conclusion

    And that's it! We learned how to integrate Storyblok into a Next.js project. We saw how to manage and consume content using the Storyblok API, and how to enable a real-time visual experience using the Visual Editor. We went through different features that Next.js offers to create great user experiences: Static site generation, server-side rendering, etc.

    Next Part:

    In the next part of this series, we will see how to start making a real website with Next.js and Storyblok.

    Resource Link
    Storyblok Next.js Ultimate Tutorial https://www.storyblok.com/tp/nextjs-headless-cms-ultimate-tutorial
    Stackblitz Demo https://stackblitz.com/edit/nextjs-5-minutes
    Next.js Technology Hub Storyblok Next.js Technology Hub
    Storyblok React SDK storyblok/storyblok-react
    Next.js Documentation Next.js Docs

    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 marketing@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