Add a headless CMS to Gatsby.js in 5 minutes
Storyblok is the first headless CMS that works for developers & marketers alike.
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.
This short tutorial will explore how to integrate Storyblok into a Gatsby.js site and enable the live preview in the Visual Editor. We will use the gatsby-source-storyblok plugin to load our data from Storyblok and enable the Storyblok Bridge to preview our changes.
You can find all the code for this tutorial and commits in our gatsby-storyblok-boilerplate repo. You can also follow the video below, which guides you through all the steps.
Requirements
To follow this tutorial, there are the following requirements:
Basic understanding of Gatsby.js and React
Node, yarn (or npm) and Gatsby installed
An account on Storyblok to manage content
A new Storyblok space
The project in this article was developed using the following versions of these technologies:
- Gatsby ^4.17.3
- Nodejs v16.13.2
- npm v8.1.2
Keep in mind that these versions may be slightly behind the latest ones.
Setup the project
Let's start by creating a new Gatsby.js application. This time, we use Gatsby's default starter.
npx gatsby new storyblok-gatsby-boilerplate https://github.com/gatsbyjs/gatsby-starter-default
Next, we need to install a Gatsby SDK, gatsby-source-storyblok.
cd storyblok-gatsby-boilerplate
npm install --save gatsby-source-storyblok
Then let's start the development server.
npm run develop
Open your browser in http://localhost:8000. You should see the following screen.

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
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
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
Now you’ll see the default screen and the Visual Editor:

Visual Editor representing your Home story
Adding the development server to Storyblok
Create a new space and click on Settings {1} in the sidebar. At Visual Editor {2}, add your http://localhost:8000/ server as the default environment {3}. Or, with our Storyblok V2, add SSL certified localhost https://localhost:3010/ and run commands below.
// Install mkcert for creating a valid certificate (Mac OS):
$ brew install mkcert
$ mkcert -install
$ mkcert localhost
// Then install and run the proxy
$ npm install -g local-ssl-proxy
$ local-ssl-proxy --source 3010 --target 8000 --cert localhost.pem --key localhost-key.pem

Setting the real path
Next, click on Content and open the Home story. Since Storyblok will automatically attach the slug for any story entry, we need to set the real path on our home entry since the slug is home
. Click on Entry Configuration icon {1} and enter /
{2} as the Real Path. Now you should see your development server inside of Storyblok.

Using gatsby-source-storyblok
To connect to the Storyblok API, we have already installed the gatsby-source-storyblok plugin. Now it's time to add it to our project. Add the following to your gatsby-config.js
file (Line 32).
require("dotenv").config({
path: `.env.${process.env.NODE_ENV || "production"}`,
})
module.exports = {
siteMetadata: {
title: `Gatsby Default Starter`,
description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
author: `@gatsbyjs`,
},
plugins: [
`gatsby-plugin-react-helmet`,
`gatsby-plugin-image`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images`,
},
},
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `gatsby-starter-default`,
short_name: `starter`,
start_url: `/`,
background_color: `#663399`,
theme_color: `#663399`,
display: `minimal-ui`,
icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site.
},
},
`gatsby-plugin-gatsby-cloud`,
{
resolve: 'gatsby-source-storyblok',
options: {
accessToken: 'YOUR-PREVIEW-TOKEN',
version: process.env.NODE_ENV === 'production' ? 'published' : 'draft',
localAssets: true, // Optional parameter to download the images to use with Gatsby Image Plugin
// languages: ['de', 'at'] // Optional parameter. Omission will retrieve all languages by default.
}
}
// this (optional) plugin enables Progressive Web App + Offline functionality
// To learn more, visit: https://gatsby.dev/offline
// `gatsby-plugin-offline`,
],
}
Retrieve your preview token {3} from your space Settings {1} under API-Keys {2}. Add the token to your source plugin in gatsby-config.js
as the accessToken
on Line 35. Or, store the preview token in .env.development
and .env.production
files to use environment variables.
You’ll need to prefix the Access token with GATSBY_
for accessing it on the browser. Also, you need to add dotenv
or require
gatsby-config.js
to read environment variables. Find more details on Gatsby’s documentation.

Using the GraphiQL Explorer
If you're working with Gatsby.js, whenever you start the development, it will also begin the GraphiQL Explorer on the following URL: http://localhost:8000/___graphql. As soon as you add the source plugin, you will explore the Storyblok API endpoints there. Read the following tutorial to learn more about the explorer.
It's helpful to enable the refresh feature described in the how-to refresh content tutorial. You can do that by pretending the develop command in your package.json
file:
"scripts": {
...
"develop": "ENABLE_GATSBY_REFRESH_ENDPOINT=true gatsby develop",
...
}
If you're connected to the Storyblok API through the source plugin, you should be able to see multiple query options in the Explorer. You can select, for example, allStoryblokEntry
{1} and get specific information from Storyblok like the full_slug
{2} of a story. By clicking around the explorer, your query will automatically be built on the right {3}. If you enable the refresh endpoint, you will also see a Refresh Data {4} button to reload the data. Finally, if you got the query you want, you can click the Code Exporter button {5} to get the Javascript code you need inside of Gatsby.

Using gatsby-source-storyblok on a page
By default, Storyblok provides three components (Teaser, Feature, and Grid) and one content-type (Page). Let's create a collection route inside src/pages.
|-- src
|-- pages
|-- index.js
You can create components instead of using default components. These components are defined to show you examples of components.
Now that we’ve made sure that we have default components provided from Storyblok. Let's update components/layout.js
, a global layout component to help initialize Storyblok API only once instead of several times.
import * as React from "react"
import PropTypes from "prop-types"
import { storyblokInit, apiPlugin } from "gatsby-source-storyblok"
storyblokInit({
accessToken: process.env.GATSBY_PREVIEW_STORYBLOK,
use: [apiPlugin],
components: {
// components
}
});
const Layout = ({ children }) => {
return (
<div>
<main>{children}</main>
</div>
)
}
Layout.propTypes = {
children: PropTypes.node.isRequired,
}
export default Layout
Let's update pages/index.js, our homepage, with the code below. We're loading our home content through the gatsby-source-storyblok plugin. Keep in mind that by default, the Gatsby.js source plugin is loading content at build time because Gatsby serves pages with SSG by default. So, whenever you change the content inside Storyblok with Gatsby’s SSG, you will need to restart your server or use the refresh data feature described above. Read this article to understand the difference between build and client time in Gatsby.
import * as React from "react"
import { graphql } from "gatsby"
import { useStoryblokState } from "gatsby-source-storyblok"
import Layout from "../components/layout"
const IndexPage = ({ data }) => {
let story = data.storyblokEntry
story = useStoryblokState(story)
return (
<Layout>
<div>
<h1>{story.name}</h1>
</div>
</Layout>
)
}
export default IndexPage
export const query = graphql`
query HomeQuery {
storyblokEntry(full_slug: { eq: "home" }) {
content
name
full_slug
uuid
id
internalId
}
}
`
Since the Storyblok GraphQL API returns the content as a string, we will need to parse the story content with JSON.parse()
however, gatsby-source-storyblok provides useStoryblokState
to cover all to handle that.
useStoryblokState handles to listen to events that happen on the visual editor and load the Storyblok JS Bridge as well. We’ll have a closer look at more details on the further steps.
Adding & Loading Dynamic Components
Now that we kickstarted our project and have a simple connection to Storyblok, we want to load components dynamically. We will create one component file in the component folder: teaser.js
import * as React from "react"
const Teaser = ({ blok }) => {
return (
<div>
<h1>{blok.headline}</h1>
</div>
)
}
export default Teaser
components/teaser.js
is a component file that is dynamically loaded through the dynamic component loader. Let's import and add dynamic components to components/layout.js
.
import * as React from "react"
import PropTypes from "prop-types"
import { storyblokInit, apiPlugin } from "gatsby-source-storyblok"
import Teaser from './Teaser'
import configuration from '../../gatsby-config'
const sbConfig = configuration.plugins.find((item) => item.resolve === 'gatsby-source-storyblok')
storyblokInit({
accessToken: sbConfig.options.accessToken,
apiOptions: {
region: "us", // Pass this key/value if your space was created under US region
},
use: [apiPlugin],
components: {
teaser: Teaser
}
});
const Layout = ({ children }) => {
return (
<div>
<main>{children}</main>
</div>
)
}
Layout.propTypes = {
children: PropTypes.node.isRequired,
}
export default Layout
For spaces created in the United States, you have to set the region parameter accordingly: { apiOptions: { region: ‘us’ } }. You can see that in line {12} of components/layout.js
Next up, let's call an API, StoryblokComponent
to handle loading dynamic components.
import * as React from "react"
import { graphql } from "gatsby"
import { StoryblokComponent, useStoryblokState } from "gatsby-source-storyblok"
import Layout from "../components/layout"
const IndexPage = ({ data }) => {
let story = data.storyblokEntry
story = useStoryblokState(story)
const components = story.content.body.map(blok => (<StoryblokComponent blok={blok} key={blok._uid} />))
return (
<Layout>
<div>
<h1>{story.name}</h1>
{components}
</div>
</Layout>
)
}
export default IndexPage
export const query = graphql`
query HomeQuery {
storyblokEntry(full_slug: { eq: "home" }) {
content
name
full_slug
uuid
id
internalId
}
}
`
In Line 12, we map over all the components in our page body to display them dynamically.
StoryblokComponent
API from gatsby-source-storyblok will handle conditionally load dynamic components if they exist. The teaser
component is a component that already exists in your Storyblok space whenever you create a new space.
You can load grid
and feature
components like teaser
component. However, keep in mind that the grid
component is mapping the other components that will be nested inside. In short, the grid
component is a parent component that contains other components inside.
import React from "react";
import { StoryblokComponent } from "gatsby-source-storyblok";
const Grid = ({ blok }) => (
<ul key={blok._uid}>
{blok.columns.map((blok) => (
<li key={blok._uid}>
<StoryblokComponent blok={blok} />
</li>
))}
</ul>
);
export default Grid;
You can visually check the nested component layout from the Visual Editor.
You can click the Show form view icon in the top right corner in the Visual Editor area to minimize the Visual Editor and select the Search blocks magnifying glass icon.
|-- teaser
|-- grid
|-- feature
|-- feature
|-- feature

Components should be loaded automatically and you can see rendered components on the visual editor. If the component is not defined in your component/layout.js
file, you will see the components won't be loaded.

Enabling the Visual Editor & Live Preview
So far, we have loaded our content from Storyblok, but we cannot directly select the different components. To enable Storyblok's Visual Editor, we need to connect the Storyblok Bridge. For this tutorial, we will 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. But not to worry. You have already handled the logic to listen to events on the visual editor and load Storyblok JS Bridge by calling an API called useStoryblokState
in the previous steps.
The Logic to adding the Storyblok Bridge
Technically, we have completed loading the Storyblok JS Bridge but let's understand the fundamentals from behind the scene. We need to add a specific <script>
tag to the end of our document whenever we want to enable it. It's mostly the case when you're inside the Storyblok editor. By wrapping the page in a storyblokEditable
component, we also make the page fields like the teaser
clickable.
<script src="//app.storyblok.com/f/storyblok-v2-latest.js" type="text/javascript" id="storyblokBridge">
</script>
gatsby-source-storyblok adds the React custom hooks code after the client. Inside this hook, we have a function that adds the script tag if it's not already present. Once the loading of the bridge is completed, it will call another function to enable input
, published
, and change
events inside Storyblok.
Call storyblokEditable
Based on this logic, we need to load this hook in our pages/index.js
file on Line 10. Also, to make dynamic components editable, we call storyblokEidtable
from gatsby-source-storyblok and wrap the components with storyblokEditable
in the scope of JSX (Line 16).
import * as React from "react"
import { graphql } from "gatsby"
import { StoryblokComponent, storyblokEditable, useStoryblokState } from "gatsby-source-storyblok"
import Layout from "../components/layout"
const IndexPage = ({ data }) => {
let story = data.storyblokEntry
story = useStoryblokState(story)
const components = story.content.body.map(blok => (<StoryblokComponent blok={blok} key={blok._uid} />))
return (
<Layout>
<div {...storyblokEditable(story.content)}>
<h1>{story.name}</h1>
{components}
</div>
</Layout>
)
}
export default IndexPage
export const query = graphql`
query HomeQuery {
storyblokEntry(full_slug: { eq: "home" }) {
content
name
full_slug
uuid
id
internalId
}
}
`
If the connection with the Storyblok hook is working from useStoryblokState
, you should be able to select the component directly. Let's apply storyblokEditable
to the rest of the dynamic components.
import * as React from "react"
import { storyblokEditable } from "gatsby-source-storyblok";
const Teaser = ({ blok }) => {
return (
<div {...storyblokEditable(blok)}>
<h1>{blok.headline}</h1>
</div>
)
}
export default Teaser
import React from "react";
import { StoryblokComponent, storyblokEditable } from "gatsby-source-storyblok";
const Grid = ({ blok }) => (
<ul {...storyblokEditable(blok)} key={blok._uid}>
{blok.columns.map((blok) => (
<li key={blok._uid}>
<StoryblokComponent blok={blok} />
</li>
))}
</ul>
);
export default Grid;
import React from "react";
import { storyblokEditable } from "gatsby-source-storyblok";
const Feature = ({ blok }) => {
return (
<div {...storyblokEditable(blok)} key={blok._uid}>
<h2>{blok.name}</h2>
<p>{blok.description}</p>
</div>
);
};
export default Feature;

Automatic Page Generation with File System Route API
In most cases, you would want to automatically generate the pages from the content set up in Storyblok. To do that with Gatsby, we can follow this tutorial. What we need to do is to create a page file: pages/{storyblokEntry.full_slug}.js
, and that's it! By creating {storyblokEntry.full_slug}.js
file, Gatsby will use this page template for each storyblokEntry
. The full_slug
query will also recognize the nested entries inside the folders.
|-- src
|-- pages
|-- index.js
|-- {storyblokEntry.full_slug}.js
With Gatsby's File System Route API, we don't have to configure gatsby-node.js
file anymore, and no need to create template files. Easy and more performant. Let’s import dynamic components and load JS Bridge as well as JS Client in pages/{storyblokEntry.full_slug}.js
file.
import * as React from "react"
import { graphql } from "gatsby"
import { StoryblokComponent, storyblokEditable, useStoryblokState } from "gatsby-source-storyblok"
import Layout from "../components/layout"
const StoryblokEntry = ({ data }) => {
let story = data.storyblokEntry
story = useStoryblokState(story)
const components = story.content.body.map(blok => (<StoryblokComponent blok={blok} key={blok._uid} />))
return (
<Layout>
<div {...storyblokEditable(story.content)}>
<h1>{story.name}</h1>
{components}
</div>
</Layout>
)
}
export default StoryblokEntry
export const query = graphql`
query ($full_slug: String!) {
storyblokEntry(full_slug: { eq: $full_slug }) {
content
name
full_slug
uuid
id
internalId
}
}
`
Gatsby officially recommends File System Route API for better performance and minor complications to set up page generations. If you still prefer Gatsby’s createPages API, check our How to generate pages by createPages API with Gatsby.js tutorial with createPages API.
Adding a fallback page
Since the production build will only have the content and data available during build time, we need to add a fallback to our 404 page to display Storyblok content via a client-side request. Add the following to the pages/404.js
:
import * as React from "react"
import Layout from "../components/layout"
const NotFoundPage = () => (
<Layout>
<h1>404: Not Found</h1>
<p>You just hit a route that doesn't exist... the sadness.</p>
</Layout>
)
export default NotFoundPage
We're calling our Storyblok hook with no given story. However, suppose we're inside the Storyblok editor. In that case, we can access the editor story and update the page dynamically on the input event, which will give us a preview of the page. If you're on the development server, you have to click the Preview custom 404-page button {1} to see this fallback page.


Using Storyblok's GraphQL API
If you want to use our GraphQL API directly instead of the gatsby-source-storyblok plugin, we recommend using the gatsby-source-graphql plugin. It can be helpful to query the content object instead of a stringified version. Add the following to your gatsby-config.js
file:
module.exports = {
/* Your site config here */
plugins: [
...
{
resolve: `gatsby-source-graphql`,
options: {
fieldName: `Storyblok`,
typeName: `storyblok`,
url: `https://gapi.storyblok.com/v1/api`,
headers: {
Token: `YOUR_PREVIEW_TOKEN`,
Version: `draft`,
},
},
},
],
}
gatsby-source-graphql is a Gatsby plugin. gatsby-source-graphql doesn’t have an API to enable the real-time visual editor feature.
If you’d like to speed up GraphQL API performance, you can read the “GraphQL speed improvements” changelog article.
Conclusion
Congrats! You just completed the first step of our Gatsby ultimate tutorial! We learned how to integrate Storyblok into a Gatsby.js project. We saw how to manage and consume content using the Storyblok API, GraphQL from Gatsby, and GraphQL API from Storyblok. Also, we learned how to enable a real-time visual experience using the Visual Editor. We went through different features that Gatsby.js offers to create great user experiences: GraphQL, File System Route API to generate performant pages, etc.
In the next part of this series, we will see how to start making a real website with Gatsby.js and Storyblok.
Resource | Link |
---|---|
Boilerplate Repository | https://github.com/storyblok/gatsby-storyblok-boilerplate |
Storyblok Gatsby.js Ultimate Tutorial | https://www.storyblok.com/tp/storyblok-gatsby-ultimate-tutorial |
Gatsby.js Technology Hub | https://www.storyblok.com/tc/gatsbyjs |
Storyblok Gatsby SDK | https://github.com/storyblok/gatsby-source-storyblok |
Gatsby Documentation | https://www.gatsbyjs.com/docs/ |