Add a headless CMS to Gatsby.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.

    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

    WARN:

    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.

    localhost:8000
    Storyblok editing capabilities

    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
    app.storyblok.com
    Storyblok editing capabilities
    1
    2
    3

    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.

    Storyblok editing capabilities
    1
    2

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

    gatsby-config.js
    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.

    HINT:

    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.

    https://app.storyblok.com/
    Storyblok editing capabilities
    1
    2
    3

    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:

    package.json
      "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.

    GrapiQL Explorer

    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
    HINT:

    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.

    components/layout.js
    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.

    HINT:

    Gatsby supports SSR and DSG (Differed Static Generation) from V4. You can switch between SSG, SSR, and DSG by following their documentation.

    pages/index.js
    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.

    HINT:

    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

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

    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

    hint:

    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.

    page/index.js
    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.

    components/grid.js
    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.

    HINT:

    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

    storyblok.com
    Storyblok editing capabilities
    1
    2

    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.

    storyblok.com
    Storyblok editing capabilities

    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 storyblokEditablecomponent, 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).

    pages/index.js
    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.

    components/teaser.js
    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
    components/grid.js
    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;
    
    components/feature.js
    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;
    

    storyblok.com
    Storyblok editing capabilities
    1
    2

    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.

    pages/{storyblokEntry.full_slug}.js
    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
        }
      }
    `
    HINT:

    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:

    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&#39;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.

    storyblok.com
    Storyblok editing capabilities
    1
    localhost:8000
    Storyblok editing capabilities

    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`,
            },
          },
        },
      ],
    }
    
    WARN:

    gatsby-source-graphql is a Gatsby plugin. gatsby-source-graphql doesn’t have an API to enable the real-time visual editor feature.

    HINT:

    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.

    NEXT PART:

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