Add a headless CMS to Next.js in 5 minutes
Storyblok is the first headless CMS that works for developers & marketers alike.
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 (opens in a new window) to load our data using the Storyblok API and enable the live editing experience.
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
The project in this tutorial and its subsequent parts were developed using the following versions:
@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 (opens in a new window) . 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 (opens in a new window) . You should see the following screen.
)
Connecting to Storyblok
Now that we kickstarted our project, we need to create a connection to Storyblok and enable the Visual Editor (opens in a new window) .
To initialize the connection, go to pages/_app.js
and add the following code.
...
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 EUus
: For spaces created in the USap
: For spaces created in Australiaca
: For spaces created in Canadacn
: For spaces created in China
Here's an example for a space created in the US:
apiOptions: {
region: "us",
},
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 (opens in a new window) 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.
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.
)
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.
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
};
}
)
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/
.
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
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
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:
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;
import { storyblokEditable } from "@storyblok/react";
const Teaser = ({ blok }) => {
return <h2 {...storyblokEditable(blok)}>{blok.headline}</h2>;
};
export default Teaser;
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;
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.
...
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:
...
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
Optional: Changing Styles
Let’s add TailwindCSS (opens in a new window) to our project for the ease of adding styles. You can refer to this guide (opens in a new window) 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 -
...
return
<h2 className="text-2xl mb-10" {...storyblokEditable(blok)}>
{blok.headline}
</h2>;
...
Change the Grid.js
code to the following -
...
<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 -
...
<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
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.
...
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
Using Server Side Rendering
If you don't want to use static rendering, you can also use Next.js server-side rendering (opens in a new window) . 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.
...
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.
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.
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 |