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

Contents

This guide is for beginners and professionals who want to build a full-blown multilanguage website using Next.js and Storyblok. With this step by step guide, you will get a dynamic Next.js website running on a now server, using a Storyblok API for the multilanguage content.

If you are in hurry, you can download the whole project at Github: storyblok/nextjs-multilanguage-website.

  1. Introduction

  2. Environment setup

  3. Build a homepage

  4. Build a navigation menu

  5. Build a blog section

  6. Adding another language

  7. Deploy to live

Environment setup

Requirements

If you haven't yet done so, install NodeJS and NPM.

We will start by initializing the project with the Next.js starter template given to us by the create-next-app CLI.

$ npx create-next-app
$ cd my-next-app
$ npm run dev

Next.js starts its server on port 3000 by default, so after running npm run dev, open your browser at http://localhost:3000.

Screenshot of Next.js

To track the changes we make over time, we will also initialize the Git repository.

$ git init && git add . && git commit -m 'init'

Build a skeleton

Now, we'll start to build the skeleton for your website. At the end, you'll have a navigation menu, a main page, an overview page for a blog, individual pages for blog posts, and multilanguage support, as well as some useful global utility CSS classes.

Global CSS in Next.js

Without the need for any additional CSS loader or Babel plugin, you can add the global attribute inside the JSX style tag <style jsx global> to define the global CSS. Because we will wrap each page in a Layout component, the CSS will be loaded on every single page.

Create the file components/layout.js and add the following content.

import React from 'react'
import Head from '../components/head'
import Nav from '../components/nav'

export default ({ children, settings = {} }) => (
  <div>
    <Head />
    <Nav settings={settings} />
    <div className="util__container">
      {children}
    </div>

    <style jsx global>{`
      article, aside, footer, header, hgroup, main, nav, section {
        display: block;
      }

      body {
        font-family: 'Zilla Slab', Helvetica, sans-serif;
        line-height: 1;
        font-size: 18px;
        color: #000;
        margin: 0;
        padding: 0;
      }

      .util__flex {
        display: flex;
      }

      .util__flex-col {
        flex: 0 0 auto;
      }

      .util__flex-eq {
        flex: 1;
      }

      .util__container {
        max-width: 75rem;
        margin-left: auto;
        margin-right: auto;
        padding-left: 20px;
        padding-right: 20px;
        box-sizing: border-box;
      }

      #nprogress .bar {
        background: #29d;
        position: fixed;
        z-index: 1031;
        top: 0;
        left: 0;
        width: 100%;
        height: 2px;
      }
    `}</style>
  </div>
)

Add a Google font to Next.js

In the components/layout.js file, we defined Zilla Slab as the font for the body element. As this is not a system font, we need to add it to the head section of our document. This means we need to build a custom NextHead component.

Open head.js and replace the code with the following:

import React from 'react'
import NextHead from 'next/head'

const Head = ({ title, description }) => (
  <NextHead>
    <meta charSet="UTF-8" />
    <title>{title || ''}</title>
    <meta name="description" content={description || ''} />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Zilla+Slab:400,700" />
  </NextHead>
)

export default Head

Wrap the content with the Layout component

Replace the content of pages/index.js with the following code, wrapping the content with the Layout component.

import React from 'react'
import Layout from '../components/layout'

export default class extends React.Component {
  render() {
    return (
      <Layout>
        <h1>Welcome to Next!</h1>
      </Layout>
    )
  }
}

Currently, the website should look similar to the following screenshot. In the next step, I'll show you how to create the homepage with a teaser and a feature section.

Nextjs Screenshot Tutorial

Now, let's commit that to Git. See my GitHub commit for reference.

$ git add . && git commit -m 'creates the skeleton'

Build a homepage

Install the Storyblok Client and React module

The Storyblok client allows you to request content from Storyblok's API, and the storyblok-react module gives you a wrapper component that creates editable blocks in the visual editor.

$ npm install storyblok-js-client storyblok-react --save

After installing the module, you'll need to initialize it with the preview token of your Storyblok space, which we'll do in the next step.

To get a token, sign up or log in at app.storyblok.com and click "Create new space". Select the "Create a new space" option and use the default, already selected service type, "Headless (API/SDKs)." Once the space has been created, you'll find your preview token on the dashboard.

Api-Keys

Create a Storyblok service

We're about to create a StoryblokService class that will initialize the client and provide you with some helpers. Create the folder utils and the file utils/storyblok-service.js. Copy and paste in the code below, and replace PREVIEW_TOKEN with your preview token from the Storyblok dashboard.

For development, it's sometimes useful to set the variable this.devMode to true so you can retrieve unpublished content from the API without needing to use Storyblok’s preview mode.

import StoryblokClient from 'storyblok-js-client'

class StoryblokService {
  constructor() {
    this.devMode = false // Always loads draft
    this.token = 'qvOwrwasP7686hfwBsTumAtt'
    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()
      window.storyblok.on(['change', 'published'], () => 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.content._uid === reactComponent.state.pageContent._uid) {
          reactComponent.setState({pageContent: window.storyblok.addComments(event.story.content, event.story.id)})
        }
      })
    }
  }

  setQuery(query) {
    this.query = query
  }

  getQuery(param) {
    return this.query[param]
  }

  bridge() {
    if (!this.getQuery('_storyblok') && !this.devMode) {
      return ''
    }
    return (<script src={'//app.storyblok.com/f/storyblok-latest.js?t=' + this.token}></script>)
  }
}

const storyblokInstance = new StoryblokService()

export default storyblokInstance

Pass the cache version from the server to the client

For the best performance, we recommend that you pass the cache version that gets generated on the server side to the client side. We do this by setting the window variable StoryblokCacheVersion in a script tag which gets handled later by the code in storyblok-service.js.

Create the file pages/_document.js and insert the following code. This way, all API requests to Storyblok will be cached in the CDN properly. You can read more about the cache technique here.

import React from 'react'
import Document, { Head, Main, NextScript } from 'next/document'
import StoryblokService from '../utils/storyblok-service'

export default class MyDocument extends Document {
  render() {
    return (
      <html>
        <Head>
          <script dangerouslySetInnerHTML={{__html: `var StoryblokCacheVersion = '${StoryblokService.getCacheVersion()}';` }}></script>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    )
  }
}

Get access to initial props

Create pages/_app.js and insert the following code:

import React from "react"
import App, { Container } from "next/app"

class MyApp extends App {
    static async getInitialProps ({ Component, ctx, router }) {
        let pageProps = {}
        if (Component.getInitialProps) {
            pageProps = await Component.getInitialProps(ctx)
        }
        return {
            pageProps,
        }
    }
    render () {
        const { Component, pageProps } = this.props
        return (
            <Container>
                <Component {...pageProps} />
            </Container>
        )
    }
}

export default MyApp

Add the Storyblok Bridge

To build a bridge between Storyblok and your website, you need to include a special script on your page. 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.

Let's include the script tag by calling the helper bridge() just before the closing tag for the first div in components/layout.js.

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

export default ({ children, settings = {} }) => (
  <div>
    ...
    {StoryblokService.bridge()}
  </div>
)

To render the full homepage, we'll need to create components for our demo Storyblok content. Add the file index.js to the components folder.

import React from 'react'
import Teaser from './teaser'
import Feature from './feature'
import Page from './page'
import Grid from './grid'

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

export default (blok) => {
  if (typeof Components[blok.component] !== 'undefined') {
    return React.createElement(Components[blok.component], { key: blok._uid, content: blok })
  }
  return React.createElement(() => (
    <div>The component {blok.component} has not been created yet.</div>
  ), {key: blok._uid})
}

Then create the following React.js components inside the components folder.

page.js

import React from 'react'
import Components from './index'

export default (props) => (
  <div>
    {props.content.body.map((blok) =>
      Components(blok)
    )}
  </div>
)

teaser.js

import React from 'react'
import Components from './index'
import SbEditable from 'storyblok-react'

export default class extends React.Component {
  render() {
    return (
      <SbEditable content={this.props.content}>
        <div className="teaser">
          Hello world!
        </div>
      </SbEditable>
    )
  }
}

grid.js

import React from 'react'
import Components from './index'
import SbEditable from 'storyblok-react'

export default (props) => (
  <SbEditable content="props.content">
    <div className="util__flex">
      {props.content.columns.map((blok) =>
        Components(blok)
      )}
    </div>
  </SbEditable>
)

feature.js

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

export default class extends React.Component {
  render() {
    const { content } = this.props
    return (
      <SbEditable content={content}>
        <div className="feature util__flex-eq">
          <h2>{content.name}</h2>
        </div>
      </SbEditable>
    )
  }
}

Add the homepage

Now that we've created all the components, let's update index.js, our homepage, with the code below.

import React from 'react'
import Components from '../components/index'
import Layout from '../components/layout'
import StoryblokService from '../utils/storyblok-service'

export default class extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      pageContent: props.page.data.story.content,
    }
  }

  static async getInitialProps({ query }) {
    StoryblokService.setQuery(query)

    return {
      page: await StoryblokService.get('cdn/stories/home')
    }
  }

  componentDidMount() {
    StoryblokService.initEditor(this)
  }

  render() {
    return (
      <Layout settings={""}>
        {/* We will define these settings later on */}
        {Components(this.state.pageContent)}
      </Layout>
    )
  }
}

The get method of the StoryblokService will load a JSON file that defines which components we'll render on the homepage. When the component finishes rendering, we'll initialize the Editor.

Updating the server URL

To connect your environment to the Storyblok composer, you'll need to make sure that your server points to the port on which your Next.js app runs, typically localhost:3000. You can find the field in the image below by selecting Content in the sidebar, then your Home entry, then the Compose tab.

You should also see some demo content visible in the sidebar, provided to you by Storyblok, namely the Teaser and Grid widgets. Our next goal is to render that demo content.

Since we've already created our own application, ignore Step 1 on the page. Skip ahead to Step 2, adding localhost:3000 to your server field and hitting "Go." Make sure you're also running your local application with npm run dev.

Storyblok host

Changing the real path field

Your website will now attempt to load inside the Storyblok interface. But it will show a 404 - Not Found page, because Storyblok, by default, uses the path /home for the homepage.

To change that, you'll need to go to the Config tab and place a slash, / in the Real Path field. Be sure to hit Save.

Head back to the Compose tab. When reloading http://localhost:3000/, you should now see the following. (Again, make sure you're running your application locally, as well!)

Components

Create your first block in Storyblok

We've just loaded the demo content of Storyblok, and now we'll extend the existing Teaser component with interactive slides. This video explains how to create your teaser and slide blocks in the Storyblok interface.

<div class="video"> <iframe width="560" height="315" src="https://www.youtube.com/embed/NMM1qbGx9eo?rel=0" title="youtube video" frameborder="0" allowfullscreen></iframe> </div>

Here's an image to get you started!

<img src="https://images.pexels.com/photos/273222/pexels-photo-273222.jpeg?auto=compress&cs=tinysrgb&h=750&w=1260" height="200" />

After you add the schema and the content to Storyblok per the steps laid out in the above video, we need to add the React.js Slide component to our project. Create components/slide.js with the following content.

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

export default ({ content }) => (
  <SbEditable content={content}>
    <div className="slide">
      <img src={content.image} />
      <style jsx>{`
        .slide img {
          width: 100%;
          max-height: 500px;
        }
      `}</style>
    </div>
  </SbEditable>
)

Add the new component to your components/index.js file.

...
import Slide from './slide'

const Components = {
  ...
  'slide': Slide
}

...

Now it's time to implement the logic to show the slides in the Teaser component. You can use any React.js slider plugin for a more advanced slider, but let's keep it simple for now. Replace the code in teaser.js with the following.

import React from 'react'
import Components from './index'
import SbEditable from 'storyblok-react'

export default class extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      currentSlide: 0
    }
  }

  slide() {
    let slides = this.props.content.body.filter((slide, index) => {
      return this.state.currentSlide === index
    })
    if (slides.length) {
      return slides[0]
    }
    return null
  }

  pagClass(index) {
    return 'teaser__pag-dot ' + (index == this.state.currentSlide ? 'teaser__pag-dot--current' : '')
  }

  handleDotClick(index) {
    this.setState({
      currentSlide: index
    })
  }

  render() {
    const { content } = this.props
    return (
      <SbEditable content={content}>
        <div className="teaser">
          {this.slide() ? Components(this.slide()) : ''}
          <div className="teaser__pag">
            {content.body.map((blok, index) =>
              <button key={index} onClick={() => this.handleDotClick(index)}
                className={this.pagClass(index)}>Next</button>
            )}
          </div>

          <style jsx>{`
            .teaser__pag {
              width: 100%;
              text-align: center;
              margin: 30px 0;
            }

            .teaser__pag-dot {
              text-indent: -9999px;
              border: 0;
              border-radius: 50%;
              width: 17px;
              height: 17px;
              padding: 0;
              margin: 5px 6px;
              background-color: #ccc;
              -webkit-appearance: none;
              cursor: pointer;
            }

            .teaser__pag-dot--current {
              background-color: #000;
            }
          `}</style>
        </div>
      </SbEditable>
    )
  }
}

After saving, you should see the following result.

Extending the feature section

Each feature block currently has only a title. We'll now extend the feature block with a description and icon.

Click on the feature block (you can access it via the grid block) and then "Define schema." Add the fields description (with type textarea) and icon (with type image).

Open the Feature component (components/feature.js) and use the following code to add some more styling and make the component aware of the new fields.

import React from 'react'
import Components from './index'
import SbEditable from 'storyblok-react'

export default class extends React.Component {
  resizedIcon(index) {
    const { content } = this.props
    if (typeof content.icon !== 'undefined') {
      return content.icon.replace('//img2.storyblok.com/80x80', '//a.storyblok.com')
    }
    return null
  }

  render() {
    const { content } = this.props
    return (
      <SbEditable content={content}>
        <div className="feature util__flex-eq">
          <img src={this.resizedIcon()} className="feature__icon" />
          <h2>{content.name}</h2>
          <div className="feature__description">
            {content.description}
          </div>
          <style jsx>{`
            .feature {
              text-align: center;
              padding: 30px 10px 100px;
            }

            .feature__icon {
              max-width: 80px;
            }
          `}</style>
        </div>
      </SbEditable>
    )
  }
}

After you've filled in some content, you should have a fully editable homepage. Tip: Feel free to check out this site for free, no-attribution icons!

Build a navigation menu

To build a dynamic navigation menu, you have several possibilities. 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 item that contains the global configurations.

Because we're creating a multilanguage website, we'll create a global configuration for each language. Let's start with creating a folder for English en in the root folder for your Content.

Create a global settings content item

Inside the folder en, create a content item/entry called Settings with the new content type settings. This is the content item where we'll put navigation items and other global configurations for our website.

Change the real path for this folder to /.

Initialize the schema for your navigation by defining the key main_navi with the type Blocks.

Schema for settings:
-- main_navi (type: Blocks)

Add a block called NavItem for the nav item with the following schema definition:

Schema for NavItem:
-- name (type: Text)
-- link (type: Link)

Add Nav Items for Home and Blog! Once you do, you should see the following outlined structure in your right sidebar. Note: Don't worry about true URLs for these items for right now -- Just add a placeholder / in the Url field for each Nav Item for now.

Also: While we display our Nav Items in the image below, you also shouldn't see those in your own preview just yet!

You may, however, see a link to Github that was inserted by the create-next-app boilerplate for nav.js. Feel free to remove the links code from nav.js so the code looks like this:

import React from 'react'
import Link from 'next/link'

const Nav = () => (
  <nav>
    <ul>
      <li>
        <Link prefetch href="/">
          <a>Home</a>
        </Link>
      </li>
      <ul>
      </ul>
    </ul>
    <style jsx>{`
      :global(body) {
        margin: 0;
        font-family: -apple-system, BlinkMacSystemFont, Avenir Next, Avenir,
          Helvetica, sans-serif;
      }
      nav {
        text-align: center;
      }
      ul {
        display: flex;
        justify-content: space-between;
      }
      nav > ul {
        padding: 4px 16px;
      }
      li {
        display: flex;
        padding: 6px 8px;
      }
      a {
        color: #067df7;
        text-decoration: none;
        font-size: 13px;
      }
    `}</style>
  </nav>
)

export default Nav

Extending the properties in getInitialProps

As Storyblok’s JS SDK handles caching in production mode automatically, you can simply add the additional request StoryblokService.get('cdn/stories/en/settings') to your getInitialProps function in pages/index.js to get the global settings for your navigation.

As you can see below, we pass the settings to the Layout component via the settings prop: <Layout settings={this.props.settings.data.story}>.

import React from 'react'
import Components from '../components/index'
import Layout from '../components/layout'
import StoryblokService from '../utils/storyblok-service'

export default class extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      pageContent: props.page.data.story.content,
    }
  }

  static async getInitialProps({ query }) {
    StoryblokService.setQuery(query)

    let [page, settings] = await Promise.all([
      StoryblokService.get('cdn/stories/home'),
      StoryblokService.get('cdn/stories/en/settings')
    ])

    return {
      page,
      settings
    }
  }

  componentDidMount() {
    StoryblokService.initEditor(this)
  }

  render() {
    const { settings } = this.props
    const { pageContent } = this.state
    return (
      <Layout settings={settings.data.story}>
        {Components(pageContent)}
      </Layout>
    )
  }
}

Defining custom routes

Before we go on with implementing the logic to show the navigation items, we need to define custom routes for the multilanguage pages and the blog.

There is a nice npm module called next-routes that lets us add routes very easily. Install it via npm install next-routes --save and create the file routes.js.

const nextRoutes = require('next-routes')
const routes = module.exports = nextRoutes()

routes.add('blog', '/:language/blog')
routes.add('blog/detail', '/:language/blog/:slug')
routes.add('index', '/')

Next.js allows us to extend the routes by defining a custom NodeJS server. Let's create the file server.js and include our routes there.

const { createServer } = require('http')
const next = require('next')
const routes = require('./routes')

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handler = routes.getRequestHandler(app)

app.prepare()
.then(() => {
  createServer(handler)
  .listen(port, (err) => {
    if (err) throw err
    console.log(`> Ready on http://localhost:${port}`)
  })
})

Start the new server

To start the new server we created in the previous step, we'll need to modify the starter scripts in package.json.

{
  ...
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
  },
  ...
}

The command npm run dev will now start our custom server.

$ npm run dev

Access the data in the Nav component

You may not have noticed this earlier, but in our homepage, we pass the settings from the Layout component to the Nav component with another settings prop: <Nav settings={settings} />.

Because of that, we can easily access the navigation items with this.props.settings.content.main_navi and loop over them. Replace the content of components/nav.js with the following.

import React from 'react'
import { Link } from '../routes'

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

  render() {
    const { settings } = this.props
    return (
      <header className="top-header util__flex util__container">
        <nav className="top-header__col">
          <ul className="top-header__nav">
            {settings && settings.content.main_navi.map((navitem, index) =>
              <li key={index}>
                <Link route={navitem.link.cached_url} prefetch>
                  <a className="top-header__link">{navitem.name}</a>
                </Link>
              </li>
            )}
          </ul>
        </nav>
        <a href="/" className="top-header__col top-header__logo">
          <img src="//a.storyblok.com/f/42016/1096x313/0353bf6654/logo2.png" />
        </a>
        <nav className="top-header__col top-header__second-navi">
          <ul className="top-header__nav top-header__nav--right">
            <li>
              <Link route="/en/blog"><a className="top-header__link" >English</a></Link>
            </li>
          </ul>
        </nav>

        <style jsx>{`
          .top-header {
            justify-content: space-between;
            padding-top: 30px;
            padding-bottom: 30px;
          }

          .top-header__logo {
            text-align: center;
            position: absolute;
            left: 50%;
          }

          .top-header__logo img {
            position: relative;
            max-height: 60px;
            left: -50%;
            top: -15px;
          }

          .top-header__second-navi {
            text-align: right;
          }

          .top-header__nav {
            display: flex;
            list-style: none;
            margin: 0;
            padding: 0;
          }

          .top-header__nav li {
            padding: 0 20px 0 0;
          }

          .top-header__nav--right li {
            padding-right: 0;
            padding-left: 20px;
          }

          .top-header__link {
            line-height: 1.5;
            color: #000;
            text-decoration: none;
            border-bottom: 2px solid transparent;
            transition: border .15s ease;
          }

          .top-header__link:hover {
            border-bottom: 2px solid #000;
          }
        `}</style>
      </header>
    )
  }
}

Reloading the page, we should see now the header navigation with the configurable navigation items from Storyblok. You'll see both English and German language options. We'll add the actual German language features later on in this tutorial.

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. In the previous steps we already defined the routes for the blog in the file routes.js so let's create our folder structure now.

Looking back at our /:language/blog/slug URL, it's tied to a blog/detail page view. We'll be adding the following folder structure within the pages folder in our application.

pages/
--| blog/
----| index.js
----| detail.js

Add a blog detail page

Let's implement the blog detail page at pages/blog/detail.js, which will fetch the content from the API and then render the blog post with Markdown. We'll use the marked library for our Markdown parser.

Install the Markdown parser:

$ npm install marked --save

Create the blog directory within your pages folder, then add the file pages/blog/detail.js to accommodate the dynamic route for the blog posts.

import React from 'react'
import { Link } from '../../routes'
import Components from '../../components/index'
import Layout from '../../components/layout'
import StoryblokService from '../../utils/storyblok-service'
import SbEditable from 'storyblok-react'
import marked from 'marked'

export default class extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      pageContent: props.page.data.story.content
    }
  }

  static async getInitialProps({ asPath, query }) {
    StoryblokService.setQuery(query)

    let [page, settings] = await Promise.all([
      StoryblokService.get(`cdn/stories${asPath}`),
      StoryblokService.get(`cdn/stories/${query.language}/settings`)
    ])

    return {
      page,
      settings
    }
  }

  componentDidMount() {
    StoryblokService.initEditor(this)
  }

  body() {
    let rawMarkup = marked(this.state.pageContent.body)
    return { __html:  rawMarkup}
  }

  render() {
    const { settings } = this.prop
    const { pageContent } = this.state
    return (
      <Layout settings={settings.data.story}>
        <SbEditable content={pageContent}>
          <div className="blog">
            <h1>{pageContent.name}</h1>
            <div dangerouslySetInnerHTML={this.body()} className="blog__body"></div>
          </div>
        </SbEditable>

        <style jsx>{`
          .blog {
            padding: 0 20px;
            max-width: 600px;
            margin: 40px auto 100px;
          }

          .blog :global(img) {
            width: 100%;
            height: auto;
          }

          .blog__body {
            line-height: 1.6;
          }
        `}</style>
      </Layout>
    )
  }
}

Create the overview page

To set up a page listing all the blog posts, associated with the URL /:language/blog, we'll create an index.js file in the blog folder. Add the code below to that file.

Note: Storyblok's API can list all content items of a specific folder with the parameter starts_with. The number of content items you get back is, by default, 25, but you can change that with the per_page parameter and jump to the other pages with the page parameter.

import React from 'react'
import { Link } from '../../routes'
import Components from '../../components/index'
import Head from '../../components/head'
import Layout from '../../components/layout'
import StoryblokService from '../../utils/storyblok-service'

export default class extends React.Component {
  static async getInitialProps({ query }) {
    StoryblokService.setQuery(query)

    let [blogPosts, settings] = await Promise.all([
      StoryblokService.get('cdn/stories', {
        starts_with: `${query.language}/blog`
      }),
      StoryblokService.get(`cdn/stories/${query.language}/settings`)
    ])

    return {
      blogPosts,
      settings
    }
  }

  render() {
    const { settings, blogPosts } = this.props
    return (
      <Layout settings={settings.data.story}>
        {blogPosts.data.stories.map((blogPost, index) => {
          const { published_at, content: { name, intro }} = blogPost
          return (
             <div key={index} className="blog__overview">
                <h2>
                  <Link route={'/' + blogPost.full_slug}>
                    <a className="blog__detail-link">
                      {name}
                    </a>
                  </Link>
                </h2>
                <small>
                  {published_at}
                </small>
                <p>
                  {intro}
                </p>
              </div>
            )}
          )
        }
        <style jsx>{`
          .blog__overview {
            padding: 0 20px;
            max-width: 600px;
            margin: 40px auto 60px;
          }

          .blog__overview p {
            line-height: 1.6;
          }

          .blog__detail-link {
            color: #000;
          }
        `}</style>
      </Layout>
    )
  }
}

Create the blog content folder

After creating the React.js components for showing the blog content, we need to create a new folder in Storyblok to store the blog pages.

In Storyblok, inside your en folder, create a new folder called blog and choose blog as the folder's default content type.

Create the blog article

Go inside the blog folder and create a new content item called post_one. It will now automatically choose blog as the content type. Once the item has been created, select Define schema and add the schema fields intro (with the type Textarea), name (with the type Text) and body (with the type Markdown).

Add at least two posts with some content of your choosing. Tip: We enjoy Loren Ipsum's site for dummy content, especially since it offers translations into various languages -- Handy for the internationalization section of this tutorial!

Schema for blog
-- intro (type: Textarea)
-- name (type: Text)
-- body (type: Markdown)

You should now update your Nav Item for the Blog link. Select Url and add /en/blog as the Nav Item's URL. In the overview, when you click on the Blog link, you should see the list of blog articles.

You can leave the link for the Home Nav Item as is, since / indicates the root path for the site, which matches up with our own application's route assumptions.

Blog overview

Adding another language

As all our routes are dynamic, adding another language is simple. Go to your content repository, select the English en folder and click Copy. You can change the name of the folder to reflect the language you choose (In our case, we chose de to indicate German language content).

You can then go through your posts in the new folder and begin updating them with translated content! Tip: As mentioned earlier, we recommend Loren Ipsum's website for finding dummy content written in different languages. You can also use Google Translate.

Be sure to add a new link to your Nav component in your Next.js application, right below your link to your English translation of your site. Here's an example of how it could look:

<li>
  <Link route="/de/blog"><a className="top-header__link">German</a></Link>
</li>

Deploy to live

Now -- great pun, right? -- it's time to show your project to the world!

For easy, zero configuration deployment, you can use now. After you have downloaded and installed now's desktop application, you can deploy Next.js with a single command.

$ now

You will get a unique URL which you can then link via now alias to your custom domain.

Bonus: Progress bar

To give the user some visual feedback when the pages are loading, you can listen to the Next.js router events. We'll install the npm module nprogress to show a blue loading bar when the route changes. If you went through the previous steps, you should already have the necessary CSS code for #nprogress .bar in the file components/layout.js.

$ npm install nprogress --save

After installing the npm module, open the file components/head.js and add the following listeners.

import React from 'react'
import NextHead from 'next/head'
import NProgress from 'nprogress'
import Router from 'next/router'

Router.onRouteChangeStart = () => NProgress.start()
Router.onRouteChangeComplete = () => NProgress.done()
Router.onRouteChangeError = () => NProgress.done()
...

Now, you should see a fancy loading bar when browsing the website!

Any feedback?

I hope you liked this tutorial! If you have any questions or difficulties getting the app running, please don't hesitate to contact me using the chat icon on the right corner. I'm here to help!

ResourceLink
Github repository of this Tutorialgithub.com/storyblok/nextjs-multilanguage-website
The project livenext.blok.ink
NowNow
Next.jsNext.js
React.jsReact.js
Storyblok AppStoryblok

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.


More to read...