⚠️Draft Content

Storyblok Raises $80M Series C - Read News

What’s the True Total Price of Enterprise CMS? Find out here.

Skip to main content

Add a headless CMS to Next.js in 5 minutes

Try Storyblok

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

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.

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:

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 at 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]
});

...
    

Setting the correct region

Depending on whether your space was created in the EU, the US, Australia, Canada, or China, you may need to set the region parameter of the API accordingly:

  • eu (default): For spaces created in the EU
  • us: For spaces created in the US
  • ap: For spaces created in Australia
  • ca: For spaces created in Canada
  • cn: For spaces created in China

Here's an example for a space created in the US:

        
      apiOptions: {
  region: "us",
},
    
WARN:

Note: For spaces created in any region other than the EU, the region parameter must be specified.

This storyblokInit 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.

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

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

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.

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.

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-

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

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.

ResourceLink
Storyblok Next.js Ultimate Tutorialhttps://www.storyblok.com/tp/nextjs-headless-cms-ultimate-tutorial
Stackblitz Demohttps://stackblitz.com/edit/nextjs-5-minutes
Next.js Technology HubStoryblok Next.js Technology Hub
Storyblok React SDKstoryblok/storyblok-react
Next.js DocumentationNext.js Docs

Author

Facundo Giuliani

Facundo Giuliani

Facundo is a Developer Relations Engineer at Storyblok. From Buenos Aires, Argentina, he has more than 15 years of experience in software development. Full Stack Developer. Auth0 Ambassador. Prisma Ambassador. Cloudinary Media Developer Expert. He is also an open-source contributor.