The Complete Guide to Build a Full-Blown Multilanguage Website with Gatsby.js

Contents

This guide is for beginners and professionals who want to build a full-blown multilanguage website using Gatsby.js. You can take a look at the deployed Demo project here.

With this step by step guide, you will get a Gatsby website using Storyblok's API for the multilanguage content and a live preview.

If you are in a hurry you can download the whole source code of the project at Github github.com/storyblok/gatsbyjs-multilanguage-website or jump into one of the following chapters.

  1. Environment setup

  2. Setting up Storyblok

  3. Build a multi-language navigation menu

  4. Build a blog section

  5. Build a blog overview

  6. Deployment

Storyblok editing capabilities

Requirements

To continue with this tutorial, we don't expect you are an expert web developer, but you should understand a few basic concepts listed under this paragraph. We will guide you through most topics of this tutorial, but if you are beginning with Gatsby.js, you should consider checking out their quick start tutorial.

Environment setup

If you haven't done so already, install Node.js, NPM and the Gatsby.js CLI on your machine. Start with running the following shell commands. If you don't specify differently to the Gatsby CLI it will start with gatsby-starter-default as the starter template.

$ npm install -g gatsby-cli 
$ gatsby new gatsbyjs-multilanguage-website
$ cd gatsbyjs-multilanguage-website
$ gatsby develop

Gatsby.js will start your local development environment at http://localhost:8000 and you should see the introduction screen of the Gatsby.js starter.

Gatsby.js default starter template website

Connect Storyblok

At first, sign up or log in at app.storyblok.com and click on Create new space. Select the optionCreate a new space and create your first Storyblok space.

Before we run Gatsby we need to install the source plugin of Storyblok. The Gatsby source plugin makes Storyblok’s content available in GraphQL. For the editing interface, we also need to install the Storyblok's React module storyblok-react
.

$ npm install --save gatsby-source-storyblok storyblok-react

In the next step, we need to go to the Settings of our Storyblok space and retrieve our preview_token from the API Keys section (1).

Fill the Location (default environment) (2) as well as the Preview URLs (3) field with your localhost URL http://localhost:8000/.

Preview Token and Live Preivew

Copy the preview token (1) into the gatsby.config.js file. Exchange YOUR_PREVIEW_TOKEN with the preview token of your space.

gatsby-config.js

module.exports = {
  siteMetadata: {
    title: 'Gatsby Default Starter',
  },
  plugins: [
    {
      resolve: 'gatsby-source-storyblok',
      options: {
        accessToken: 'YOUR_PREVIEW_TOKEN',
        homeSlug: 'home',
        version: process.env.NODE_ENV === 'production' ? 'published' : 'draft'
      }
    },
    ...
  ]
}

Now let's start the Gatsby.js server on port 8000 to see if everything is working.

$ gatsby develop

Creating components

To render the full homepage, we will need to create Gatsby.js components for our demo space in Storyblok. Add the file components/DynamicComponent.js containing the following code. As you can see we will create a teaser, feature, page and grid component for our demo project.

import React from 'react'
import Teaser from './Teaser'
import Feature from './Feature'
import Grid from './Grid'
import Placeholder from './Placeholder'

const Components = {
  'teaser': Teaser,
  'feature': Feature,
  'grid': Grid
}

const Component = ({blok}) => {
  if (typeof Components[blok.component] !== 'undefined') {
    const Component = Components[blok.component]
    return <Component blok={blok} />
  }
  return blok.component ? <Placeholder componentName={blok.component}/> : null
}

export default Component

This dynamic component allows us to automatically inject the right component based on our Storyblok content. You can find the code for the four components on Github: Teaser, Feature, Grid, Placeholder or copy it from below.

Page.js

import React from "react"
import DynamicComponent from "./DynamicComponent"
import SbEditable from 'storyblok-react'

const Page = ({ blok }) => {
  const content =
    blok.body &&
    blok.body.map(childBlok => <DynamicComponent blok={childBlok} key={childBlok._uid}/>)
  const hasTitle = blok.title.length ? (<h1 className="text-5xl font-bold font-serif text-primary tracking-wide text-center py-8">{ blok.title }</h1>) : null
  return (
    <SbEditable content={blok}>
        { hasTitle }
        { content }
    </SbEditable>
  )
}

export default Page

Teaser.js

import React from 'react'
import SbEditable from 'storyblok-react'

const Teaser = ({blok}) => {
  return (
    <SbEditable content={blok}>
      <div className="container mx-auto py-16">
        <h1 className="text-4xl font-serif font-bold">{blok.headline}</h1>
      </div>
    </SbEditable>
  )
}

export default Teaser

Grid.js

import React from 'react'
import DynamicComponent from './DynamicComponent'
import SbEditable from 'storyblok-react'

const Grid = ({blok}) => (
  <SbEditable content={blok} key={blok._uid}>
    <ul className="flex flex-wrap p-8 bg-white container mx-auto">
      {blok.columns.map((blok) => (
          <li key={blok._uid} className="flex-auto px-2">
            <DynamicComponent blok={blok} />
          </li>
        )
      )}
    </ul>
  </SbEditable>
)

export default Grid

Feature.js

import React from 'react'
import SbEditable from 'storyblok-react'

const Feature = ({blok}) => {
  return (
    <SbEditable content={blok} key={blok._uid}>
      <div className="py-16 max-w-sm p-2 sm:p-10 text-center flex flex-col items-center">
            <img src="" />
            <div className="px-6 py-4">
                <div className="font-bold text-xl my-4">{blok.name}</div>
                <p className="text-base text-gray-600">
                  {blok.description}
                </p>
            </div>
        </div>
    </SbEditable>
  )
}

export default Feature

Placeholder.js

import React from "react"

const Placeholder = ({componentName}) => (
  <div className="py-4 border border-red-200 bg-red-100">
    <p className="text-red-700 italic text-center">The component <strong>{componentName}</strong> has not been created yet.</p>
  </div>
);

export default Placeholder;


Loading the components in Gatsby

Now that we've created all the components, 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 the Gatsby.js source plugin is loading content at build time, so whenever you change the graphQL query, you will need to restart your server. Read this article to understand the difference between build and client time in Gatsby.

pages/index.js

import React from "react"
import Page from '../components/Page'
import Layout from "../components/Layout"
import { graphql } from 'gatsby'

export default class extends React.Component {
  render() {
    const contentOfStory = JSON.parse(this.props.data.storyblokEntry.content)

    return (
       <Layout location={this.props.location}>
        <Page blok={contentOfStory} />
      </Layout>
    )
  }
}

export const query = graphql`
  query {
    storyblokEntry(full_slug: { eq: "home" }) {
      id
      name
      content
      lang
      slug
      uuid
    }
  }
`

Setting up the Storyblok Editor

Now, navigate to the content section of the Storyblok and open Home entry in the view. The visual editor will open and you should see an error screen. Gatsby.js throws an error and you should see the demo content in the sidebar created by default in Storyblok space.

Content in Storyblok

Changing the real path field

To fix the error from Gatsby.js, we just need to change the real path of our homepage. By default is the slug of the home entry defined as /home, but we define only the index page of our Gatsby.js application. Open the config tab in the sidebar of the screen and change the real path to the / . You can now reload the page and you should see your Next.js application in preview space. Be sure you hit the Save before reload.

Gatsby 404

Once you changed the real path, you should already see the Gatsby page with our loaded components.

Gatsby in Storyblok

Connecting the Storyblok Bridge

To build a bridge between Storyblok and your website, you need to include a special script on your website. This script will communicate via iframe with Storyblok to tell the editing interface which component needs to be opened when the user clicks on it.

Add the Storyblok Service

In order to be able to recognize that some content was changed within Storyblok, we need to add the Event Listeners to our Gatsby Example. Create a new folder utils with a storyblok-service.js file.

import StoryblokClient from 'storyblok-js-client'
import config from '../../gatsby-config'
const sbConfig = config.plugins.find((item) => item.resolve === 'gatsby-source-storyblok')

class StoryblokService {
  constructor() {
    this.devMode = false
    this.token = sbConfig.options.accessToken
    this.client = new StoryblokClient({
      accessToken: this.token,
      cache: {
        clear: 'auto',
        type: 'memory'
      }
    })
    this.query = ''
  }

  getCacheVersion() {
    return this.client.cacheVersion
  }

  get(slug, params) {
    params = params || {}

    if (this.getQuery('_storyblok') || this.devMode || (typeof window !== 'undefined' && window.storyblok)) {
      params.version = 'draft'
    }

    if (typeof window !== 'undefined' && typeof window.StoryblokCacheVersion !== 'undefined') {
      params.cv = window.StoryblokCacheVersion
    }

    return this.client.get(slug, params)
  }

  initEditor(reactComponent) {
    if (window.storyblok) {
      window.storyblok.init({
        accessToken: sbConfig.options.accessToken
      })
      window.storyblok.on(['change', 'published'], () => window.location.reload(true))

      // this will alter the state and replaces the current story with a current raw story object (no resolved relations or links)
      window.storyblok.on('input', (event) => {
        if (event.story && event.story.content._uid === reactComponent.state.story.content._uid) {
          event.story.content = window.storyblok.addComments(event.story.content, event.story.id)
          window.storyblok.resolveRelations(event.story, ['featured-articles.articles'], () => {
            reactComponent.setState({
              story: event.story
            })
          })
        }
      })
    }
  }

  setQuery(query) {
    this.query = query
  }

  getQuery(param) {
    return this.query.includes(param)
  }
}

const storyblokInstance = new StoryblokService()

export default storyblokInstance

Add the Storyblok Bridge and Cache Version

We can include the Storyblok Bridge script tag by making use of React Helmet. First, install React Helmet with npm i react-helmet --save and then add the following Helmet tag in the components/layout.js file. The script needs our access token, which we can get from our Storyblok Service.

import { Helmet } from 'react-helmet'

// get Storyblok Service
import StoryblokService from '../utils/storyblok-service'

const Layout = ({ children }) => {
  // ... 
  return (
    <>
      {/* insert helmet */}
      <Helmet
          script={[
            {"src": `//app.storyblok.com/f/storyblok-latest.js?t=${StoryblokService.token}`, 
            "type": "text/javascript"}
          ]}
      />
      <Helmet
          script={[
            {
            "innerHTML": `var StoryblokCacheVersion = '${StoryblokService.getCacheVersion()}';`,
            "type": "text/javascript"
            }
          ]}
      />
      <Header siteTitle={data.site.siteMetadata?.title || `Title`} />
      {/* ... */}
    </>
  )
}

export default Layout

Load the Storyblok Service in your React Component

The last step is to load our Storyblok Service at the right place. Open pages/index.js and replace the content with the following code.

import React from "react"
import Page from '../components/Page'
import Layout from "../components/layout"
import { graphql } from 'gatsby'
import StoryblokService from '../utils/storyblok-service'

export default class extends React.Component {
  state = {
    story: {
       content: this.props.data.story ? JSON.parse(this.props.data.story.content) : {}
    }
  }

  async getInitialStory() {
    StoryblokService.setQuery(this.props.location.search)
    let { data: { story } } = await StoryblokService.get(`cdn/stories/${this.props.data.story.full_slug}`)
    return story
  }

  async componentDidMount() {
    let story = await this.getInitialStory()
    if(story.content) this.setState({ story })
    setTimeout(() => StoryblokService.initEditor(this), 200)
  }

  render() {
    return (
       <Layout location={this.props.location}>
        <Page blok={this.state.story.content} />
      </Layout>
    )
  }
}

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

Since the graphQL source plugin loads the content at build time, we need to update our story content during client runtime with our Storyblok Service, so whenever we save in Storyblok, we can see the updates live. Once our component loaded, we'll reload our Story from the API and initialize the Editor to listen for our change events.

Storyblok editing capabilities

With this in place, the complete live preview is setup. You should be able to click on the components directly, edit them, and see the updates change live in the preview.

Build a multilanguage navigation

To build dynamic navigation, you have several options. One approach is to use the links API to generate the navigation automatically from your content tree. Another option, the one we'll be using in this tutorial, is to create a global content entry/item which will contain the global configurations of our website.

Create a global settings content item

We are going to create a global configuration item for each language. First, we must create a folder for the English language named en in the root folder for your content section.

Folder Creation

Now inside the new en folder we want to create a new entry called settings.

Gatsby Settings

Open the new entry and change the real path of the Settings entry to /, since it doesn't have it's own page.

Initialize the schema for your navigation settings by defining the key main_navi with the type Blocks (see the image below).

Setting Config

Create and add a block called NavItem for the main_navi (second image below) with the following schema definition:

Schema of the NavItem:
-- name (type: Text)
-- link (type: Link)

Inside the folder en, create a content item/entry called settings with the new content type settings. We will define the navigation items and other global configurations for our website in this entry for each language.

Storyblok editing capabilities

Create two Nav Items for Home and About page with some URLs. Your content in the sidebar should look like the next screenshot.

Gatsby Navigation

Now we need to create the Navigation component and load the correct settings.

components/Navigation.js

import Link from 'gatsby-link'
import React from "react"
import SbEditable from 'storyblok-react'

const Nav = ({ settings, lang }) => (
  <header className="w-full bg-white">
    <nav className="" role="navigation">
      <div className="container mx-auto p-4 flex flex-wrap items-center md:flex-no-wrap">
        <div className="mr-4 md:mr-8">
          <Link to="/"> 
            <svg width="69" height="66" xmlns="http://www.w3.org/2000/svg"><g fill="none" fillRule="evenodd"><path fill="#FFF" d="M-149-98h1440v938H-149z"/><path d="M37.555 66c17.765 0 27.051-16.38 30.24-33.415C70.986 15.549 52.892 4.373 35.632.52 18.37-3.332 0 14.876 0 32.585 0 50.293 19.791 66 37.555 66z" fill="#672E9B"/><path d="M46.366 42.146a5.55 5.55 0 01-1.948 2.043c-.86.557-1.811 1.068-2.898 1.3-1.087.279-2.265.511-3.487.511H22V20h18.207c.905 0 1.675.186 2.4.604a6.27 6.27 0 011.811 1.485 7.074 7.074 0 011.54 4.504c0 1.207-.317 2.368-.905 3.482a5.713 5.713 0 01-2.718 2.507c1.45.418 2.582 1.16 3.442 2.229.815 1.114 1.223 2.553 1.223 4.364 0 1.16-.226 2.136-.68 2.971h.046z" fill="#FFF"/></g></svg>
          </Link>
        </div>
        <div className="text-purple-700">
          <p className="text-lg">Storyblok</p>
          <p>GatsbyJS Demo</p>
        </div>
        <div className="ml-auto md:hidden">
          <button
            className="flex items-center px-3 py-2 border rounded"
            type="button"
          >
            <svg
              className="h-3 w-3"
              viewBox="0 0 20 20"
              xmlns="http://www.w3.org/2000/svg"
            >
              <title>Menu</title>
              <path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z" />
            </svg>
          </button>
        </div>
        <div className="w-full md:w-auto md:flex-grow md:flex md:items-center">
          <ul className="flex flex-col mt-4 -mx-4 pt-4 border-t md:flex-row md:items-center md:mx-0 md:mt-0 md:pt-0 md:mr-4 md:ml-auto lg:mr-8 md:border-0">
            {settings &&
              settings.content.main_navi.map((navitem, index) => (
                <SbEditable content={navitem} key={navitem._uid}>
                <li key={index}>
                  <Link to={`/${navitem.link.cached_url.replace("en/", "").replace("home", "")}`} prefetch="true" className="block px-4 py-1 md:p-2 lg:px-8">
                    {navitem.name}
                  </Link>
                </li>
                </SbEditable>
              ))}
          </ul>
          <ul className="flex flex-col mt-4 -mx-4 pt-4 border-t md:flex-row md:items-center md:mx-0 md:mt-0 md:pt-0 md:border-0">
            <li>
              <Link to="/" 
              className={`block px-4 py-1 md:p-2 rounded-lg lg:px-4 ${lang === 'en' ? "bg-purple-700 text-white" : ""}`}
              >
                EN
              </Link>
            </li>
            <li>
              <Link to="/de" 
              className={`block px-4 py-1 md:p-2 rounded-lg lg:px-4 ${lang === 'de' ? "bg-purple-700 text-white" : ""}`}
              >
                DE
              </Link>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </header>
);

export default Nav;

Then we import our Navigation component inside our Layout file and load the settings for all languages via GraphQL. Then pass the settings from the correct language, which we can get from our current URL to the navigation.

components/layout.js

import React from "react"
import Navigation from './Navigation'
import Footer from './Footer'
import { Helmet } from 'react-helmet'
import { useStaticQuery, graphql } from "gatsby"

import StoryblokService from '../utils/storyblok-service'

export default function Layout({ children, location }){
  const { settings } = useStaticQuery(graphql`
  query Settings {
    settings: allStoryblokEntry(filter: {field_component: {eq: "settings"}}) {
      edges {
        node {
          name
          full_slug
          content
        }
      }
    }
  } 
  `)
  let { pathname } = location
  let language = pathname.split("/")[1].replace('/', '')
  let activeLanguage = ['de', 'en'].includes(language) ? language : 'en'
  let correctSetting = settings.edges.filter(edge => edge.node.full_slug.indexOf(activeLanguage) > -1)
  let hasSetting = correctSetting && correctSetting.length ? correctSetting[0].node : {}
  let content = typeof hasSetting.content === 'string' ? JSON.parse(hasSetting.content) : hasSetting.content
  let parsedSetting = Object.assign({}, content, {content: content})

  return (
    <div className="bg-gray-300">
      <Helmet
          script={[
            {"src": `//app.storyblok.com/f/storyblok-latest.js?t=${StoryblokService.token}`, 
            "type": "text/javascript"}
          ]}
      />
      <Helmet
          script={[
            {
            "innerHTML": `var StoryblokCacheVersion = '${StoryblokService.getCacheVersion()}';`,
            "type": "text/javascript"
            }
          ]}
      />
      <Navigation settings={parsedSetting} lang={activeLanguage} />
      <main>
      { children }
      </main>
    </div>
  )
}

Adding Tailwind

At this point, it's also a good idea to add Tailwind. Follow this tutorial to learn how to add Tailwind to Gatsby. to have the correct stylings. If you have issues with the fs resolution follow this tutorial.

$ npm install tailwindcss postcss gatsby-plugin-postcss --save
$ npx tailwindcss init # optional

Include the postcss plugin in your gatsby-config.js file and add a postcss.config.js file to your root folder.

# gatsby-config.js
plugins: [`gatsby-plugin-postcss`],

# postcss.config.js
module.exports = () => ({
  plugins: [require("tailwindcss")],
})

Remove all the CSS from thelayout.css file and just add the following tailwind imports:

// layout.css
@tailwind base;
@tailwind components;
@tailwind utilities;

To create a second language duplicate your en folder and adapt the settings to a different language.

Storyblok Duplicate Content

To create the other language page, we will make use of the Gatsby plugin gatsby-plugin-i18n. Install the plugin first:

npm i gatsby-plugin-i18n --save

Then we need to add the plugin to our gatsby.config.js file:

module.exports = {
  /* Your site config here */
  plugins: [
  {
    resolve: `gatsby-plugin-i18n`,
    options: {
      langKeyDefault: 'en',
      langKeyForNull: 'en',
      prefixDefault: false,
      useLangKeyLayout: false,
    },
  }
 ],
}

Then we duplicate the pages/index.js file to pages/index.de.jsand change the slug in the graphQL query to "de/home"

# pages/de.js
export const query = graphql`
  query {
    story: storyblokEntry(full_slug: { eq: "de/home" }) {
      name
      content
      full_slug
      uuid
    }
  }
`

Now we need to duplicate our Home page in Storyblok into the de folder.

Duplicate Home

Finally restart your server and you should be able to see the multilanguage navigation and toggle between the two different language pages.

Multilanguage Navigation

Build a blog section

A common task when creating a website is to develop an overview page of collections like news, blog posts or products. In our example, we will create a simple blog. We can use the gatsby-node.js to automatically generate the different blog pages for us.

Create content of the blog in Storyblok

Inside the en folder create a new folder blog with a content type blogpost, which is based on the Post blueprint.

If you did all correctly, you can now create your first blog post. Navigate inside the blog folder and create your first blog post.

Blog Content

Create the templates in Gatsby

Create the folder src/templates and place the file blog-entry.js with the following code inside. This is very similar to what is happening in our index file, but instead we will dynamically generate all pages via the pageContext, which is passed from the gatsby-node.js file in the next section.

src/templates/blog-entry.js

import React from "react"
import DynamicComponent from "../components/DynamicComponent"
import Layout from "../components/layout"
import StoryblokService from '../utils/storyblok-service'

export default class extends React.Component {
  state = {
    story: {
      content: this.props.pageContext.story ? JSON.parse(this.props.pageContext.story.content) : {}
    }
  }

  async getInitialStory() {
    let { data: { story } } = await StoryblokService.get(`cdn/stories/${this.props.pageContext.story.full_slug}`)
    return story
  }

  async componentDidMount() {
    let story = await this.getInitialStory()
    if(story.content) this.setState({ story })
    setTimeout(() => StoryblokService.initEditor(this), 200)
  }

  render() {
    return (
       <Layout location={this.props.location}>
        <DynamicComponent blok={this.state.story.content} key={this.props.pageContext.story._uid} />
      </Layout>
    )
  }
}

And create a new component Blogpost.js, which will automatically be loaded via the Dynamic Component. Since posts can have Richtext fields, we need to render the Richtext passed from the Storyblok API. This is possible with our Storyblok Service like described in this tutorial or by using the storyblok-rich-text-react-renderer. For now we will install the renderer package with npm install storyblok-rich-text-react-renderer --save.

src/components/Blogpost.js

import React from "react"
import SbEditable from "storyblok-react"
import { render } from "storyblok-rich-text-react-renderer"

const BlogPost = ({ blok }) => {
  return (
    <SbEditable content={blok} key={blok._uid}>
      <div className="bg-white w-full">
        <div className="max-w-3xl mx-auto text-center pt-20 flex flex-col items-center">
          <h1 className="text-5xl font-bold font-serif text-purple-700 tracking-wide">
            {blok.title}
          </h1>
          <p className="text-gray-500 text-lg max-w-lg">{blok.intro}</p>
          <img className="w-full bg-gray-300 my-16" src={blok.image} />
        </div>
      </div>
      <div className="max-w-3xl mx-auto text-center pt-20 flex flex-col items-center">
        <div className="leading-relaxed text-xl text-left text-gray-800 drop-cap">
          {render(blok.long_text)}
        </div>

        <div className="py-16 max-w-sm p-2 sm:p-10 text-center flex flex-col">
          <div className="p-4 bg-purple-700 rounded-full mx-auto">
          </div>
        </div>
      </div>
    </SbEditable>
  )
}

export default BlogPost

We will also need to import the BlogPost in our DynamicCompoents file, so we can dynamically include it where we'll need it.

src/components/DynamicComponent.js

import React from 'react'
import Teaser from './Teaser'
import Feature from './Feature'
import Grid from './Grid'
import BlogPost from './BlogPost'
import Placeholder from './Placeholder'

const Components = {
  'teaser': Teaser,
  'grid': Grid,
  'feature': Feature,
  'blogpost': BlogPost,
}

const DynamicComponent = ({blok}) => {
  if (typeof Components[blok.component] !== 'undefined') {
    const Component = Components[blok.component]
    return <Component blok={blok} key={blok._uid}/>
  }
  return <Placeholder componentName={blok.component}/>
}

export default DynamicComponent

Dynamically generate pages

Open the file gatsby-node.js and add the following code for the dynamic generation of pages. The call to createPage will create a page for each item with the content type blogpost you have created in Storyblok.

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/blog-entry.js')

    resolve(
      graphql(
        `{
          stories: allStoryblokEntry(filter: {field_component: {eq: "blogpost"}}) {
            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) => {
          const page = {
            path: `/${entry.node.full_slug}`,
            component: storyblokEntry,
            context: {
              story: entry.node
            }
          }
          createPage(page)
        })
      })
    )
  })
}

If we open our first post in Storyblok now you should be able to see your first post. You might need to restart the server in between.

Storyblok Post

Creating a client-side fallback for new posts

Now when we create a new post and haven't rebuilt our bundle on the server, we will see the Gatsby 404 page. In order to avoid that, we can load the data on the client side in a pages/404.js file.

And then we add a new 404.js file to load our stories if they exist.

pages/404.js

import React from "react"
import Page from '../components/Page'
import Layout from "../components/Layout"
import StoryblokService from '../utils/storyblok-service'

export default class extends React.Component {
  state = {
    story: {}
  }

  async getInitialStory() {
    const { pathname } = this.props.location
    let { data: { story } } = await StoryblokService.get(`cdn/stories/${pathname}`)
    return story
  }

  async componentDidMount() {
    let story = await this.getInitialStory()
    if(story.content) this.setState({ story })
    setTimeout(() => StoryblokService.initEditor(this), 200)
  }

  render() {
    let content = (<h1>Not found</h1>)
    if(this.state.story.content) content = (<Page blok={this.state.story.content} />)
    return (
      <Layout location={this.props.location}>
       { content }
      </Layout>
    )
  }
}

Now when we click Preview custom 404 pageon the Gatsby Developer Preview, we should see our blogpost without reloading.

Creating a Blog Overview Page

Let's quickly create an overview page of our posts before we move on. First we need to create the two Blog Overview Pages in Storyblok. Go into the en/blog folder {1}, create a new entry {2}, define it as root {4} with a content type of page {5}.

Blog Overview

Then open the entry, add a tite like All Posts {1} and add a new blok {2} to the body called posts-list {3}.

Gatsby Multi Option

Then define the schema for the new posts-list component by adding a new item called posts {1} with a type of multi option.

Gatsby Multi Option

The settings for the posts field will be Multi-Options {1}, source Stories {2} and restrict to cotent type blogpost {3}.

Storyblok editing capabilities

Once we have these settings, we should be able to select a few for our posts {1}.

Storyblok editing capabilities

Now in order to display our overview of posts, we need to create two new files blog.js and blog.de.js.

blog.js

import React from "react"
import Page from '../components/Page'
import Layout from "../components/Layout"
import { graphql } from 'gatsby'
import StoryblokService from '../utils/storyblok-service'

export default class extends React.Component {
  constructor(props) {
    super(props);

    let content =  this.props.data.story ? JSON.parse(this.props.data.story.content) : {}
    // we need to join the story posts with the full posts information
    let posts = this.props.data.posts.edges.map(n => n.node)
    let postList = content.body.find(c => c.component === 'posts-list')
    if(postList) {
        let index = content.body.indexOf(postList)
        let joinedPosts = postList.posts.map(uuid => {
            let fullPost = posts.find(p => p.uuid === uuid)
            let content = fullPost ? JSON.parse(fullPost.content) : ""
            return Object.assign({}, fullPost, { content })
        })
        content.body[index].posts = joinedPosts  
    }   
    this.state = {
        story: {
          content
        }
    };
  }

  async getInitialStory() {
    let { data: { story } } = await StoryblokService.get(`cdn/stories/${this.props.data.story.full_slug}`,{
        "resolve_relations": "posts-list.posts"
      })
    return story
  }

  async componentDidMount() {
    let story = await this.getInitialStory()
    if(story.content) this.setState({ story })
    setTimeout(() => StoryblokService.initEditor(this), 200)
  }

  render() {
    return (
      <Layout location={this.props.location}>
        <Page blok={this.state.story.content} />
      </Layout>
    )
  }
}

export const query = graphql`
  query {
    story: storyblokEntry(full_slug: { eq: "en/blog/" }) {
      name
      content
      full_slug
      uuid
    }
    posts: allStoryblokEntry(filter: {full_slug: {regex: "/en/blog/(.)+/"}}) {
      edges {
        node {
          full_slug
          uuid
          name
          content
        }
      }
    }
  }
`

The blog.de.js file is the same as the blog.js file with the only difference in the graphQL query:

blog.de.js

# same as blog.js, just the query is different

export const query = graphql`
  query {
    story: storyblokEntry(full_slug: { eq: "de/blog/" }) {
      name
      content
      full_slug
      uuid
    }
    posts: allStoryblokEntry(filter: {full_slug: {regex: "/de/blog/(.)+/"}}) {
        edges {
          node {
            full_slug
            uuid
            name
            content
          }
        }
    }
  }
`

Finally we need to create a PostList.js component to display the blogposts.

components/PostList.js

import React from "react"
import SbEditable from "storyblok-react"

const PostsList = ({ blok }) => {
  return (
    <SbEditable content={blok} key={blok._uid}>
      <div className="container mx-auto">
      <ul>
        {blok.posts.map(post => {
          return (
            <li
              key={post.name}
              className="max-w-4xl px-10 my-4 py-6 rounded-lg shadow-md bg-white"
            >
              <div className="flex justify-between items-center">
                <span className="font-light text-gray-600">
                  {`
                    ${new Date(post.created_at).getDay()}.
                    ${new Date(post.created_at).getMonth()}.
                    ${new Date(post.created_at).getFullYear()}`}
                </span>
              </div>
              <div className="mt-2">
                <a
                  className="text-2xl text-gray-700 font-bold hover:text-gray-600"
                  href={`/${post.full_slug}`}
                >
                  {post.content.title}
                </a>
                <p className="mt-2 text-gray-600">{post.content.intro}</p>
              </div>
              <div className="flex justify-between items-center mt-4">
                <a
                  className="text-blue-600 hover:underline"
                  href={`/${post.full_slug}`}
                >
                  Read more
                </a>
              </div>
            </li>
          )
        })}
      </ul>
      </div>
    </SbEditable>
  )
}

export default PostsList

And add it to the DynamicComponent.js file.

import React from 'react'
import PostsList from './PostsList'

const Components = {
  'posts-list': PostsList
}

export default DynamicComponent

Finally you should be able to see an overview page on the /blog and /de/blog path.

Storyblok editing capabilities

Deploying to Vercel

You have multiple options for the deployment of your website/application to go live or to preview the environment. One of the easiest ways is to use Vercel and deploy using the command line or their outstanding GitHub Integration.

First, create an account on Vercel and install their CLI application.

npm i -g vercel

Deploy your website by running the vercel in your console.

vercel

Take a look at the deployed Demo project.

Multi-language setup complete!

Congratulations! You now have a multi-language Gatsby website with global navigation and live preview.

Conclusion

Gatsby.js and Storyblok make it super easy for your content editors to manage content. With this setup, Storyblok’s true live preview can be mounted on your statically generated website so you don’t even need to run a server in the background.

ResourceLink
Github repository of this tutorialgithub.com/storyblok/gatsbyjs-multilanguage-website
Demo Projectgatsby-multilanguage-website.vercel.app
Gatsby.jsgatsbyjs.org
React.jsreactjs.org
Storyblok AppStoryblok
Lisi Linhart

Author

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.