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 Vercel, using a Storyblok API for the multilanguage content.

If you are in a hurry you can download the whole source code of the project from GitHub https://github.com/storyblok/nextjs-multilanguage-website or jump into one of the following chapters.

  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

warn:

The project in this article was developed using the following versions of these technologies:

  • Next.js v10.0.7
  • Nodejs v12.3.1
  • Npm v6.9.0

Keep in mind that these versions may be slightly behind the latest ones.

Initialization of the Next.js Website

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.

yarn create next-app my-next-website // npm init next-app my-next-website 
cd my-next-website
yarn dev // npm run dev

Next.js will start your local development environment at http://localhost:3000 and you should see the introduction screen of the Next.js app similar to the following picture.

Welcome screen of Next.js

Building a Skelton

We are going to use TailwindCSS to style the website in this project. Install tailwindcss and postcss-preset-env packages.

Bash

yarn add tailwindcss postcss-preset-env -D

Create postcss.config.js file with the following content.

./postcss.config.js

module.exports = {
  plugins: [
    'tailwindcss',
    'postcss-preset-env'
  ],
}

Create tailwind.config.js file with the following content.

./tailwind.config.js

module.exports = {
  purge: [],
  theme: {
    fontFamily: {
      'sans': 'Roboto, Arial, sans-serif',
      'serif': 'Merriweather, Georgia, serif'
    },
    extend: {
    },
  },
  variants: {},
  plugins: [],
}

You may have noticed that we are using special fonts. Download the fonts from fonts.google.com and add them to the public folder. You will need the following fonts - Merriweather-Bold, Roboto-Medium & Roboto-Regular. After downloading add them in the folder ./public/fonts in your project.

Next we will have to create a file named fonts.css in the fonts folder.

./public/fonts/fonts.css

@font-face {
  font-family: 'Merriweather';
  font-weight: 700;
  font-style: normal;
  src: url('Merriweather-Bold.ttf');
}

@font-face {
  font-family: 'Roboto';
  font-weight: 400;
  font-style: normal;
  src: url('Roboto-Regular.ttf');
}

@font-face {
  font-family: 'Roboto';
  font-weight: 500;
  font-style: normal;
  src: url('Roboto-Medium.ttf');
}

Create tailwind.css file in the styles folder with the following imports.

./styles/tailwind.css

@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

Finally, edit the _app.js file and remove the import of the globals.css and import the tailwind.css instead.

./pages/_app.js

import '../styles/tailwind.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

You should see a change of fonts at http://localhost:3000/ after these changes.

hint:

To configure Purge CSS to remove unused parts of tailwindCSS check this medium article.

Defining the Layout

Next we will create a folder named components for all of our components. As the first component we create the file components/Layout.js with the following content in our newly created components folder.

./components/Layout.js

import Head from '../components/Head'
import Navigation from '../components/Navigation'
import Footer from '../components/Footer'

const Layout = ({ children }) => (
  <div className="bg-gray-300">
    <Head />
    <Navigation />
    {children}
    <Footer />
  </div>
)

export default Layout

Creating Header & Footer

In Layout.js you can see that we have to create also Header (Navigation) and Footer for our website. We will keep them as dummy component for now and configure them with links later in this guide. Create Head.js, Navigation.js and Footer.js in the components folder.

./components/Head.js

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" />
  </NextHead>
)

export default Head

./components/Navigation.js

const Navigation = ({settings}) => (
  <header className="w-full bg-white">
    <nav className="" role="navigation">
      <div className="container mx-auto p-4 flex flex-wrap items-center md:flex-no-wrap">
        <div className="mr-4 md:mr-8">
          <a href="/">
            <svg width="69" height="66" xmlns="http://www.w3.org/2000/svg"><g fill="none" fillRule="evenodd"><path fill="#FFF" d="M-149-98h1440v938H-149z"/><path d="M37.555 66c17.765 0 27.051-16.38 30.24-33.415C70.986 15.549 52.892 4.373 35.632.52 18.37-3.332 0 14.876 0 32.585 0 50.293 19.791 66 37.555 66z" fill="#000"/><path d="M46.366 42.146a5.55 5.55 0 01-1.948 2.043c-.86.557-1.811 1.068-2.898 1.3-1.087.279-2.265.511-3.487.511H22V20h18.207c.905 0 1.675.186 2.4.604a6.27 6.27 0 011.811 1.485 7.074 7.074 0 011.54 4.504c0 1.207-.317 2.368-.905 3.482a5.713 5.713 0 01-2.718 2.507c1.45.418 2.582 1.16 3.442 2.229.815 1.114 1.223 2.553 1.223 4.364 0 1.16-.226 2.136-.68 2.971h.046z" fill="#FFF"/></g></svg>
          </a>
        </div>
        <div className="text-black">
          <p className="text-lg">Storyblok</p>
          <p>NextJS Demo</p>
        </div>
        <div className="ml-auto md:hidden">
          <button
            className="flex items-center px-3 py-2 border rounded"
            type="button"
          >
            <svg
              className="h-3 w-3"
              viewBox="0 0 20 20"
              xmlns="http://www.w3.org/2000/svg"
            >
              <title>Menu</title>
              <path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z" />
            </svg>
          </button>
        </div>
        <div className="w-full md:w-auto md:flex-grow md:flex md:items-center">
          <ul className="flex flex-col mt-4 -mx-4 pt-4 border-t md:flex-row md:items-center md:mx-0 md:mt-0 md:pt-0 md:mr-4 md:ml-auto lg:mr-8 md:border-0">
            <li>
              <a href="/" className="block px-4 py-1 md:p-2 lg:px-8">Home</a>
            </li>
            <li>
              <a href="/" className="block px-4 py-1 md:p-2 lg:px-8">About</a>
            </li>
          </ul>
          <ul className="flex flex-col mt-4 -mx-4 pt-4 border-t md:flex-row md:items-center md:mx-0 md:mt-0 md:pt-0 md:border-0">
            <li>
              <a href="" className="block px-4 py-1 md:p-2 rounded-lg lg:px-4 bg-black text-white">EN</a>
            </li>
            <li>
              <a href="/de" className="block px-4 py-1 md:p-2 rounded-lg lg:px-4">DE</a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </header>
)

export default Navigation

./components/Footer.js

const Footer = () => {
  return (
    <footer className="text-center flex flex-col items-center py-20 container mx-auto">
      <p>Next.js Demo Website created with Storyblok</p>
      <div className="flex items-center my-8">
        <img
          src="https://a.storyblok.com/f/51376/3856x824/fea44d52a9/colored-full.png"
          alt="Storyblok Logo"
          className="w-48 m-4"
        />
        <svg className="w-32 m-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 207 124"><defs/><g fill="#000" fillRule="nonzero"><path d="M48.942 32.632h38.96v3.082H52.512v23.193h33.278v3.082H52.513v25.464h35.794v3.081H48.942V32.632zm42.45 0h4.139l18.343 25.464 18.749-25.464L158.124.287l-41.896 60.485 21.59 29.762h-4.302l-19.642-27.086L94.15 90.534h-4.22l21.751-29.762-20.29-28.14zm47.967 3.082v-3.082h44.397v3.082h-20.453v54.82h-3.571v-54.82h-20.373zM.203 32.632h4.464l61.557 91.671-25.439-33.769L3.936 37.011l-.162 53.523H.203zM183.397 86.523c.738 0 1.276-.563 1.276-1.29 0-.727-.538-1.29-1.276-1.29-.73 0-1.277.563-1.277 1.29 0 .727.547 1.29 1.277 1.29zm3.509-3.393c0 2.146 1.555 3.549 3.822 3.549 2.414 0 3.874-1.446 3.874-3.956v-8.837h-1.946v8.828c0 1.394-.704 2.138-1.946 2.138-1.112 0-1.867-.692-1.893-1.722h-1.911zm10.24-.113c.14 2.233 2.007 3.662 4.787 3.662 2.97 0 4.83-1.498 4.83-3.887 0-1.878-1.06-2.917-3.632-3.514l-1.38-.338c-1.634-.38-2.294-.891-2.294-1.783 0-1.125 1.025-1.86 2.563-1.86 1.459 0 2.466.718 2.649 1.869h1.893c-.113-2.103-1.971-3.583-4.516-3.583-2.737 0-4.56 1.48-4.56 3.704 0 1.835 1.033 2.926 3.3 3.454l1.616.39c1.659.389 2.388.96 2.388 1.912 0 1.108-1.146 1.913-2.71 1.913-1.676 0-2.84-.753-3.005-1.939h-1.928z"/></g></svg>
      </div>
      <p className="underline">
        <a href="https://www.storyblok.com/tp/next-js-react-guide">View tutorial on Storyblok</a>
      </p>
    </footer>
  )
}

export default Footer

Next we need to replace the default welcome HTML code in the index.js file. Replace it with the following code:

./pages/index.js

import Layout from '../components/Layout'

export default function Home() {
  return (
    <Layout>
      <div className="container mx-auto p-4 text-center">The content from Storyblok will follow soon...</div>
    </Layout>
  )
}

If you did everything correct you should see a result similar to the following picture.

Preview with sample header and footer

Creating the Content Structure

Up to this point, we only used hard-coded mockup data in our Next.js project. Before we connect Storyblok with Next.js and render our first data onto the screen, we have to define the structure of our data in Storyblok. A good thing about this process is that we don't need to set up the connection right away so you can already prepare the data before you choose the front-end framework.

hint:

To better understand the content structure, we strongly recommend reading the Structures of Content chapter of the developer guide.

First signup or login at app.storyblok.com and create a new space. You can name it however you like. The first screen you see should be similar to the following image:

Initial content section in Storyblok UI

Above you can see that once you created a new Space in Storyblok it will ship with default content. We will use this content and the created components in our project. For your own project feel free to change, rename, or even delete those components and create your own. If you navigate to Components in the main navigation you will see a list the default components (page. teaser, feature, grid). These components are ready to be used to create new Stories in Storyblok.

Storyblok initial look of components section

By default Storyblok ships with a component called Page. The Page component has a checkmark in the Content Type column. The checkmark there tells us that the Page component can be used to create new Stories from. You would not be able to create new Stories from other components such as grid, feature, and teaser as they can only be used inside a Field of the type blocks that lives inside a Content Type. That way you can create an almost infinite number of combinations with your components, with multiple levels of nesting.

hint:

Read more about components and the difference between Content Types and Bloks (nestable components) in an essential part of the developer guide.

Now that we already have components defined in Storyblok what is missing is the implementation of these components defined in the Next.js project. Let's create them in our components folder using the following source code.

Teaser.js

./components/Teaser.js

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

const Teaser = ({blok}) => {
  return (
    <SbEditable content={blok}>
      <div className="py-10">
        <h2 className="font-serif text-3xl text-center">{blok.headline}</h2>
      </div>
    </SbEditable>
  )
}

export default Teaser

Feature.js

./components/Feature.js

import SbEditable from 'storyblok-react'

const Feature = ({blok}) => {
  return (
    <SbEditable content={blok}>
      <div className="text-center">
        <h2 className="text-xl font-medium">{blok.name}</h2>
      </div>
    </SbEditable>
  )
}

export default Feature

Grid.js

./components/Grid.js

import DynamicComponent from './DynamicComponent'
import SbEditable from 'storyblok-react'

const Grid = ({blok}) => (
  <SbEditable content={blok}>
    <ul className="flex py-8 mb-6">
      {blok.columns.map((nestedBlok) => (
          <li key={nestedBlok._uid} className="flex-auto px-6">
            <DynamicComponent blok={nestedBlok} />
          </li>
        )
      )}
    </ul>
  </SbEditable>
)

export default Grid

Page.js

./components/Page.js

import DynamicComponent from './DynamicComponent'
import SbEditable from 'storyblok-react'

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

export default Page

Explanation of blok Prop

You probably noticed the blok prop in all of the components created by above. The prop is used to pass data down into each of the components. There are various solutions for this challenge and you don't have to call this prop blok as we did. Keep in mind that you need to pass the data down to the nested components to render them.

hint:

Want to learn more about dynamic component rendering in React.js? We’ve prepared an article for you on that.

Explanation of DynamicComponent.js

You might have noticed a DynamicComponent.js in the code of the previous components (Grid.js, Page.js). We are using this component to decide which component should be rendered on the screen depending on the component name it has in Storyblok. In the source of DynamicComponent.js you can see that all the possible components are getting imported and will get rendered according to the the value of blok.component. Create the DynamicComponent.js using the following code.

./components/DynamicComponent.js

import Teaser from './Teaser'
import Feature from './Feature'
import Grid from './Grid'
import Placeholder from './Placeholder'

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

const DynamicComponent = ({blok}) => {
  if (typeof Components[blok.component] !== 'undefined') {
    const Component = Components[blok.component]
    return <Component blok={blok} />
  }
  return <Placeholder componentName={blok.component}/>
}

export default DynamicComponent

In the Placeholder.js component contained in the DynamicComponent.js, which is used to render a placeholder in case of unknown components is coming from the Storyblok API. Don't forget to create it.

./components/Placeholder.js

const Placeholder = ({componentName}) => (
  <div className="py-4 border border-red-200 bg-red-100">
    <p className="text-red-700 italic text-center">The component <strong>{componentName}</strong> has not been created yet.</p>
  </div>
);

export default Placeholder;
COMMIT:

You can check the current progress in this GitHub commit created default sb components.

Connecting Storyblok with Next.js

So far we have created the skeleton of our website and prepared the default components in Next.js. Before we start with the design of the pages, we need to set up the connection between Next.js and Storyblok to get the data for the components. To do that, we can use the storyblok-js-client package maintained by the Storyblok. Install it together with the following packages (storyblok-react, axios) as shown in the following example:

Bash

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

Storyblok-js-client will use axios under the hood to get the data from the Storyblok Content Delivery API and storyblok-react will bring a special component, which allows us to listen to events from the Storyblok app to make use of the real-time editing features of Storyblok Visual Editor. After you've installed the packages we have to configure the environment and add the Storyblok Access Token.

Create storyblok-service.js in the utils folder, which will be responsible for the communication with Storyblok. Replace <Paste Space Access Token (API-key)> with the preview token from your space.

./utils/storyblok-service.js

import StoryblokClient from 'storyblok-js-client'

class StoryblokService {
  constructor() {
    this.devMode = true // Always loads draft
    this.token = '<Paste Space Access Token (API-key)>'
    this.client = new StoryblokClient({
      accessToken: this.token,
      cache: {
        clear: 'auto',
        type: 'memory'
      }
    })

    this.query = {}
  }

  getCacheVersion() {
    return this.client.cacheVersion
  }

  // ask Storyblok's Content API for content of story
  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)
  }

  // initialize the connection between Storyblok & Next.js in Visual Editor
  initEditor(reactComponent) {
    if (window.storyblok) {
      window.storyblok.init()

      // reload on Next.js page on save or publish event in Storyblok Visual Editor
      window.storyblok.on(['change', 'published'], () => location.reload(true))

      // Update state.story on input in Visual Editor
      // this will alter the state and replaces the current story with a current raw story object and resolve relations
      window.storyblok.on('input', (event) => {
        if (event.story.content._uid === reactComponent.state.story.content._uid) {
          event.story.content = window.storyblok.addComments(event.story.content, event.story.id)
          window.storyblok.resolveRelations(event.story, ['featured-articles.articles'], () => {
            reactComponent.setState({
              story: event.story
            })
          })
        }
      })
    }
  }

  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

You can find all tokens of your space in the Settings navigation item under the API-Key (1) tab. Check the following image if you struggle to find the preview access token.

How to get preview access token of space
HINT:

You should save sensitive data and your API tokens,even through the Storyblok Content Delivery API token is read only, in .env and not directly in the storyblok-service.js file as we did.

Requesting the First Data

WARN:

This tutorial is still using getInitialProps to request data on the server, if you want to use getStaticProps (which is the recommended way by Next.js) have a look at our 5 minute Next.js tutorial.

Let's use the storyblok-sevice.js in the index.js and load the first Story from Storyblok using the slug home to retrieve the homepages' content. Update index.js with the following code.

./pages/index.js

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 = {
      story: props.res.data.story
    }
  }

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

    let res = await StoryblokService.get('cdn/stories/home', {})

    return {
      res
    }
  }

  componentDidMount() {
    StoryblokService.initEditor(this)
  }

  render() {
    const contentOfStory = this.state.story.content

    return (
      <Layout>
        <Page content={contentOfStory} />
      </Layout>
    )
  }
}

In the Layout.js we added the storyblok-bridge initialization to enable the real-time editing of the Storyblok Visual Editor so make sure to update the file as shown below.

./components/Layout.js

import Head from '../components/Head'
import Navigation from '../components/Navigation'
import Footer from '../components/Footer'
import StoryblokService from '../utils/storyblok-service'

const Layout = ({ children }) => (
  <div className="bg-gray-300">
    <Head />
    <Navigation />
    {children}
    <Footer />
    {StoryblokService.bridge()}
  </div>
)

export default Layout

After running yarn dev in the terminal you already should be greeted with a screen as shown below on your own local environment (localhost:3000). Reach out to us on Discord if you have any problems with the tutorial, we're more than happy to give you a helping hand.

Preview of default components on localhost:3000
COMMIT:

You can check the current progress in this GitHub commit created connection to storyblok.

Setting Up a Real-Time Visual Editor

Our website is running on the localhost:3000 and there we can preview the content of the Home story. The better way to preview your content is by using the real-time preview in Visual Editor, where you and your editors can see the changes instantly in real-time.

If you open the Home Story in the Content area of Storyblok you will see that the Visual Editor is not yet configured.

Open the home story in Storyblok.

You may edit your content, or even save it and publish it. You will still need to go to another browser tab and open the localhost:3000 to see the result, so let's change it.

First time opened Visual Editor.

Navigate into the Settings (1) of your Space and click on the General (2) tab to change the value of the Location (3) (default environment) to http://localhost:3000/. You can also create a special preview URL for different environments like the preview from Vercel, Netlify, or other hosting platforms. This can be done by clicking on "Add preview url" (4) button on the screen.

Setup of the default location for Visual Editor/Preview.

If you open your Home Story using the Visual Editor right now, you will see a Next.js error message. This is good and it means that we connected Next.js with Storyblok. Next.js tells us that we are trying to reach the non-existent URL of http://localhost:3000/home as the slug of the Home (1) story is home.

Let's fix this by overriding the Real Path (3) property of the Home (1) story to / as this Story represents the homepage (index.js) and shouldn't have any slug. We cannot remove the slug in Storyblok as our Content Delivery API needs an identifier, in our case the slug of this Story which can not be empty.

Setup of the real path.

On the top right corner we will hit Save so we don't forget to save the changes that we just did. After that you should see the update of the preview. With that your real-time preview is up and running. Try to edit the content of the story and see the instant change in the window on the left side.

Next.js website preview in Visual Editor of Storyblok.

Understanding the Structure of Components

Since we already have a working Visual Editor with a preview function, we can click on different highlighted areas. The content editor will open the clicked components in the right-hand side of the Visual Editor. You can also open and collapse these components (bloks) in the right-hand side editor without using the preview.

You can see the opened Grid component and its content in the next image. The Grid in this case consists only of an array of other bloks (nested components) and in this particular case is made up of three Feature components. Feel free to add more or remove one of them.

Understanding the components.

If you inspect the source code using the React.js devtools you will see the same structure of components that we created in Storyblok since our Next.js set-up uses the DynamicComponent.js we've created to include components depending on its name in Storyblok (Grid, Feature, ...).

Source code of website in devtools.

Extending the Schema of Bloks in Storyblok

We have loaded the demo content of Storyblok and set up the real-time visual editor. In this section we will extend existing bloks (nested components) with new fields and adjust the templates in Next.js.

Update of the Teaser

First, we will add an image field to the Teaser component. For that, navigate to the Components section in the Storyblok UI and click on the Teaser component. Create a new field named image in the opened overlay.

Create Component in Storyblok

Click on the created image field and define its type to be Asset and as field-type and allow only Images. Save the changes.

Asset Field

We have to update our Teaser component ./components/Teaser.js with the following code to render the image.

./components/Teaser.js

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

const Teaser = ({blok}) => {
  return (
    <SbEditable content={blok}>
      <div className="bg-white-half">
        <div className="pb-6 pt-16 container mx-auto">
          <h2 className="text-6xl font-bold font-serif text-primary">{blok.headline}</h2>
          <img src={blok.image.filename} alt={blok.image.alt} className="w-full" />
        </div>
      </div>
    </SbEditable>
  )
}

export default Teaser

After adding the field and uploading an image you will see that the image is already being shown if you changed the content of the Teaser.js.

Teaser Edit
COMMIT:

You can check the current progress in this Github commit added teaser image.

Extending the Feature Component

Every feature component currently only has one field: title. Let's extend the Feature blok by including a description text field and an icon field.

Open up the schema of the component directly from the Visual Editor. Click on one of the Features components and in the right-hand corner you should see the Define Schema button. Click on it and the Schema overlay will open. Changes done to a component here will effect all components with the same name it is basically a quick access the the Components main navigation item.

Feature Edit

Now we need to define two new fields in the Feature component. Add a field named description of the type textarea, and a field named icon of the type single-select.

Feature

For the icons we will need to create some new Icon Components. First we will create a DynamicIcon.js file in a new components/icons folder. This will resolve all our Icons later to the Single Select option.

./components/icons/DynamicIcon.js

import React from 'react'
import Twitter from './Twitter'

const Components = {
  'twitter': Twitter,
}

const DynamicIcon = ({ type }) => {
  if (typeof Components[type] !== 'undefined') {
    const Component = Components[type]
    return <Component />
  }
  return null
}

export default DynamicIcon

Then we need the actual icon, which is just a function that returns our SVG icon. You can make use of svg2Jsx to create the code for you. We're going to use three icons where one (components/icons/Twitter.js) can be copied from below. You can find the code for the other two icons in the commit.

./components/icons/Twitter.js

import React from "react";

function Icon() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" width="63" height="61">
      <g fill="none" fillRule="evenodd">
        <path fill="#FFF" d="M-212-65h1340v372H-212z"></path>
        <path
          fill="#672E9B"
          d="M34.71 61c16.419 0 25.002-15.139 27.95-30.884C65.607 14.371 48.884 4.042 32.93.481 16.98-3.079 0 13.75 0 30.116S18.292 61 34.71 61z"
        ></path>
        <path
          stroke="#FFF"
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth="2"
          d="M45 20.012a12.935 12.935 0 01-3.71 1.79 5.33 5.33 0 00-5.884-1.457A5.244 5.244 0 0032 25.314v1.17a12.639 12.639 0 01-10.636-5.302s-4.728 10.533 5.909 15.214A13.855 13.855 0 0119 38.737c10.636 5.852 23.636 0 23.636-13.459a5.217 5.217 0 00-.094-.97A9.013 9.013 0 0045 20.011z"
        ></path>
      </g>
    </svg>
  );
}

export default Icon;

Open the Feature component in components/Feature.js and extend it with the new fields as well.

./components/Feature.js

import React from 'react'
import SbEditable from 'storyblok-react'
import DynamicIcon from './icons/DynamicIcon'

const Feature = ({blok}) => {
  return (
    <SbEditable content={blok} key={blok._uid}>
      <div className="py-16 max-w-sm p-2 sm:p-10 text-center flex flex-col items-center">
            <DynamicIcon type={blok.icon} />
            <div className="px-6 py-4">
                <div className="font-bold text-xl my-4">{blok.name}</div>
                <p className="text-base text-gray-600">
                  {blok.description}
                </p>
            </div>
        </div>
    </SbEditable>
  )
}

export default Feature

Finally, add new content to your features and you should see results similar to the following image:

Icon Added
COMMIT:

You can check the current progress in this Github commit added icons & description to feature.

Creating a Blog Section

A common task when creating a website is to develop an overview page of collections like News, Reviews, Posts or Products. In our example, we will create a simple blog with blog posts.

Creating the Post Content Type in Storyblok

To create new blog posts, we will go over to Content and create a new folder called blog with a default content type of Post by making use of Storyblok blueprints.

Create blog

If we take a look at the Content Type after we created it, we can see that it has a couple of different fields: title, image, intro, long_text and author.

Post Content Type

Creating the Post Component in Next

Now we need to create the component for our blog posts. Let's create a BlogPost.js file.

components/BlogPost.js

import React from "react"
import SbEditable from "storyblok-react"
import { render } from "storyblok-rich-text-react-renderer"

const BlogPost = ({ blok }) => {
  return (
    <SbEditable content={blok} key={blok._uid}>
      <div className="bg-white-half w-full">
        <div className="max-w-3xl mx-auto text-center pt-20 flex flex-col items-center">
          <h1 className="text-5xl font-bold font-serif text-primary tracking-wide">
            {blok.title}
          </h1>
          <p className="text-gray-500 text-lg max-w-lg">{blok.intro}</p>
          <img className="w-full bg-gray-300 my-16" src={blok.image} />
        </div>
      </div>
      <div className="max-w-3xl mx-auto text-center pt-20 flex flex-col items-center">
        <div className="leading-relaxed text-xl text-left text-gray-800 drop-cap">
          {render(blok.long_text)}
        </div>
      </div>
    </SbEditable>
  )
}

export default BlogPost

As you can see here, we used storyblok-rich-text-react-renderer to render our Richtext content. To make this component work correctly we need to install the package from npm.

yarn add storyblok-rich-text-react-renderer

Let's move on to creating our first post in Storyblok. Go to Content (1), navigate into the blog folder (2) and create a new Entry (3) with a distinct Name (4) for this Story.

Post Entry

If we open our new post, we will see that nothing is rendered yet (1), because we haven't created that route in Next.js yet. You can go ahead and add some content to our first post (2). In the bottom you will find the Richtext field (3), which we rendered with our storyblok storyblok-rich-text-react-renderer.

Post Entry

Creating the blog route in Next

Now the last thing that is missing is our blog route. We can do that with Next.js dynamic route feature. First we have to create a new folder in Pages called blog. Then we have to create a new file called pages/blog/[slug].js. Here we will load our BlogPost component and display it depending on what route was opened with the correct content.

pages/blog/[slug].js

import React from 'react'
import Layout from '../../../components/Layout'
import BlogPost from '../../../components/BlogPost'
import StoryblokService from '../../../utils/storyblok-service'

export default class extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
          story: props.res.data.story,
          language: props.language,
        }
      }
    
      static async getInitialProps({ asPath, query }) {
        StoryblokService.setQuery(query)
    
        let language = query.language || "en"
        let trimDefault = asPath.replace("/en/blog", "/blog")
        let res = await StoryblokService.get(`cdn/stories${trimDefault}`)
    
        return {
          res,
          language,
        }
      }
    

  componentDidMount() {
    StoryblokService.initEditor(this)
  }

  render() {
    const contentOfStory = this.state.story.content

    return (
      <Layout language={this.state.language}>
        <BlogPost blok={contentOfStory} />
      </Layout>
    )
  }
}

Now if you open the first post entry, it should look similar to the image below.

Next JS Post
COMMIT:

You can check the current progress in this GitHub commit added blog posts and routes.

Showing Featured Articles on the Homepage

At this point in our project, we are missing any listing showing the articles. To do this, we'll create a Featured Articles section on the homepage.

Preparing Storyblok's Structure

We will create a new blok (nested component) called featured-posts containing the field named title of the type Text and the field named posts of the type Multi-option. Set the Source of posts field to Stories and in the field named Path to folder of stories write blog/.

Schema of featured-posts:

- title (type: Text)
- posts (type: Multi-option) (source: Stories), (restrict to: Post)

Featured Posts

Adding the Next.js template

Now that we have our content type, let's go ahead and add a component FeaturedPosts.js in Next.js.

components/FeaturedPosts.js

import React from "react";
import SbEditable from "storyblok-react";

const FeaturedPosts = ({ blok }) => {
  return (
    <SbEditable content={blok} key={blok._uid}>
      <div className="py-8 mb-6 container mx-auto text-left" key={blok._uid}>
        <div className="relative">
          <h2 className="relative font-serif text-4xl z-10 text-primary">
            {blok.title}
          </h2>
          <div className="absolute top-0 w-64 h-10 mt-6 -ml-4 bg-yellow-300 opacity-50" />
        </div>
        <ul className="flex">
          {blok.posts.map((post) => {
            const lang = post.lang === "default" ? "en" : post.lang;

            return (
              <li key={post.content._uid} className="pr-8 w-1/3">
                <a
                  href={`/${lang}/blog/${post.slug}`}
                  className="py-16 block transition hover:opacity-50"
                >
                  <img src={post.content.image} className="pb-10 w-full" />
                  <h2 className="pb-6 text-lg font-bold">
                    {post.content.title}
                  </h2>
                  <p className="pb-6 text-gray-700 leading-loose">
                    {post.content.intro}
                  </p>
                </a>
              </li>
            );
          })}
        </ul>
      </div>
    </SbEditable>
  );
};

export default FeaturedPosts;

We also have to add this component to our DynamicComponents.js file.

import Teaser from './Teaser'
import Feature from './Feature'
import FeaturedPosts from './FeaturedPosts'
import Grid from './Grid'
import Placeholder from './Placeholder'

const Components = {
  'teaser': Teaser,
  'grid': Grid,
  'feature': Feature,
  'featured-posts': FeaturedPosts
}

const DynamicComponent = ({blok}) => {
  if (typeof Components[blok.component] !== 'undefined') {
    const Component = Components[blok.component]
    return <Component blok={blok} />
  }
  return <Placeholder componentName={blok.component}/>
}

export default DynamicComponent

Now the last step is to load the associated blog posts in our Home entry. We will do this by adapting our request with the Storyblok Service in pages/index.js. You will need to add a resolve_relations to the request.

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 = {
      story: props.res.data.story
    }
  }

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

    let res = await StoryblokService.get('cdn/stories/home',
    {
      "resolve_relations": "featured-posts.posts"
    })

    return {
      res
    }
  }

  componentDidMount() {
    StoryblokService.initEditor(this)
  }

  render() {
    const contentOfStory = this.state.story.content

    return (
      <Layout>
        <Page content={contentOfStory} />
      </Layout>
    )
  }
}


With the new content type and the component in Next.js set up, we will go back to the Home entry and create a new block of the type featured-posts .

Storyblok editing capabilities Storyblok editing capabilities

Then we will go ahead and add our first entry as a featured post to this blok. If all went correctly you should see the featured post on the bottom of the home page.

Featured Post
COMMIT:

You can check the current progress in this Github commit added featured post to home page.

Creating a Post Overview Page

Let's quickly create an overview page of our articles before we implement another language. Create index.js on the path ./pages/blog/index.js with the following source code. This will load all the stories inside the blog folder (every Story that starts with blog/) and display a list of the blog entries.

./pages/blog/index.js

import Layout from "../../components/Layout";
import StoryblokService from "../../utils/storyblok-service";

export default class extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      stories: props.res.data.stories,
    };
  }

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

    const res = await StoryblokService.get("cdn/stories", {
      starts_with: "blog/",
    });

    return {
      res,
    };
  }

  componentDidMount() {
    StoryblokService.initEditor(this);
  }

  render() {
    const posts = this.state.stories;

    return (
      <Layout>
        <main className="container mx-auto">
          <h1 className="text-5xl font-bold font-serif text-primary tracking-wide pt-12">
            All Posts
          </h1>

          <ul>
            {posts.map((post) => (
              <li className="max-w-4xl px-10 my-4 py-6 rounded-lg shadow-md bg-white">
                <div className="flex justify-between items-center">
                  <span className="font-light text-gray-600">
                    {`
                    ${new Date(post.created_at).getDay()}.
                    ${new Date(post.created_at).getMonth()}.
                    ${new Date(post.created_at).getFullYear()}`}
                  </span>
                </div>
                <div className="mt-2">
                  <a
                    className="text-2xl text-gray-700 font-bold hover:text-gray-600"
                    href={`/${post.full_slug}`}
                  >
                    {post.content.title}
                  </a>
                  <p className="mt-2 text-gray-600">{post.content.intro}</p>
                </div>
                <div className="flex justify-between items-center mt-4">
                  <a
                    className="text-blue-600 hover:underline"
                    href={`/${post.full_slug}`}
                  >
                    Read more
                  </a>
                </div>
              </li>
            ))}
          </ul>
        </main>
      </Layout>
    );
  }
}

We need to also update our navigation to this path. Change the About entry in line 35 to the URL blog and name it Blog.

./components/Navigation.js

 <ul className="...">
   <li>
       <a href="/" className="...">Home</a>
   </li>
   <li>
       <a href="/blog" className="...">Blog</a>
   </li>
</ul>

Our navigation should work now and when you click on Blog, you should see a page similar to the one below.

Overview Blog Posts
COMMIT:

You can check the current progress in this Github commit added post overview page.

Adding Another Language

We can implement internationalization with two different approaches in Storyblok. It strongly depends on your use case, which you should use. Read more about the Internationalization in docs. We are going to use Field Level Translation.

All you need to do on the Storyblok side is go to the Settings of your space and define a new language in the Languages tab. Let's add German as a language and set English as the default language.

Add Language

If you open any story in the Visual Editor now, you will see the language dropdown in the header.

Storyblok editing capabilities

To do that open the field headline of the Teaser in the Define Schema and check the box translatable.

Storyblok editing capabilities

If you choose German language right now, you won't see any changes because we didn't set any of the fields in our components as translatable.

Change the language to German and you will see a Translate checkbox next to the translatable field. If you set the checkbox to true you are able to translate the value and you will see a real-time preview in the Preview. If the checkbox stays false, the default value will be used.

Translate Field

Adding language support to Next.js

The next step is to add the language support to Next.js. We will need to switch the Navigation and the translated fields to German if we click the German Language. Let's start by creating a dyamic start page depending on the slug. In pages create a [language].js file, which is very similar to the index.page. What we do here is reading the current language via the query.language paramter in the getInitialProps function and passing the current language (e.g. en or de) to the Layout.js file. Add the same code also to the index.js file.

pages/[language].js and pages/index.js

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 = {
      story: props.res.data.story,
      language: props.language,
    }
  }

  static async getInitialProps({ query }) {
    StoryblokService.setQuery(query)
    let language = query.language || "en"
    let insertLanguage = language !== "en" ? `/${language}` : ""
    let res = await StoryblokService.get(`cdn/stories${insertLanguage}/home`,
    {
      "resolve_relations": "featured-posts.posts"
    })

    return {
      res,
      language
    }
  }

  componentDidMount() {
    StoryblokService.initEditor(this)
  }

  render() {
    const contentOfStory = this.state.story.content

    return (
      <Layout language={this.state.language}>
        <Page content={contentOfStory} />
      </Layout>
    )
  }
}

Next we have to adapt the Layou.js and Navigation.js file to display the correct language. In Layout.js we need to receive and pass the language prop to the Navigation.

components/Layout.js

import Head from '../components/Head'
import Navigation from '../components/Navigation'
import Footer from '../components/Footer'
import StoryblokService from '../utils/storyblok-service'

const Layout = ({ children, language }) => (
  <div className="bg-gray-300">
    <Head />
    <Navigation language={language} />
    {children}
    <Footer />
    {StoryblokService.bridge()}
  </div>
)

export default Layout

And then in the Navigation we need to set some classes depending if the language is active.

components/Navigation.js

const Navigation = ({ language }) => (
  <header className="w-full bg-white">
    <nav className="" role="navigation">
      <div>
       ...
       </div>
        <div>
          <ul>
          ...
          </ul>
          <ul className="flex flex-col mt-4 -mx-4 pt-4 border-t md:flex-row md:items-center md:mx-0 md:mt-0 md:pt-0 md:border-0">
            <li>
              <a href="/" className={`block px-4 py-1 md:p-2 rounded-lg lg:px-4 
              ${language === "en" ? "bg-black text-white" : ""}`}>EN</a>
            </li>
            <li>
              <a href="/de" className={`block px-4 py-1 md:p-2 rounded-lg lg:px-4 
              ${language === "de" ? "bg-black text-white" : ""}`}>DE</a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </header>
)

export default Navigation

Now you should be able to switch between the two language and see a different headline when clicking the language buttons.

Translated Home Page

Translating the Blog Post

We can also translate our post. Let's open up our first entry and see what happens when we switch the language to German.

Translate Blog Post

Since Storyblok automatically prepends the language to our slug (1), we need to change our blog path in Next.js. Inside pages, create a new folder language and move the blog folder inside, so the path for our blog file is: pages/[language]/blog/[slug].js.

Then we need to update the reading of the current path on every file that is importing Layout. First edit the blog entry:

pages/[language]/blog/[slug].js

import Layout from "../../../components/Layout";
import StoryblokService from "../../../utils/storyblok-service";

export default class extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      stories: props.res.data.stories,
      language: props.language,
    };
  }

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

    let language = query.language || "en";
    let insertLanguage = language !== "en" ? `${language}/` : "";
    const res = await StoryblokService.get(`cdn/stories`, {
      starts_with: `${insertLanguage}blog/`,
    });

    return {
      res,
      language,
    };
  }

  componentDidMount() {
    StoryblokService.initEditor(this);
  }

  render() {
    const posts = this.state.stories;

    return (
      <Layout language={this.state.language}>
        <main className="container mx-auto">
          <h1 className="text-5xl font-bold font-serif text-primary tracking-wide pt-12">
            All Posts
          </h1>

          <ul>
            {posts.map((post) => {
              const lang = post.lang === "default" ? "/en" : `/${post.lang}`;
              return (
                <li
                  key={post._uid}
                  className="max-w-4xl px-10 my-4 py-6 rounded-lg shadow-md bg-white"
                >
                  <div className="flex justify-between items-center">
                    <span className="font-light text-gray-600">
                      {`
                    ${new Date(post.created_at).getDay()}.
                    ${new Date(post.created_at).getMonth()}.
                    ${new Date(post.created_at).getFullYear()}`}
                    </span>
                  </div>
                  <div className="mt-2">
                    <a
                      className="text-2xl text-gray-700 font-bold hover:text-gray-600"
                      href={`${lang}/blog/${post.slug}`}
                    >
                      {post.content.title}
                    </a>
                    <p className="mt-2 text-gray-600">{post.content.intro}</p>
                  </div>
                  <div className="flex justify-between items-center mt-4">
                    <a
                      className="text-blue-600 hover:underline"
                      href={`${lang}/blog/${post.slug}`}
                    >
                      Read more
                    </a>
                  </div>
                </li>
              );
            })}
          </ul>
        </main>
      </Layout>
    );
  }
}

And then we also need to update the overview page for multilanguage support

pages/[language]/blog/index.js

import Layout from "../../../components/Layout";
import StoryblokService from "../../../utils/storyblok-service";

export default class extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      stories: props.res.data.stories,
      language: props.language,
    };
  }

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

    let language = query.language || "en";
    let insertLanguage = language !== "en" ? `${language}/` : "";
    const res = await StoryblokService.get(`cdn/stories`, {
      starts_with: `${insertLanguage}blog/`,
    });

    return {
      res,
      language,
    };
  }

  componentDidMount() {
    StoryblokService.initEditor(this);
  }

  render() {
    const posts = this.state.stories;

    return (
      <Layout language={this.state.language}>
        <main className="container mx-auto">
          <h1 className="text-5xl font-bold font-serif text-primary tracking-wide pt-12">
            All Posts
          </h1>

          <ul>
            {posts.map((post) => {
              const lang = post.lang === "default" ? "en" : post.lang;
              return (
                <li
                  key={post._uid}
                  className="max-w-4xl px-10 my-4 py-6 rounded-lg shadow-md bg-white"
                >
                  <div className="flex justify-between items-center">
                    <span className="font-light text-gray-600">
                      {`
                    ${new Date(post.created_at).getDay()}.
                    ${new Date(post.created_at).getMonth()}.
                    ${new Date(post.created_at).getFullYear()}`}
                    </span>
                  </div>
                  <div className="mt-2">
                    <a
                      className="text-2xl text-gray-700 font-bold hover:text-gray-600"
                      href={`/${lang}/blog/${post.slug}`}
                    >
                      {post.content.title}
                    </a>
                    <p className="mt-2 text-gray-600">{post.content.intro}</p>
                  </div>
                  <div className="flex justify-between items-center mt-4">
                    <a
                      className="text-blue-600 hover:underline"
                      href={`/${lang}/blog/${post.slug}`}
                    >
                      Read more
                    </a>
                  </div>
                </li>
              );
            })}
          </ul>
        </main>
      </Layout>
    );
  }
}

Finally we need to update the Blog Link in our Navigation.js component depending on which language is active:

components/Navigation.js

<li>
 <a href={`/${language}/blog`} className="block px-4 py-1 md:p-2 lg:px-8">Blog</a>
</li>

Then we also need to update the paths to match our language in our components/FeaturePosts.js:

components/FeaturesPosts.js

import React from "react";
import SbEditable from "storyblok-react";

const FeaturedPosts = ({ blok }) => {
  return (
    <SbEditable content={blok} key={blok._uid}>
      <div className="py-8 mb-6 container mx-auto text-left" key={blok._uid}>
        <div className="relative">
          <h2 className="relative font-serif text-4xl z-10 text-primary">
            {blok.title}
          </h2>
          <div className="absolute top-0 w-64 h-10 mt-6 -ml-4 bg-yellow-300 opacity-50" />
        </div>
        <ul className="flex">
          {blok.posts.map((post) => {
            const lang = post.lang === "default" ? "/en" : `/${post.lang}`;

            return (
              <li key={post.content._uid} className="pr-8 w-1/3">
                <a
                  href={`${lang}/blog/${post.slug}`}
                  className="py-16 block transition hover:opacity-50"
                >
                  <img src={post.content.image} className="pb-10 w-full" />
                  <h2 className="pb-6 text-lg font-bold">
                    {post.content.title}
                  </h2>
                  <p className="pb-6 text-gray-700 leading-loose">
                    {post.content.intro}
                  </p>
                </a>
              </li>
            );
          })}
        </ul>
      </div>
    </SbEditable>
  );
};

export default FeaturedPosts;

Now when we click on DE and then Blog, we should see the translated blog post.

Translated Blog Storyblok editing capabilities
COMMIT:

You can check the current progress in this Github commit added multilanguage support.

Deploying to Vercel

You have multiple options for the deployment of your website/application to go live or to preview the environment. One of the easiest ways is to use Vercel and deploy using the command line or their outstanding GitHub Integration.

First, create an account on Vercel and install their CLI application.

npm i -g vercel

Deploy your website by running the vercel in your console.

vercel

Take a look at the deployed Demo project: nextjs-storyblok-multilanguage-website.vercel.app/

Resource Link
Github repository of this Tutorial github.com/storyblok/nextjs-multilanguage-website
The project live nextjs-storyblok-multilanguage-website.vercel.app
Vercel Vercel
Next.js Next.js
React.js React.js
Storyblok App Storyblok
Next.JS Setup Next.js Set-up
Tailwindcss with Next.js How to install tailwindcss with Next
React.js: Dynamic Component Rendering React.js: Dynamic Component Rendering