⚠️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

How to generate pages by createPages API with Gatsby.js

Try Storyblok

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

This tutorial will explore integrating Storyblok into a Gatsby.js site with createPages API and enabling 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.

WARN::

Officially, Gatsby.js recommends generating pages dynamically with their File System Route API. Please check our Add a headless CMS to Gatsby.js in 5 minutes tutorial .

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.6.1
  • Nodejs v16.13.2
  • npm v8.1.2

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

Adding Dynamic Components

Now that we kickstarted our project and have a simple connection to Storyblok, we want to load components dynamically. We will create two files in the component folder: dynamicComponent.js and teaser.js

components/dynamicComponent.js
        
      import { sbEditable } from "@storyblok/storyblok-editable"
import Teaser from "./teaser"
import React from "react"

const Components = {
  'teaser': Teaser,
}

const DynamicComponent = ({ blok }) => {
  if (typeof Components[blok.component] !== 'undefined') {
    const Component = Components[blok.component]
    return (<div {...sbEditable(blok)}><Component blok={blok} /></div>)
  }
  return (<p>The component <strong>{blok.component}</strong> has not been created yet.</p>)
}

export default DynamicComponent
    

DynamicComponent is a wrapper around our components to load the correct components and enable live editing, when we click a component. You need to add all components you want to dynamically load to this loader.

components/teaser.js
        
      import * as React from "react"

const Teaser = ({ blok }) => (
    <div>
      <h2>
          { blok.headline }
      </h2>
      <p>
          { blok.intro }
      </p>
    </div>
)

export default Teaser
    

Teaser is a component that is dynamically loaded through the dynamic component loader. The teaser component is a component, that already exists in your Storyblok space, whenever you create a new space.


Loading Dynamic Components

Finally, we need to add the last part to our pages/index.js file to display our components.

pages/index.js
        
      import * as React from "react"
import { Link } from "gatsby"
import { StaticImage } from "gatsby-plugin-image"
import { graphql } from "gatsby"
import { sbEditable } from "@storyblok/storyblok-editable"

import Layout from "../components/layout"
import Seo from "../components/seo"
import DynamicComponent from "../components/dynamicComponent"

const IndexPage = ({ data }) => { 
  let story = data.storyblokEntry
  story.content = JSON.parse(story.content)

  const components = story.content.body.map(blok => {
    return (<DynamicComponent blok={blok} key={blok._uid} />)
  })

  return (
  <Layout>
    <Seo title="Home" />
    <h1>{ story.content.title }</h1>
    { components }
    <StaticImage
      src="../images/gatsby-astronaut.png"
      width={300}
      quality={95}
      formats={["AUTO", "WEBP", "AVIF"]}
      alt="A Gatsby astronaut"
      style={{ marginBottom: `1.45rem` }}
    />
    <p>
      <Link to="/page-2/">Go to page 2</Link> <br />
      <Link to="/using-typescript/">Go to "Using TypeScript"</Link>
    </p>
  </Layout>
)}

export default IndexPage

export const query = graphql`
  query HomeQuery {
    storyblokEntry(full_slug: {eq: "home"}) {
      content
      name
    }
  }
`
    

On Line 15 we map over all the components in our page body to display them dynamically.

If you added a Title field to the page component {1} by defining the schema (see video around minute 19:00), you should be able to see the title. Then the components should be loaded {2} automatically. If the component is not defined in your component/dynamicComponent.js file, you will see the fallback text {3}.

Content loaded
Hint:

You can check the commit feat: add storyblok source plugin to index and feat: add dynamic component for the changes.

Enabling the Visual Editor & Live Preview

So far we loaded our content from Storyblok, but we aren't able to directly select the different components. To enable Storyblok's Visual Editor, we need to connect the Storyblok Bridge. For this tutorial, we will already 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.

Adding the Storyblok Bridge

To do that we have to add a specific <script> tag to the end of our document, whenever we want to enable it. This is mostly the case when you're inside the Storyblok editor. By wrapping the page in a component with sbEditablefunction, we also make the page fields like the title clickable.

        
      <script src="//app.storyblok.com/f/storyblok-v2-latest.js" type="text/javascript" id="storyblokBridge">
</script>
    

Inside our lib/storyblok.js file, add the following code after the client. In Line 12, we're creating a custom React hook called useStoryblok.

lib/storyblok.js
        
      import { useEffect, useState } from "react"

export default function useStoryblok(originalStory, location) {
    let [story, setStory] = useState(originalStory)

    if(story && typeof story.content === "string"){
      story.content = JSON.parse(story.content)
    }
    
    // see https://www.storyblok.com/docs/Guides/storyblok-latest-js
    function initEventListeners() {
      const { StoryblokBridge } = window

      if (typeof StoryblokBridge !== 'undefined') {
        const storyblokInstance = new StoryblokBridge()

        storyblokInstance.on(['published', 'change'], (event) => {
          // reloade project on save an publish
          window.location.reload(true)
        })  
    
        storyblokInstance.on(['input'], (event) => {
          // live updates when editing
          if (event.story._uid === story._uid) {
            setStory(event.story)
          }
        }) 

        storyblokInstance.on(['enterEditmode'], (event) => {
          // loading the story with the client
        }) 
      }
    }

    function addBridge(callback) {
        // check if the script is already present
        const existingScript = document.getElementById("storyblokBridge");
        if (!existingScript) {
          const script = document.createElement("script");
          script.src = `//app.storyblok.com/f/storyblok-v2-latest.js`;
          script.id = "storyblokBridge";
          document.body.appendChild(script);
          script.onload = () => {
            // call a function once the bridge is loaded
            callback()
          };
        } else {
            callback();
        }
    }

    useEffect(() => {
      // load bridge only inside the storyblok editor
      if(location.search.includes("_storyblok")) {
        // first load the bridge and then attach the events
        addBridge(initEventListeners)
      }
    }, []) // it's important to run the effect only once to avoid multiple event attachment

    return story;
}
    

Inside this hook, we have a function addBridge (Line 39), which basically just adds the script tag, if it's not already present. Once the loading of the bridge is completed (Line 58), it will call the initEventListeners function (Line 17) to enable input (Line 25) and published and change events (Line 22) inside Storyblok. We could also make use of the enterEditmode event to load the draft story when the editor is open.

Finally, we need to load this hook in our pages/index.js file on Line 13. We will also move the parsing of the story content to the lib/storyblok.js file.

pages/index.js
        
      import * as React from "react"
import { Link } from "gatsby"
import { StaticImage } from "gatsby-plugin-image"
import { graphql } from "gatsby"
import { sbEditable } from "@storyblok/storyblok-editable"
 
import Layout from "../components/layout"
import Seo from "../components/seo"
import DynamicComponent from "../components/dynamicComponent"
import useStoryblok from "../lib/storyblok"
 
const IndexPage = ({ data, location }) => { 
  let story = data.storyblokEntry
  story = useStoryblok(story, location)
 
  const components = story.content.body.map(blok => {
    return (<DynamicComponent blok={blok} key={blok._uid} />)
  })
 
  return (
  <Layout>
    <div {...sbEditable(story.content)}>
      <Seo title="Home" />
      <h1>{ story.content.title }</h1>
      { components }
        <StaticImage
          src="../images/gatsby-astronaut.png"
          width={300}
          quality={95}
          formats={["AUTO", "WEBP", "AVIF"]}
          alt="A Gatsby astronaut"
          style={{ marginBottom: `1.45rem` }}
        />
        <p>
          <Link to="/page-2/">Go to page 2</Link> <br />
          <Link to="/using-typescript/">Go to "Using TypeScript"</Link>
        </p>
    </div>
  </Layout>
)}
 
export default IndexPage
 
export const query = graphql`
  query HomeQuery {
    storyblokEntry(full_slug: {eq: "home"}) {
      content
      name
    }
  }
`
    

If the connection with the Storyblok hook is working, you should be able to select the component directly.

Visual Editor active
Hint:

You can check the commit feat: add storyblok bridge and live updates for the changes.

Using Storyblok JS client

Finally, we can extend our hook to also make use of the JS client to load the draft version of our content when the editor is open. Add the following to the lib/storyblok.js file on Line 3 and Line 42:

lib/storyblok.js
        
      import { useEffect, useState } from "react"
import StoryblokClient from "storyblok-js-client";
import config from "../../gatsby-config"
const sbConfig = config.plugins.find((item) => item.resolve === 'gatsby-source-storyblok')

const Storyblok = new StoryblokClient({
  accessToken: sbConfig.options.accessToken,
  cache: {
    clear: "auto",
    type: "memory",
  },
});

export default function useStoryblok(originalStory, location) {
    let [story, setStory] = useState(originalStory)

    if(story && typeof story.content === "string"){
      story.content = JSON.parse(story.content)
    }
    
    // see https://www.storyblok.com/docs/Guides/storyblok-latest-js
    function initEventListeners() {
      const { StoryblokBridge } = window

      if (typeof StoryblokBridge !== 'undefined') {
        const storyblokInstance = new StoryblokBridge()

        storyblokInstance.on(['published', 'change'], (event) => {
          // reloade project on save an publish
          window.location.reload(true)
        })  
    
        storyblokInstance.on(['input'], (event) => {
          // live updates when editing
          if (event.story._uid === story._uid) {
            setStory(event.story)
          }
        }) 

        storyblokInstance.on(['enterEditmode'], (event) => {
          // loading the draft version on initial view of the page
          Storyblok
            .get(`cdn/stories/${event.storyId}`, {
              version: 'draft',
            })
            .then(({ data }) => {
              if(data.story) {
                setStory(data.story)
              }
            })
            .catch((error) => {
              console.log(error);
            }) 
        }) 
      }
    }

    function addBridge(callback) {
        // check if the script is already present
        const existingScript = document.getElementById("storyblokBridge");
        if (!existingScript) {
          const script = document.createElement("script");
          script.src = `//app.storyblok.com/f/storyblok-v2-latest.js`;
          script.id = "storyblokBridge";
          document.body.appendChild(script);
          script.onload = () => {
            // call a function once the bridge is loaded
            callback()
          };
        } else {
            callback();
        }
    }

    useEffect(() => {
      // load bridge only inside the storyblok editor
      if(location.search.includes("_storyblok")) {
        // first load the bridge and then attach the events
        addBridge(initEventListeners)
      }
    }, []) // it's important to run the effect only once to avoid multiple event attachment

    return story;
}
    

Using the enterEditmode event to request the draft content from Storyblok with the open story entry.

Automatic Page Generation

In most cases, you would want to automatically generate the pages from the content you have set up in Storyblok. To do that with Gatsby, we can follow this tutorial. Basically, what we need to do is to add a template file: templates/page.js as well as change our gatsby-node.js.

Let's start by creating the template, similar to our pages/index.js. Create a new folder templates with a file page.js:

templates/page.js
        
      import * as React from "react"
import { Link } from "gatsby"
import { StaticImage } from "gatsby-plugin-image"
import { graphql } from "gatsby"

import Layout from "../components/layout"
import Seo from "../components/seo"
import DynamicComponent from "../components/dynamicComponent"
import useStoryblok from "../lib/storyblok"

const Page = ({ pageContext, location }) => { 
  let story = pageContext.story
  story = useStoryblok(story, location)

  const components = story.content.body.map(blok => {
    return (<DynamicComponent blok={blok} key={blok._uid} />)
  })

  return (
  <Layout>
    <Seo title="Home" />
    <h1>{ story.content.title }</h1>
    { components }
    <StaticImage
      src="../images/gatsby-astronaut.png"
      width={300}
      quality={95}
      formats={["AUTO", "WEBP", "AVIF"]}
      alt="A Gatsby astronaut"
      style={{ marginBottom: `1.45rem` }}
    />
    <p>
      <Link to="/page-2/">Go to page 2</Link> <br />
      <Link to="/using-typescript/">Go to "Using TypeScript"</Link>
    </p>
  </Layout>
)}

export default Page

    

The biggest difference to the pages/index.js is that it's not using GraphQl directly on the page, but exposing the story via the pageContext object.

To get that working we also need to add some logic to our gatsby-node.js file.

gatsby-node.js
        
      const path = require('path')

 exports.createPages = ({ graphql, actions }) => {
    const { createPage } = actions

    return new Promise((resolve, reject) => {
        const storyblokEntry = path.resolve('src/templates/page.js')
    
        resolve(
          graphql(
            `{
              stories: allStoryblokEntry(filter: {field_component: {eq: "page"}}) {
                edges {
                  node {
                    id
                    name
                    slug
                    field_component
                    full_slug
                    content
                  }
                }
              }
            }`
          ).then(result => {
            if (result.errors) {
              console.log(result.errors)
              reject(result.errors)
            }
    
            const entries = result.data.stories.edges
    
            entries.forEach((entry) => {
                if(entry.slug !== "home") {
                    const page = {
                        path: `/${entry.node.full_slug}`,
                        component: storyblokEntry,
                        context: {
                            story: entry.node
                        }
                    }
                    createPage(page)
                }
            })
          })
        )
      })
 }

    

On Line 7, we're loading our template we just created for all pages. On Line 10 we're requesting all stories from Storyblok with the content type Page. On Line 35, we're creating the actual pages, but are skipping the Home story, because we're already loading that one in the index.js file.


When we create a new entry in Storyblok, saving it, and restarting the development server (or using the refresh button in the GraphiQL explorer), the new page should now be visible on your localhost.

Hint:

You can check the commit feat: automatic page generation for the changes.

Author

Lisi Linhart

Lisi Linhart

Lisi is a front-end engineer with a passion for web animation and UX. She is working for Storyblok as a Developer Experience Engineer from Salzburg, Austria.