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

Contents

This guide is for the beginners and the 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.sh server, using a Storyblok API for the multilanguage content.

If you are in a 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 Node.js and NPM.

We will start by initializing the project with the Next.js starter template. Create or open a folder, where you want to store your project and run the following command in the terminal.

$ npm init next-app
# or
$ yarn create next-app

Define the name of your website and after that inside the created folder you can run npm run dev or yarn dev (depends on the package manager you are using) in command line. Next.js will start your local development environment at http://localhost:3000 and you should see the introduction screen of the Next.js app.

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. In the end, you'll have a navigation menu, a homepage, the blog overview page, detail 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 of 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.

Create components/head.js and add the following code:

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

Create a navigation file in the components folder

We just noticed small changes in the process of create-next-app. So at this moment, you have to create also components/nav.js file. Create it with following content.

const Nav = ({settings}) => (
  <header className="top-header util__flex util__container">
    <nav className="top-header__col">
      Navigation
    </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">
      Languagues
    </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>
)

export default Nav


Use the Layout component on the pages

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-Storyblok World!</h1>
      </Layout>
    )
  }
}

At this moment, the website should look similar to the following screenshot. As next step, we will tackle how to create the homepage with a teaser and a feature section.

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

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

Connect Next.js with Storyblok

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 live visual editor/composer of Storyblok.

$ npm install storyblok-js-client storyblok-react --save
# OR
$ yarn add storyblok-js-client storyblok-react

After the installation of the modules, you will need to initialize it with the preview token from your Storyblok space, which we will do in the next step.

Create a Storyblok space and get the preview token

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

Once the space has been created, you'll find your preview token in the dashboard section.

Create a Storyblok service

At first, we will create a StoryblokService class that will initialize the client and provide us a few helpers. Create the utils/storyblok-service.js file in the folder utils. Copy and paste the code below in, and replace the PREVIEW_TOKEN with your preview token from the Storyblok dashboard.

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

In the development phase, it is useful to set the variable this.devMode to true so you can retrieve unpublished content from the Storyblok API without the need to use Storyblok’s preview mode.

Pass the cache version from the server to the client

For the best performance, we recommend you pass the cache version that gets generated on the server-side to the client-side. You will 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 the API requests to Storyblok will be cached in the CDN properly. You can read more about the caching technique in the content delivery documentation.

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 }) {
        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 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.

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>
)

Creating components

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

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 <Placeholder componentName={blok.component}/>
}

export default Component

Create the following React.js components inside the components folder.

Page.js

import Component from './index'
import SbEditable from 'storyblok-react'

const Page = ({body}) => (
  <SbEditable content={body}>
  <main>
    {body.map((blok) =>
      <Component blok={blok} key={blok._uid} />
    )}
  </main>
  </SbEditable>
)

export default Page

Teaser.js

import SbEditable from 'storyblok-react'

const Teaser = ({blok}) => {
  return (
    <SbEditable content={blok}>
      <div className="teaser">
        <h1>{blok.headline}</h1>
      </div>
    </SbEditable>
  )
}

export default Teaser

Grid.js

import Component from './index'
import SbEditable from 'storyblok-react'

const Grid = ({blok}) => (
  <SbEditable content={blok}>
    <div className="util__flex">
      {blok.columns.map((blok) =>
        <Component blok={blok} />
      )}
    </div>
  </SbEditable>
)

export default Grid

Feature.js

import SbEditable from 'storyblok-react'

const Feature = ({blok}) => (
  <SbEditable content={blok}>
    <div className="feature util__flex-eq">
      <h2>{blok.name}</h2>
    </div>
  </SbEditable>
)

export default Feature

Placeholder.js

const Placeholder = ({componentName}) => (
  <div>The component {componentName} has not been created yet.</div>
);

export default Placeholder;Update the homepage code

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

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 {
  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 */}
        <Page body={this.state.pageContent.body} />
      </Layout>
    )
  }
}

The get method of the StoryblokService will load a JSON file that defines which components will be rendered on the homepage. Storyblok initializes the visual editor when the component finishes rendering.

Setup of the visual editor

To preview the website in the Storyblok app, we will need to set the default preview URL in our space. The best way to do it is to open the setting of your space and fill Location (default environment) field with your localhost URL in. If you didn't change any default Next.js setting you local URL should be http://localhost:3000/ .

Settings of Preview URL in Storyblok

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 screen like this one. Next.js app throws error and you should see the demo content in the sidebar created by default in Storyblok space.

First preview of Next.js app in Storyblok

Changing the real path field

To fix the error from Next.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 index page our Next.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.

Setup of the Real Path for homepage

Your visual editor should look like this screen after the reload. If you try to change the order or the content of your components, you should see instant/live change of the content or order without the need to save the changes. Isn't it cool?

Preview of visual editor

Create your first component 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 component/blok in the Storyblok interface.

You can use any image you want, but if you want to use the same image as we - here it is.

After you added the schema and the content to Storyblok following the video above, you need to create components/Slide.js file in your project.

import SbEditable from 'storyblok-react'

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

export default Slide

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 components/Teaser.js with the following.

import React from 'react'
import Component 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.blok.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 { blok } = this.props

    return (
      <SbEditable content={blok}>
        <div className="teaser">
          <Component blok={this.slide()} />
          <div className="teaser__pag">
            {blok.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 a result similar to this one.

Result of adding the slides to the sample

Extending the feature section

The feature component has currently only a title text. Let's extend it with a description text and icon image.

Open the feature component (you can access it via the grid component or through the components section in the app) and then press Define schema button. Add the fields description type of textarea and icon type of image. Save and close schema editor.

Defining the features fields

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

import SbEditable from 'storyblok-react'

const Feature = ({blok}) => {
  const resizedIcon = function(iconImage) {
    if (typeof iconImage !== 'undefined') {
      return iconImage.replace('//img2.storyblok.com/80x80', '//a.storyblok.com')
    }
    return null
  }

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

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

export default Feature

Fill any content in and you should end with homepage similar to the next screenshot. Tip: Feel free to check out this site for free, no-attribution icons!

Sample with filled feature section

Build a 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.

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

Creating english folder

Create a global settings content item

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

Create settings entry

Change the real path of the Settings entry to /.

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

Create and add a block called NavItem for the main_navi with the following schema definition:

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

At first, create Nav Items for Home and Blog page with fakes URLs (Note: Don't worry about true URLs for these items right now -- use any placeholder in the URL for now). Your content in the sidebar should look like the next screenshot.

Structure of the main navi

Replace the code of components/nav.js with the following content.

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

Your localhost should look like the following screenshot.

Updated navigation of sample project

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 content of the setting to the Layout component via the settings prop: <Layout settings={settingsContent}>.

// updated version of pages/index.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 {
  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 settingsContent = this.props.settings.data.story
    const bodyOfPage = this.state.pageContent.body

    return (
      <Layout settings={settingsContent}>
        {/* We will define these settings later on */}
        <Page body={bodyOfPage} />
      </Layout>
    )
  }
}

Access the data in the Nav component

You may not have noticed this earlier, but we pass the settings prop through the components/layout.js component into our nav.js component <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 code.

import Link from 'next/link'

const Nav = ({settings}) => (
  <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 href={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 href="/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>
)

export default Nav

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.

Connected settings with the 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. So let's use the default routing behavior of Next.js and create a correct folder structure in pages folder for our blog.

pages/
--| [language]/
----| blog/
------| index.js
------| [slug].js

Copy and paste the following code into the pages/[language]/blog/[slug].js file.

import { useRouter } from 'next/router'

const Blogpost = () => {
  const router = useRouter()
  const { slug } = router.query

  return <p>Blog post slug : {slug}</p>
}

export default Blogpost

If you will open now following URL http://localhost:3000/en/blog/SlugOfThePost, you should see this result.

Blogpost routing

Define a blog detail page

Let's implement the blog detail page at pages/[language]/blog/[slug].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 // yarn add marked

To make it quicker just replace code in the [slug].js file with the following code.

import React from 'react'
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 settingsContent = this.props.settings.data.story
    const { pageContent } = this.state

    return (
      <Layout settings={settingsContent}>
        <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 a blog overview page

Create pages/[language]/blog/index.js file to show the overview of all blog posts in Storyblok. Add the code below to that file.

import React from 'react'
import Link from 'next/link'
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 settingsContent = this.props.settings.data.story
    const { blogPosts } = this.props

    return (
      <Layout settings={settingsContent}>
        {blogPosts.data.stories.map((blogPost, index) => {
            const { published_at, content: { name, intro }} = blogPost
            return (
              <div key={index} className="blog__overview">
                  <h2>
                    <Link href={'/' + 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>
    )
  }
}

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 it with the per_page parameter and jump to the other pages with the page parameter.

Create content of the blog in Storyblok

We just created components in Next.js with dynamic routing, but at this moment we will get only errors, if we try the paths we defined. We didn't create any content for the blog section of our page.

Navigate into the components section in Storyblok and create a new component name blogpost and don't forget to set the component as the content type component - this enables us to use blogpost as an entry in the content folder of Storyblok.

Creation of blogpost in Storyblok

Define these three fields in the blogpost with the following types.

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

If you did all correctly, you can now create your first blog post. Open the content section of Storyblok and navigation into the en folder. Inside the en folder create a new folder named blog and in this folder create your first blog post.

Creation of the blog folder

Fill any random content for now and you should see result similar to this.

Blogpost with sample content

You should now update your Nav Item for the Blog link in the en/settings. Select Url and add /en/blog as the Nav Item's URL. You can now use the Blog link in the header of your website. After you navigate to en/blog you should see a result similar to the next screenshot.

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.

Overview of blogs

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).

Create a german duplicate

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 href="/de/blog">
    <a className="top-header__link" >German</a>
  </Link>
</li>

Deploy to live

For easy, zero-configuration deployment, you can use now.sh. Download and install now's desktop application and you are able to deploy Next.js with a single command now from the root of your project.

$ now

You will get a unique URL which you can then link via now alias to your custom domain, but this is the topic for another article.

Any feedback?

I hope you manage to read it trough and you liked this tutorial! If you have any questions or difficulties getting the app running, please don't hesitate to contact us using the chat icon in the right corner or write directly to our dev🥑(Samuel Snopko) on twitter. We are here to support you as much as possible.

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
Samuel Snopko

Author

Samuel Snopko

Samuel is the Head of Developer Relations at Storyblok with a passion for JAMStack and beautiful web. Co-creator of DX Meetup Basel and co-organizer of Frontend Meetup Freiburg. Recently fell in love with Vue.js, Nuxt.js, and Storyblok.