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 https://github.com/gatsbyjs/gatsby-starter-hello-world
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.

console
npm install --save gatsby-source-storyblok

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

Create the file gatsby-config.js and exchange YOUR_PREVIEW_TOKEN with the preview token of your space.

gatsby-config.js
module.exports = {
  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 Hello World 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.

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 = []

  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) {
    storyblok.get({
      slug: payload.storyId, 
      version: 'draft'
    }, (data) => {
      this.setState({story: data.story})
    })
  }

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

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

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

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

    let content = this.state.story.content

    return (
      <SbEditable content={content}>
      <div>
        {React.createElement(Components[content.component], {key: content._uid, blok: content})}
      </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.

Add the file components.js to the src/components folder and create 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>
    );
  }
}

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 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 {
  constructor(props) {
    super(props)
    let story = Object.assign({}, props.pathContext.story)
    story.content = JSON.parse(story.content)
    this.state = {story: story}
  }

  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

Create gatsby-node.js in the root of your project with following content. 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 Promise = require('bluebird')
const path = require('path')

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

  return new Promise((resolve, reject) => {
    const storyblokEntry = path.resolve('src/templates/storyblok-entry.js')

    resolve(
      graphql(
        `{
          allStoryblokEntry {
            edges {
              node {
                id
                name
                created_at
                published_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) => {
          createPage({
            path: `/${entry.node.full_slug}/`,
            component: storyblokEntry,
            context: {
              story: entry.node
            }
          })
        })
      })
    )
  })
}

Fine, 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. For that you can use Storyblok’s published webhook feature to call a url when clicking on the publish button of a content item. Netlify, a Lambda function or a continuous integration tool can then handle the post request and generate the static website with Storyblok’s content.

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.

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.