Contents

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

This guide is for beginners and professionals who want to build a full-blown multilanguage website using Gatsby.js.

With this step by step guide, you will get a Gatsby website using an Storyblok’s api for the multilanguage content and true live preview.

You can clone the code of the tutorial at github.com/storyblok/gatsby-storyblok-boilerplate

Environment setup

Requirements

  • Basic understanding of Gatsby.js
  • Gatsby.js & their CLI
  • NodeJS
  • NPM
  • An account on Storyblok.com to manage content

If not done yet install NodeJs, NPM and the Gatsby.js CLI with npm install --global gatsby-cli. We will start with initializing project with the Gatsby.js starter template.

gatsby new my-multilanguage-website
cd my-multilanguage-website

Add the Storyblok plugin

Before we run Gatsby we 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.

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

Now you should copy the preview token of a new empty Storyblok space to your clipboard.

Add the config for the Storyblok’s source plugin in the file gatsby-config.js and 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: 'draft'
      }
    },
    ...
  ]
}

Now let’s start the Gatsby.js server on port 8000 to see if everything is working. Opening localhost:8000 in your browser should show a welcome message.

console
gatsby develop

Creating the editor page

For the Storyblok’s true live preview feature we create a page that acts as a container for all editable content.

Add the file components.js to the components folder with following content an then create the editor.js file below.

src/components/components.js
export default {}
src/pages/editor.js
import React from 'react'
import Components from '../components/components.js'
import SbEditable from 'storyblok-react'
import config from '../../gatsby-config'

const loadStoryblokBridge = function(cb) {
  let sbConfigs = config.plugins.filter((item) => {
    return item.resolve === 'gatsby-source-storyblok'
  })
  let sbConfig = sbConfigs.length > 0 ? sbConfigs[0] : {}
  let script = document.createElement('script')
  script.type = 'text/javascript'
  script.src = `//app.storyblok.com/f/storyblok-latest.js?t=${sbConfig.options.accessToken}`
  script.onload = cb
  document.getElementsByTagName('head')[0].appendChild(script)
}

const getParam = function(val) {
  var result = ''
  var tmp = []

  window.location.search
    .substr(1)
    .split('&')
    .forEach(function (item) {
      tmp = item.split('=')
      if (tmp[0] === val) {
        result = decodeURIComponent(tmp[1])
      }
    })

  return result
}

class StoryblokEntry extends React.Component {
  constructor(props) {
    super(props)
    this.state = {story: null}
  }

  componentDidMount() {
    loadStoryblokBridge(() => { this.initStoryblokEvents() })
  }

  loadStory(payload) {
    window.storyblok.get({
      slug: payload.storyId, 
      version: 'draft'
    }, (data) => {
      this.setState({story: data.story})
    })
  }

  initStoryblokEvents() {
    this.loadStory({storyId: getParam('path')})

    let sb = window.storyblok

    sb.on(['change', 'published'], (payload) => {
      this.loadStory(payload)
    })

    sb.on('input', (payload) => {
      if (this.state.story && payload.story.id === this.state.story.id) {
        payload.story.content = sb.addComments(payload.story.content, payload.story.id)
        this.setState({story: payload.story})
      }
    })

    sb.pingEditor(() => {
      if (sb.inEditor) {
        sb.enterEditmode()
      }
    })
  }

  render() {
    if (this.state.story == null) {
      return (<div></div>)
    }

    let content = this.state.story.content

    return (
      <SbEditable content={content}>
      <div>
        {Components[content.component] ? React.createElement(Components[content.component], {key: content._uid, blok: content}) : `Component ${content.component} not created yet`}
      </div>
      </SbEditable>
    )
  }
}

export default StoryblokEntry

The next step is to configure the preview domain inside Storyblok. After you have created a new empty space click on the first content item and scroll down to the domain setting to set following value:

localhost:8000/editor?path=

Creating the homepage components

To render the full homepage we will need to create some components.

The components Grid, Teaser and Feature are subcomponents of the component Page which is a Storyblok content type. For simplicity we decided to organize everything in components. If you want to create a blog article content type you can just add a BlogArticle component to the file components.js and you’re done.

Open the file src/components/components.js and add the four basic components as following.

src/components/components.js
import Page from './page'
import Grid from './grid'
import Teaser from './teaser'
import Feature from './feature'

export default {
  page: Page,
  grid: Grid,
  teaser: Teaser,
  feature: Feature
}

Add the feature component

src/components/feature.js
import React from 'react'
import SbEditable from 'storyblok-react'

const Feature = (props) => (
  <SbEditable content={props.blok}>
    <div className="col-4">
      <h2>{props.blok.name}</h2>
    </div>
  </SbEditable>
)

export default Feature

Add the grid component

src/components/grid.js
import React from 'react'
import Components from './components.js';
import SbEditable from 'storyblok-react'

const Grid = (props) => (
  <SbEditable content={props.blok}>
    <div className="container">
      <div className="row">
        {props.blok.columns.map((blok) =>
          React.createElement(Components[blok.component], {key: blok._uid, blok: blok})
        )}
      </div>
    </div>
  </SbEditable>
)

export default Grid

Add the page component

src/components/page.js
import React from 'react'
import Components from './components.js';

const Page = (props) => (
  <div>
    {props.blok.body && props.blok.body.map((blok) => React.createElement(Components[blok.component], {key: blok._uid, blok: blok}))}
  </div>
)

export default Page

Add the teaser component

src/components/teaser.js
import React from 'react'
import Link from 'gatsby-link'
import SbEditable from 'storyblok-react'

const Teaser = (props) => (
  <SbEditable content={props.blok}>
    <div className="jumbotron jumbotron-fluid">
      <div className="container">
        <h1 className="display-4">{ props.blok.headline }</h1>
        <p className="lead">This is a modified jumbotron that occupies the entire horizontal space of its parent.</p>
        <p className="lead">
          <Link className="btn btn-primary" to={'/blog/'}>
            Blog Posts
          </Link>
        </p>
      </div>
    </div>
  </SbEditable>
)

export default Teaser

Create the html container

To add some basic stylings we’ll add bootstrap CSS by creating the file html.js in the src folder as following.

src/html.js
import React from 'react';

export default class HTML extends React.Component {
  render() {
    return (
      <html lang="en">
        <head>
          <meta charSet="utf-8" />
          <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0"
          />
          {this.props.headComponents}
          <link
            rel="stylesheet"
            href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"
          />
        </head>
        <body>
          <div
            id="___gatsby"
            dangerouslySetInnerHTML={{ __html: this.props.body }}
          />
          {this.props.postBodyComponents}
        </body>
      </html>
    );
  }
}

Optionally you can load a configurable site navigation from a global settings content item in Storyblok and render it in your template. Create a content item in Storyblok with the slug “settings” and extend your gatsby-node.js file with the content of following gist: https://gist.github.com/onefriendaday/3460c2ead0779d34097dc82042cd341a

Testing

After creating all components you should already be able to manage content using the true live preview in Storyblok. Go to the Storyblok editor and create a new page and insert a teaser in the compose mode to test if everything is working.

Setup the static file generation

In the last steps we made our content editable but for the static file generation we need two more things: A template and a gatsby-node.js file which generates pages dynamically.

Create the template

Create the folder src/templates and place the file storyblok-entry.js with following code inside.

src/templates/storyblok-entry.js
import React from 'react'
import Components from '../components/components.js'

class StoryblokEntry extends React.Component {
  static getDerivedStateFromProps(props, state) {
    if (state.story.uuid === props.pageContext.story.uuid) {
      return null
    }

    return StoryblokEntry.prepareStory(props)
  }

  static prepareStory(props) {
    const story = Object.assign({}, props.pageContext.story)
    story.content = JSON.parse(story.content)
    
    return { story }
  }

  constructor(props) {
    super(props)

    this.state = StoryblokEntry.prepareStory(props)
  }

  render() {
    let content = this.state.story.content

    return (
      <div>
        {React.createElement(Components[content.component], {key: content._uid, blok: content})}
      </div>
    )
  }
}

export default StoryblokEntry

Dynamically generate pages

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

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

    resolve(
      graphql(
        `{
          allStoryblokEntry {
            edges {
              node {
                id
                name
                created_at
                uuid
                slug
                full_slug
                content
                is_startpage
                parent_id
                group_id
              }
            }
          }
        }`
      ).then(result => {
        if (result.errors) {
          console.log(result.errors)
          reject(result.errors)
        }

        const entries = result.data.allStoryblokEntry.edges
        entries.forEach((entry, index) => {
          let pagePath = entry.node.full_slug == 'home' ? '' : `${entry.node.full_slug}/`

          createPage({
            path: `/${pagePath}`,
            component: storyblokEntry,
            context: {
              story: entry.node
            }
          })
        })
      })
    )
  })
}

Important: Remove the file src/pages/index.js in order to generate Storyblok’s homepage instead of the one of the Gatsby boilerplate.

Try out generating the pages executing a gatsby build process in the command line:

$ gatsby build

Deploy only published content

To generate only pages that have the status “published” we’ll need to adapt the file gatsby-config.js and add a check on the environment type. When the NODE_ENV is set to production we’ll use the published version and otherwise the draft version.

module.exports = {
  ...
  plugins: [
    {
      resolve: 'gatsby-source-storyblok',
      options: {
        accessToken: 'bJQb8KcUXW4NJ35XJFwGuwtt',
        homeSlug: 'home',
        version: process.env.NODE_ENV == 'production' ? 'published' : 'draft'
      }
    },
    ...

In the next step I’ll explain how to publish content using Storyblok’s webhooks.

Publishing content

To publish content to your website you need to trigger a Gatsby build. To give the editor this ability you can use Storyblok’s published webhook feature to call a url when clicking on the publish button of a content item or use Storyblok’s free tasks app. Netlify, a Lambda function or a continuous integration tool can then handle the post request and generate the static website with Storyblok’s content.

Install the “Task-Manager” from the Apps section and create a task with the webhook url from Netlify or other CI tools.

Multilanguage content

Storyblok comes with a multi-language and country architecture that let’s you create flexible content structures for each dimension. To make a website multilingual you can organize two languages in two folders and the paths of the content items will automatically be reflected in Gatsby’s url structure.

To create a new language you can then just duplicate an existing folder and beginn translating the content.

You can read more on this topic at the docs section.

Conclusion

Gatsby.js and Storyblok makes 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/gatsby-storyblok-boilerplate
Gatsby.jsgatsbyjs.org
React.jsreactjs.org
Storyblok AppStoryblok

More to read...

About the author

Alexander Feiglstorfer

Alexander Feiglstorfer

Passionate developer and always in search of the most effective way to resolve a problem. After working 13 years for agencies and SaaS companies using almost every CMS out there he founded Storyblok to solve the problem of being forced to a technology, proprietary template languages and the plugin hell of monolithic systems.