⚠️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 13 in 5 minutes

Try Storyblok

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

In this short tutorial, we will explore how to integrate Storyblok into a Next.js 13 application with app router and enable the live preview in the Visual Editor. We will use Storyblok's React SDK to load our data using the Storyblok API. We will see two different approaches, one of which uses the server-side components partially to enable the live editing experience and one that uses the server-side components fully.

Demo:

Wait no more, and get started with Next.js 13 Boilerplate right now!

Important:

If you use Next.js 14+ with App Router, we released a new version of our React SDK fully supporting Server Components, with live editor enabled for developers.

The integration steps to use the latest version are slightly different, you can find a detailed guide in the README of our repo.

Environment Setup

Requirements

To follow this tutorial, the following requirements need to be fulfilled:

  • Basic understanding of JavaScript, React, and Next.js
  • Node.js LTS version
  • An account in the Storyblok App
Important:

The project in this tutorial are developed using the following versions:

Please keep in mind that these versions may be slightly behind the latest ones.

Create a Next.js project

Following the Next.js Offical Docs, we can easily create a new Next.js project with the following command:

        
      npx create-next-app@latest <projecct-name>
# or
yarn create next-app <projecct-name>
    

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:

Next.js Landing Page

Next.js Landing Page

Configuration of the space

You can easily configure a new space by clicking Add Space {1} after having logged in to Storyblok.

Creating a new space in Storyblok
1

Creating a new space in Storyblok

Create a new space in the Storyblok app by choosing the Create space {1} option. Pick a name for it {2}. Optionally, you can choose between different server locations for your space {3} (if you choose the United States or China, please be mindful of the required API parameter explained hereinafter).

Creating a new space in Storyblok
1
2
3

Creating a new space in Storyblok

Shortly afterward, a Storyblok space with sample content has been created for you. Let’s open the Home story by first clicking on Content {1} and then on Home {2}:

Opening the Home story
1
2

Opening the Home story

Now you’ll see the default screen and the Visual Editor:

Visual Editor representing your Home story

Visual Editor representing your Home story

Enabling the Visual Editor

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.

In order to actually see the Next.js project in the Visual Editor, we’ll have to define the default environment URL. Let’s do that by going to Settings > Visual Editor {1} and setting the Location field to https://localhost:3010/ {2}:

Preview URL
1
2
3

Preview URL

Now, if you go back to the Home story, you won’t see your Next app there just yet.

Setting the Real Path

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

Set the Real Path

Connecting to Storyblok

Let's now connect Storyblok to our Next.js project.

First of all, let's install our official React SDK. This package allows us to interact with the Storyblok API and will help us to enable the real-time editing experience:

Install React SDK
        
      npm install @storyblok/react
# or
yarn add @storyblok/react
    

To initialize Storyblok in your Next.js Project, go to the layout.js inside the app directory of the project and add the storyblokInit function as follows:

app/layout.js
        
      import { storyblokInit, apiPlugin } from "@storyblok/react/rsc";

storyblokInit({
  accessToken: "your-preview-token",
  use: [apiPlugin],
});
    
Warn:

The SDK has a special module for App Router. Always import from @storyblok/react/rsc while using Server Components.

The implementation details differ slightly so be sure to check the README of our repo.

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

Storyblok Preview Token
1
2
3
Hint:

If you want to use an environment 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.

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 take care of two things: Initialize the connection with Storyblok (for 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.

Important:

If you use Next.js 14+ with App Router, the next steps are DEPRECATED: be sure to check the guide in the README of our repo.

With Live Editing Support

Note:

If you're in hurry, you can explore or fork the main branch of Next.js 13 Storyblok Boilerplate.

As all of the components are by default on the server side while using the new app directory, it limits the components' reactivity which is required to see real-time updates. In this approach, we create a wrapper to load the components on the client side. This allows us to take full advantage of the Live Editing, but the use of Server Side Components is partial. Let's create this wrapper.

Inside the components folder, create a new file and name it StoryblokProvider.js. Add the following code to it:

StoryblokProvider.js
        
      /** 1. Tag it as a client component */
"use client";
import { storyblokInit, apiPlugin } from "@storyblok/react/rsc";

/** 2. Initialize it as usual */
storyblokInit({
  accessToken: "your_preview_token",
  use: [apiPlugin],
});

export default function StoryblokProvider({ children }) {
  return children;
}
    

We are tagging this as a client component and we are reinitializing the Storyblok connection here again inside this file as we need to have it on the client as well. The initialization inside the layout file which we did previously will help us to fetch the data from Storyblok.

Now we need to use this wrapper in the layout. The layout.js file should have the following code:

layout.js
        
      import { storyblokInit, apiPlugin } from "@storyblok/react/rsc";
import StoryblokProvider from "../components/StoryblokProvider";

storyblokInit({
  accessToken: "your-preview-token",
  use: [apiPlugin]
});

export default function RootLayout({ children }) {
  return (
    <StoryblokProvider>
         <html lang="en">
             <body>{children}</body>
        </html>
    </StoryblokProvider>
  )
}

    

Fetching Data

We will fetch the data in the page.js file that is inside the app directory. Replace the code of the file with the following:

app/page.js
        
      import { getStoryblokApi} from "@storyblok/react/rsc";


export default async function Home() {
  const { data } = await fetchData();

  return (
    <div>
      <h1>Story: {data.story.name}</h1>
    </div>
  );
}

export async function fetchData() {
  let sbParams = { version: "draft" };

  const storyblokApi = getStoryblokApi();
  return storyblokApi.get(`cdn/stories/home`, sbParams, {cache: "no-store"});
}

    

We are using the getStoryblokApi object which is imported from @storyblok/react/rsc to fetch the content of the home story from Storyblok inside the fetchData function. You should be able to see the name of the story (Home) while viewing the website.

Rendering Dynamic Components in the Next.js App

The core idea of using Storyblok is the following:

  • Content managers (even if it’s only yourself) can create pages (or stories) composed of different components (or blocks)
  • Developers receive the page in the JSON format by using the Storyblok API and can render components accordingly (this is what we want to accomplish in our Next app). We already did this in our previous step.

When you create a new space from scratch, Storyblok automatically creates four default components for you:

  • page: Content type block
  • grid: Nested block
  • feature: Nested block
  • teaser: Nested block

You can find all of these in the Components section of your space.

hint:

Understand the difference between the nestable components and content type in our Structures of Content tutorial.

Creating Components in the Next.js app

Let’s create the counterparts of the four components discussed above in our Next.js app. For any component you have or you create inside Storyblok, you need to have a frontend component. Inside your components, create the following files:


Page.js
        
      import { storyblokEditable, StoryblokComponent } from "@storyblok/react/rsc";

const Page = ({ blok }) => (
  <main {...storyblokEditable(blok)}>
    {blok.body.map((nestedBlok) => (
      <StoryblokComponent blok={nestedBlok} key={nestedBlok._uid} />
    ))}
  </main>
);

export default Page;

    
Grid.js
        
      import { storyblokEditable, StoryblokComponent } from "@storyblok/react/rsc";

const Grid = ({ blok }) => {
  return (
    <div  {...storyblokEditable(blok)}>
      {blok.columns.map((nestedBlok) => (
        <StoryblokComponent blok={nestedBlok} key={nestedBlok._uid} />
      ))}
    </div>
  );
};

export default Grid;
    
Feature.js
        
      import { storyblokEditable } from "@storyblok/react/rsc";

const Feature = ({ blok }) => (
  <div  {...storyblokEditable(blok)}>
    {blok.name}
  </div>
);

export default Feature;
    
Teaser.js
        
      import { storyblokEditable } from "@storyblok/react/rsc";

const Teaser = ({ blok }) => {
  return <h2 {...storyblokEditable(blok)}>{blok.headline}</h2>;
};

export default Teaser;
    

By using storyblokEditable with any component, we can make them clickable in the Storyblok Visual Editor, and we can edit their 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 the SDK. 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 StroyblokProvider.js and add a new parameter to storyblokInit call:

StoryblokProvider.js
        
      "use client";
import { storyblokInit, apiPlugin } from "@storyblok/react/rsc";

/** Import your components */
import Page from "./Page";
import Teaser from "./Teaser";
import Feature from "./Feature";
import Grid from "./Grid";

const components = {
  feature: Feature,
  grid: Grid,
  teaser: Teaser,
  page: Page,
};

storyblokInit({
  accessToken: "your_preview_token",
  use: [apiPlugin],
  components
});

export default function StoryblokProvider({ children }) {
  return children;
}
    

We are now left with just one thing, which is to change the page.js file present inside the app directory in order to render the content we get from the API and enable live editing. So instead of displaying the story's name, let's render the components.

In order to do this, we need to import StoryblokStory component from "@storyblok/react/story". It takes a prop named story where you need to pass the story coming from the API. This component uses a hook behind the scenes to keep the state of the story for enabling live editing and renders everything with the StoryblokComponent itself.


The page.js file should now look like this:


app/page.js
        
      import { getStoryblokApi} from "@storyblok/react/rsc";
import StoryblokStory from "@storyblok/react/story";

export default async function Home() {
  const { data } = await fetchData();

  return (
    <div>
      <StoryblokStory story={data.story} />
    </div>
  );
}

export async function fetchData() {
  let sbParams = { version: "draft" };

  const storyblokApi = getStoryblokApi();
  return storyblokApi.get(`cdn/stories/home`, sbParams);
}

    

And that is it! Now if you open your website, you will see that it contains the content coming from the components we have inside the story, that is the Teaser and Grid components. Inside the Visual Editor, you will also be able to see the dotted lines which help in identifying the components. If you click over on any place inside the visual editor, you will see that the schema of that component opens up on the right-hand side.

Now you can start live editing by just clicking and filling in fields with your content. Play with changing the teaser headline or re-arranging the features and see the magic happen!

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, Grid, and Page components.

components/Teaser.js
        
      ...

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

...
    
components/Grid.js
        
      ...

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

...
    
components/Page.js
        
      ...

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

...
    

After adding the Tailwind classes to our components and removing the story name from page.js file, we should see the following:

Full React Server Components

Note:

If you're in hurry, you can clone or explore the full-server-side branch of Next.js 13 Storyblok Bolierplate.

In the previous approach, we saw how to enable live editing with the server components. This makes the use of server components partial. In this approach, we will see how to use the full potential of server components by keeping everything server-side.

Important:

This approach has a limitation. Real-time editing won't work if all the components are rendered on the server. Although, you can see the changes applied in the Visual Editor whenever you save or publish the changes applied to the story.

After initializing Storyblok by installing @storyblok/react and using the storyblokInit function inside the layout.js file, import StoryblokBridgeProvider and replace the code inside the file with the following:

layout.js
        
      import { storyblokInit, apiPlugin } from "@storyblok/react/rsc";
import StoryblokBridgeLoader from "@storyblok/react/bridge-loader";


storyblokInit({
  accessToken: "your-preview-token",
  use: [apiPlugin]
});

export default function RootLayout({ children }) {
  return (
     <html lang="en">
       <body>{children}</body>
       <StoryblokBridgeLoader options={{}} />
    </html>
  )
}

    

The StoryblokBridgeLoader loads the bridge on the client behind the scenes. Though live editing will not work, it will load the bridge so that inside the Visual Editor you're still able to click the components. Along with this, it will also register events so that the Visual Editor reloads when you click Save or Publish. You can pass bridge options through a prop named options.

Fetching Data and Rendering Components

Most of the part here remains similar to that we have in the previous approach. The fetching of the data and creation of the components remains the same.

You can refer to the previous section understand the setup of data fetching inside the page.js file and creating components. Once the components are created, we need to register them inside storyblokInit function. Let's do that inside the layout.js file.

layout.js
        
      import { storyblokInit, apiPlugin} from "@storyblok/react/rsc"
import StoryblokBridgeLoader from '@storyblok/react/bridge-loader'

import Page from "@/components/Page"
import Grid from "@/components/Grid"
import Feature from "@/components/Feature"
import Teaser from "@/components/Teaser"

export const metadata = {
  title: 'Storyblok and Next.js 13',
  description: 'A Next.js and Storyblok app using app router ',
}

storyblokInit({
  accessToken: 'your-access-token',
  use: [apiPlugin],
  components: {
    feature: Feature,
    grid: Grid,
    page: Page,
    teaser: Teaser
  }

})

export default function RootLayout({ children }) {
  return (
      <html lang="en">
        <body>{children}</body>
        <StoryblokBridgeLoader options={{}} />
      </html>
  )
}

    

We are left with just one thing now, that is to render these components dynamically after fetching the data inside the page.js file. We can do that by using the StoryblokComponent and passing the story's content.

page.js
        
      import {
  getStoryblokApi, StoryblokComponent
} from "@storyblok/react/rsc";


export default async function Home() {
  const { data } = await fetchData();

  return (
    <div>
      <h1>Story: {data.story.id}</h1>
      <StoryblokComponent blok={data.story.content} />
    </div>
  );
}

export async function fetchData() {
  let sbParams = { version: "draft" };

  const storyblokApi = getStoryblokApi();
  return storyblokApi.get(`cdn/stories/home`, sbParams);
}

    

And that's it. You should be able to see everything inside the Visual Editor now. Click on any component to open the schema on the right-hand side and change the content. Save or Publish to see the changes.

Wrapping up

Congratulations! We learned how to integrate Storyblok into a Next.js 13 project with app router. We went through two approaches, one with live-editing support where the use of server components is partial, and another, with full use of server-side components which limits the live editing. We also saw how to manage and consume content using the Storyblok API.

ResourceLink
Next.js 13 Storyblok BoilerplateNext.js 13 Storyblok Boilerplate
Next.js Technology HubStoryblok Next.js Technology Hub
React Technology HubStoryblok React Technology Hub
Storyblok React SDKstoryblok/storyblok-react
Storyblok Next.js Ultimate Tutorial with Pages Routerhttps://www.storyblok.com/tp/nextjs-headless-cms-ultimate-tutorial
Next.js DocumentationNext.js Docs

Author

Chakit Arora

Chakit Arora

Chakit is a Full Stack Developer based in India, he is passionate about the web and likes to be involved in the community. He is a Twitter space host, who likes to talk and write about technology. He is always excited to try out new technologies and frameworks. He works as a Developer Relations Engineer at Storyblok.