Contents

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

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

If you are in hurry you can download the whole project (dev-next.blok.ink) at Github onefriendaday/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 not done yet install NodeJs, NPM and the create-next-app CLI with npm install -g create-next-app.

We will start with initializing project with the Next.js starter template.

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

Next.js starts it’s 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 the time we will also initialize the git repository.

// Initialize git 
git init && git add . && git commit -m 'init'

Build a skeleton

We will start to build the skeleton for your website. At the end, you will have a header, a main and a footer section and some useful global utility CSS classes.

Global CSS in Next.js

Without installing any additional css loader or babel plugin you can add the global attribute inside the jsx style tag <style jsx global> to define global CSS. As we will wrap all pages in a Layout.js component this CSS will be loaded on all pages.

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

components/Layout.js
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 font for the body element. As this is not a system font we need to add it to the head section of our document. There the NextHead component comes into play. Open head.js and add the font stylesheet to the NextHead section.

components/head.js
import NextHead from 'next/head'

const Head = (props) => (
  <NextHead>
    <meta charSet="UTF-8" />
    <title>{props.title || ''}</title>
    <meta name="description" content={props.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 following to wrap the content with the Layout component.

pages/index.js
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 will let you receive content from Storyblok´s API and the storyblok-react module will give you a wrapper component to have editable blocks in the visual editor.

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

After installing the module you need to initialize it with the preview token of your Storyblok space. Signup or Login at app.storyblok.com and create a new space.

Screenshot Storyblok

Create a Storyblok service

The StoryblokService class will initialize the client and provide you with some helpers. Create the folder utils and the file utils/StoryblokService.js. Replace PREVIEW_TOKEN with your preview token from the Storyblok Dashboard.

utils/StoryblokService.js
import StoryblokClient from 'storyblok-js-client'

class StoryblokService {
  constructor() {
    this.devMode = true // Always loads draft
    this.token = 'PREVIEW_TOKEN'
    this.client = new StoryblokClient({
      accessToken: this.token,
      cache: {
        clear: 'auto',
        type: 'memory'
      }
    })

    this.query = {}
  }

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

    if (this.getQuery('_storyblok') || this.devMode) {
      params.version = 'draft'
    }

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

  initEditor() {
    window.storyblok.init({initOnlyOnce: true})
    window.storyblok.on('change', () => location.reload(true))
    window.storyblok.on('published', () => location.reload(true))
  }

  setQuery(query) {
    this.query = query
  }

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

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

const storyblokInstance = new StoryblokService()

export default storyblokInstance

Add the Storyblok Bridge

To build a bridge between Storyblok and your website there needs to be included a 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() inside the div tag in components/Layout.js.

components/Layout.js
...
import StoryblokService from '../utils/StoryblokService'

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

To render the full homepage we will need to create the components that are defined in the JSON of the demo content of the a new space you created in Storyblok. Add the file index.js to the components folder.

components/index.js
import React from 'react'
import Teaser from './Teaser'
import Feature from './Feature'
import Page from './Page'
import Grid from './Grid'
import Slide from './Slide'

const Components = {
  'teaser': Teaser,
  'feature': Feature,
  'page': Page,
  'slide': Slide,
  '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 React components inside the components folder.

Page.js

components/Page.js
import Components from './index'

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

Teaser.js

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

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

Grid.js

components/Grid.js
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

components/Feature.js
import SbEditable from 'storyblok-react'

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

Render the homepage

After we created all components we finally will render the homepage by replacing the content of pages/index.js with following:

pages/index.js
import Components from '../components/index'
import Layout from '../components/Layout'
import StoryblokService from '../utils/StoryblokService'

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

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

  componentDidMount() {
    StoryblokService.initEditor()
  }

  render() {
    return (
      <Layout>
        {Components(this.props.page.data.story.content)}
      </Layout>
    )
  }
}

The get method of the StoryblokService will load a JSON that defines which components we will render on the homepage. When the component has finished rendering we’ll initialize the Editor.

When reloading http://localhost:3000/ you should see following.

Components

Create your first block in Storyblok

We just loaded the demo content of Storyblok and now we will extend the teaser component with interactive slides. To do this start with connecting your environment to the Storyblok composer inserting your development host localhost:3000.

IMPORTANT: After inserting the host you need to change the real path field (see next step) otherwise you get a 404 page.

Storyblok Bridge

Changing the real path field

You now should see your website in the preview. But it will show a not found page because Storyblok by default uses the path /home for the homepage. To change that you will need to go to the advanced settings and put a slash to the real path field.

Storyblok Real Path

So let’s define the schema of a new slide block/component

Follow this video which explains how to create a new block.

After adding the schema and the content to Storyblok we will need to add the slide Next.js component to the project. Create components/Slide.js with the following content.

components/Slide.js
import SbEditable from 'storyblok-react'

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

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

components/index.js
...
import Slide from './Slide'

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

...

Now it’s time to implement the logic to show the slides in the Teaser.js component. You can use any React.js slider plugin for a more advanced slider but let’s keep it simple for now.

components/Teaser.js
import Components from './index'
import SbEditable from 'storyblok-react'
import React from '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() {
    return (
      <SbEditable content={this.props.content}>
        <div className="teaser">
          {this.slide() ? Components(this.slide()) : ''}

          <div className="teaser__pag">
            {this.props.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 have the following result.

Storyblok Teaser Slides

Extending the feature section

The features section currently has only a title. We now will extend the feature block with a description text and icons.

Click on the feature block and add the fields description (with type textarea) and icon (with type image) by clicking on “Define Schema”.

Define Schema in Storyblok

Open the feature component (components/Feature.js) and extend it with the new fields as well as some basic CSS stylings.

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

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

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

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

After you have filled in some content you should have a fully editable homepage.

Img

Build a navigation menu

To build a dynamic navigation menu you have several possibilities. One is to create a global content item that contains the global configurations. Another method is to use the Links API to generate the navigation automatically from your content tree. We will implement the first method in this tutorial.

As we are creating a multilanguage website we create a global configuration for each language. Let’s start with creating a folder for English en.

Img

Create a global settings content item

Inside the folder en we create a content item called Settings with the new content type settings. This will be the content item where we put navigation items and other global configurations of our website.

Settings

Change the real path to / and create the schema for the main navi defining the key main_navi with the type Blocks.

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

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

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

After inserting some demo content you should have following structure in the sidebar:

Storyblok

Extending the properties in getInitialProps

As the Storyblok 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 are passing the settings to the layout component with the property settings <Layout settings={this.props.settings.data.story}>.

pages/index.js
import Components from '../components/index'
import Layout from '../components/Layout'
import StoryblokService from '../utils/StoryblokService'

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

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

  componentDidMount() {
    StoryblokService.initEditor()
  }

  render() {
    return (
      <Layout settings={this.props.settings.data.story}>
        {Components(this.props.page.data.story.content)}
      </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 multi language pages and the blog.

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

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('home', '/home')

Next.js allows extending the routes by defining a custom nodejs server. Let’s create the file server.js and include our routes there.

server.js
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

After we pass the settings from the Layout component to the Nav component with <Nav settings={settings} /> we can now easily access the navigation items with this.props.settings.content.main_navi and loop over them. Replace the content of components/nav.js with following.

components/nav.js
import {Link} from '../routes'

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

  render() {
    return (
      <header className="top-header util__flex util__container">
        <nav className="top-header__col">
          <ul className="top-header__nav">
            {this.props.settings && this.props.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>
            <li>
              <Link route="/de/blog"><a className="top-header__link" >German</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.

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

Our final URL should look like /:language/blog/:slug so we will need to create following folder structure.

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

Add a blog detail page

We start with 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 using marked as a parser.

So first we will need to install the markdown parser.

$ npm install marked --save

Then we will create the file pages/blog/detail.js for the dynamic route of the blog posts.

pages/blog/detail.js
import {Link} from '../../routes'
import Components from '../../components/index'
import Layout from '../../components/Layout'
import StoryblokService from '../../utils/StoryblokService'
import SbEditable from 'storyblok-react'
import marked from 'marked'

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

    return {
      page: await StoryblokService.get(`cdn/stories${asPath}`),
      settings: await StoryblokService.get(`cdn/stories/${query.language}/settings`)
    }
  }

  componentDidMount() {
    StoryblokService.initEditor()
  }

  body() {
    let rawMarkup = marked(this.props.page.data.story.content.body)
    return { __html:  rawMarkup}
  }

  render() {
    return (
      <Layout settings={this.props.settings.data.story}>
        <SbEditable content={this.props.page.data.story.content}>
          <div className="blog">
            <h1>{this.props.page.data.story.content.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 list the blog posts we will create a route on /:language/blog simply by saving the file index.js into the blog folder.

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.

pages/blog/index.js
import {Link} from '../../routes'
import Components from '../../components/index'
import Head from '../../components/Head'
import Layout from '../../components/Layout'
import StoryblokService from '../../utils/StoryblokService'

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

    return {
      blogPosts: await StoryblokService.get('cdn/stories', {
        starts_with: `${query.language}/blog`
      }),
      settings: await StoryblokService.get(`cdn/stories/${query.language}/settings`)
    }
  }

  componentDidMount() {
    StoryblokService.initEditor()
  }

  render() {
    return (
      <Layout settings={this.props.settings.data.story}>
        {this.props.blogPosts.data.stories.map((blogPost, index) => 
          <div key={index} className="blog__overview">
            <h2>
              <Link route={'/' + blogPost.full_slug}>
                <a className="blog__detail-link">
                  {blogPost.content.name}
                </a>
              </Link>
            </h2>
            <small>
              {blogPost.published_at}
            </small>
            <p>
              {blogPost.content.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 Vue.js components for showing the blog we need to create a new folder in Storyblok to create the blog pages.

Create the folder en/blog and choose blog as default content type of this folder.

Blog

Create the blog article

When you go inside the blog folder and create a new content item it will now automatically choose blog as content type. Add the schema fields intro (Textarea), name (Text) and body (Markdown) and create some demo content.

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

Blog

In the overview you should see the list of blog articles.

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. Define the language name and then begin translating your content.

Adding language in Storyblok

Deploy to live

Now 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 their 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 there should already be 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 following listeners.

components/head.js
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.

I hope you liked the tutorial and if you have any difficulties to get the app running don’t hesitate to contact me using the chat icon on the right corner.

ResourceLink
Github repository of this Tutorialgithub.com/onefriendaday/nextjs-multilanguage-website
The project livedev-next.blok.ink
NowNow
Next.jsNext.js
React.jsReact.js
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.