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

Contents
    Try Storyblok

    Storyblok is the first headless CMS that works for developers & marketers alike.

    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.

    You can also take a look at the deployed Demo project: nextjs-storyblok-multilanguage-website.vercel.app/

    Requirements

    To continue with this tutorial, we don't expect you are an expert web developer, but you should understand a few basic concepts listed under this paragraph. We will guide you through most topics of this tutorial, but if you are beginning with Next.js & Storyblok, you should consider checking out our Add a headless CMS to Next.js in 5 minutes to begin with.

    warn:

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

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

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

    Environment setup

    If you haven't done so already, install Node.js, NPM on your machine. Start with running the following shell commands. This will clone our example repository for the multi-language blog.

    $ git clone https://github.com/storyblok/nextjs-multilanguage-website
    $ cd nextjs-multilanguage-website
    $ npm install

    Cloning the Storyblok Template

    To get the correct space setup for the Github example repository, click the following link to duplicate our example multi-language space: https://app.storyblok.com/#!/build/95804

    This will clone the space with the necessary content structure already set up. The first step is to set our Preview Url to our development server. Once you cloned the space, click on the URL {1} and then on Add or change preview urls {2}.

    Preview URL

    As the Location (default environment) {3} you need to enter http://localhost:3000/. You can also enter the URLs to enable and disable the preview mode: http://localhost:3000/api/preview?secret=MY_SECRET_TOKEN&slug= {4} and http://localhost:3000/api/exit-preview?slug= {5}.

    Default URL

    Read more about preview mode in our 5 minute tutorial.

    Connect Storyblok

    In the next step, we need to go to the Settings of our Storyblok space and retrieve our preview_token from the API Keys section {1}.

    Fill the Location (default environment) {2} as well as the Preview URLs {3} field with your localhost URL http://localhost:3000/.

    How to get preview access token of space

    To activate the connection to Storyblok, open the utils/storyblok.js file and enter the preview token you just retrieved, where it says accessToken on Line 5.

    import { useEffect, useState } from "react";
    import StoryblokClient from "storyblok-js-client";
    
    const Storyblok = new StoryblokClient({
      accessToken: "YOUR_PREVIEW_TOKEN",
      cache: {
        clear: "auto",
        type: "memory",
      },
    });
    

    Now we can start the development server by running npm run dev. From your Settings in Storyblok, click on Content {1} and open the Home entry {2}.

    Open the home story in the Content tab

    If your development server is running and your token is set correctly, you should see the URL on the top {1} and the Storyblok Bridge activated {2}. You should be able to click on the components {3} and directly edit them {3}.

    Storyblok Bridge working

    Understanding the Storyblok Bridge & Visual Editor

    The Storyblok bridge is loaded and activated in our utils/storyblok.js file via a Storyblok hook called useStoryblok.

    Inside this hook, we have a function addBridge (Line 48), which basically just adds the script for the Visual Editor, if it's not already present. Once the loading of the bridge is completed (Line 68), it will call the initEventListeners function (Line 6) to enable input (Line 20) and save events (Line 15) inside Storyblok. It also reloads the entire draft version of the story once the editor is entered (Line 26). Read our Storyblok JS Bridge documentation to learn more about it.

    On Line 79 we're using a boolean variable preview to conditionally load the bridge, when we want it. In most cases this would be used in combination with the preview mode, but can also be set to true to always load it.

    utils/storyblok.js
    import { useEffect, useState } from "react";
    import StoryblokClient from "storyblok-js-client";
    
    const Storyblok = new StoryblokClient({
      accessToken: "YOUR_PREVIEW_TOKEN",
      cache: {
        clear: "auto",
        type: "memory",
      },
    });
    
    export function useStoryblok(originalStory, preview, locale) {
      let [story, setStory] = useState(originalStory);
    
      // adds the events for updating the visual editor
      // see https://www.storyblok.com/docs/guide/essentials/visual-editor#initializing-the-storyblok-js-bridge
      function initEventListeners() {
        const { StoryblokBridge } = window
        if (typeof StoryblokBridge !== "undefined") {
          // initialize the bridge with your token
          const storyblokInstance = new StoryblokBridge({
            resolveRelations: ["featured-posts.posts", "selected-posts.posts"],
            language: locale,
          });
    
          // reload on Next.js page on save or publish event in the Visual Editor
          storyblokInstance.on(["change", "published"], () =>
            location.reload(true)
          );
    
          // live update the story on input events
          storyblokInstance.on("input", (event) => {
            if (event.story._uid === story._uid) {
              setStory(event.story);
            }
          });
    
          storyblokInstance.on('enterEditmode', (event) => {
            // loading the draft version on initial enter of editor
            Storyblok
              .get(`cdn/stories/${event.storyId}`, {
                version: 'draft',
                resolve_relations: ["featured-posts.posts", "selected-posts.posts"],
                language: locale,
              })
              .then(({ data }) => {
                if(data.story) {
                  setStory(data.story)
                }
              })
              .catch((error) => {
                console.log(error);
              }) 
          }) 
        }
      }
    
      // appends the bridge script tag to our document
      // see https://www.storyblok.com/docs/guide/essentials/visual-editor#installing-the-storyblok-js-bridge
      function addBridge(callback) {
        // check if the script is already present
        const existingScript = document.getElementById("storyblokBridge");
        if (!existingScript) {
          const script = document.createElement("script");
          script.src = "//app.storyblok.com/f/storyblok-v2-latest.js";
          script.id = "storyblokBridge";
          document.body.appendChild(script);
          script.onload = () => {
            // once the scrip is loaded, init the event listeners
            callback();
          };
        } else {
          callback();
        }
      }
    
      useEffect(() => {
          // only load inside preview mode
          if(preview) {
            // first load the bridge, then initialize the event listeners
            addBridge(initEventListeners);
          } 
      }, []);
    
      return story;
    }
    
    export default Storyblok;

    Since our visual editor is already set up with this settings, we can take a look at the content in Storyblok.

    Understanding the Content Structure

    Below you can see that once you cloned example in Storyblok it will ship with example content and components. 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 {1} in the main navigation you will see a list of components. These components, like the page component {2} for our pages and the post component {3} for our blog posts are ready to be used to in Storyblok & our project.

    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.

    We also have some content already set up. If you navigate to Content {1}, you will see the Home and About {2} stories with the conten type Page. You will also see a folder for the blog posts called blog {3}, that will have our blog posts.

    Content is set up
    hint:

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

    Using Storyblok components in Next.js

    Now that we already have components defined in Storyblok, let's take a look at the implementation of the components defined in the Next.js project. Open the components/Page.js  file:

    ./components/Page.js
    import DynamicComponent from './DynamicComponent'
    import SbEditable from 'storyblok-react'
    
    const Page = ({blok}) => (
      <SbEditable content={blok}>
        <main>
          {blok.body.map((blok) =>
            <DynamicComponent blok={blok} key={blok._uid} />
          )}
        </main>
      </SbEditable>
    )
    
    export default Page
    

    By wrapping any component with the SbEditable component, we can make all components loaded here clickable in Storyblok. If you want to control, which components are clickable, you can add or remove the SbEditable from the components.

    Explanation of blok Prop

    You probably noticed the blok prop in all of the components in the project. 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 most components (Page.js, Grid.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.

    ./components/DynamicComponent.js
    import Teaser from './Teaser'
    import Feature from './Feature'
    import FeaturedPosts from './FeaturedPosts'
    import Grid from './Grid'
    import Placeholder from './Placeholder'
    import PostsList from './PostsList'
    import Page from './Page'
    import BlogPost from './BlogPost'
    
    const Components = {
      'teaser': Teaser,
      'grid': Grid,
      'feature': Feature,
      'featured-posts': FeaturedPosts,
      'page': Page,
      'post': BlogPost,
      'selected-posts': PostsList,
    }
    
    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.

    ./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;

    If you add a new component {1} and it doesn't exist in your DynamicComponent.js file yet, it will show the Placeholder {3}.

    Unknown Component added

    Using storyblok-js-client

    To request content from Storyblok, 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.

    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.

    We already set up our client with the correct token in the previous step. This client is exported from the utils/storyblok.js file as the default export, so we can use it in other files to request content.

    ./utils/storyblok.js
    import { useEffect, useState } from "react";
    import StoryblokClient from "storyblok-js-client";
    
    const Storyblok = new StoryblokClient({
      accessToken: "YOUR-PREVIEW-TOKEN",
      cache: {
        clear: "auto",
        type: "memory",
      },
    });
    
    export function useStoryblok(originalStory, preview) {
      let [story, setStory] = useState(originalStory);
    
      // adds the events for updating the visual editor
      // see https://www.storyblok.com/docs/guide/essentials/visual-editor#initializing-the-storyblok-js-bridge
      function initEventListeners() {
        const { StoryblokBridge } = window
        if (typeof StoryblokBridge !== "undefined") {
          // initialize the bridge with your token
          const storyblokInstance = new StoryblokBridge({
            resolveRelations: ["featured-posts.posts", "selected-posts.posts"]
          });
    
          // reload on Next.js page on save or publish event in the Visual Editor
          storyblokInstance.on(["change", "published"], () =>
            location.reload(true)
          );
    
          // live update the story on input events
          storyblokInstance.on("input", (event) => {
            if (event.story._uid === story._uid) {
              setStory(event.story);
            }
          });
    
          storyblokInstance.on('enterEditmode', (event) => {
            // loading the draft version on initial enter of editor
            Storyblok
              .get(`cdn/stories/${event.storyId}`, {
                version: 'draft',
                resolve_relations: ["featured-posts.posts", "selected-posts.posts"]
              })
              .then(({ data }) => {
                if(data.story) {
                  setStory(data.story)
                }
              })
              .catch((error) => {
                console.log(error);
              }) 
          }) 
        }
      }
    
      // appends the bridge script tag to our document
      // see https://www.storyblok.com/docs/guide/essentials/visual-editor#installing-the-storyblok-js-bridge
      function addBridge(callback) {
        // check if the script is already present
        const existingScript = document.getElementById("storyblokBridge");
        if (!existingScript) {
          const script = document.createElement("script");
          script.src = "//app.storyblok.com/f/storyblok-v2-latest.js";
          script.id = "storyblokBridge";
          document.body.appendChild(script);
          script.onload = () => {
            // once the scrip is loaded, init the event listeners
            callback();
          };
        } else {
          callback();
        }
      }
    
      useEffect(() => {
          // only load inside preview mode
          if(preview) {
            // first load the bridge, then initialize the event listeners
            addBridge(initEventListeners);
          } 
      }, []);
    
      return story;
    }
    
    export default Storyblok;

    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.js file as we did.

    Generating Pages from Storyblok Stories

    We can automatically generate all the pages with a single file: pages/[[..slug]].js

    This file is using a lot of great Next.js functionality like static rendering and catch-all route generation, so we will explain the parts of it in more detail.

    getStaticPaths

    Let's start by taking a look at the function that generates the pages. In Next.js we can use the getStaticPaths function to generate static routes.

    pages/[[..slug]].js
    ...
    
    export async function getStaticPaths({ locales }) {  
      let { data } = await Storyblok.get('cdn/links/')
    
      let paths = []
      Object.keys(data.links).forEach(linkKey => {
          if (data.links[linkKey].is_folder) {
            return
          }
    
          // get array for slug because of catch all
          const slug = data.links[linkKey].slug
          let splittedSlug = slug.split("/")
          if(slug === 'home') splittedSlug = false
    
          // create additional languages
          for (const locale of locales) {
            paths.push({ params: { slug: splittedSlug }, locale })
          }
      })
    
      return {
          paths: paths,
          fallback: false,
      }
    }
    

    On Line 4 above, we use Storyblok's links endpoint to request all link entries from Storyblok. On Line 8 we check if the link is a folder and don't create a route if that is the case. We use Next.js catch all functionality to create all the routes. If it's the home entry, the slug should be false. On Line 18, we create a route for each locale in Next.js. Read the Next.js tutorial Internationalized Routing to understand how that routing works. The basis for that are the locales defined in next.config.js:

    next.config.js
    module.exports = {
        i18n: {
            locales: ['en', 'es'],
            defaultLocale: 'en',
        },
    }

    These locales should match the languages you define in your Storyblok space under Settings {1}, Languages {2}. In Storyblok you will always have the default language {3} and then you can add additional languages {4}.

    Language Setting

    getStaticProps

    The next important function is the getStaticProps function. It's the function to generate static pages in Next.js. This is the place where we want to request data from Storyblok.

    pages/[[..slug]].js
    ...
    export async function getStaticProps({ locale, locales, params, preview = false }) {
      let slug = params.slug ? params.slug.join('/') : 'home'
    
      let sbParams = {
        version: "draft", // or 'draft'
        resolve_relations: ["featured-posts.posts", "selected-posts.posts"],
        language: locale,
      }
     
      if (preview) {
        sbParams.version = "draft"
        sbParams.cv = Date.now()
      }
     
      let { data } = await Storyblok.get(`cdn/stories/${slug}`, sbParams)
     
      return {
        props: {
          story: data ? data.story : false,
          preview,
          locale,
          locales,
        },
        revalidate: 10, 
      }
    }
    
    ...

    On Line 3, we're joining the slug, this is necessary because in Catch-All routes, the slug needs to be an array. If there is no slug, so on the home page, we're passing the slug home. Then we're settings the parameters to pass to the Storyblok API. We can set the version to request the published or draft content (Line 6). We can resolve_relations to resolve relations to other stories (Line 7), for example when we select related blog posts on our blog. And finally, we request the correct language from Storyblok depending on which locale is active in Next.js (Line 8). If you're using preview mode, it makes sense to always load the draft version (Line 12), when inside of Next.js preview mode. Read our 5 minute guide to learn more about the preview mode and load the published version outside of it.

    Finally, on Line 16, we're requesting the correct story entry from Storyblok in the correct language. In the return statement on Line 18, we need to pass our props object and we can specify a revalidate parameter, which is an optional amount in seconds after which a page re-generation can occur.

    Render function

    The last missing part is our Page function, which makes use of the useStoryblok hook on Line 11. You can enable the StoryblokBridge and the Visual Editor on every page by default, or only inside of the preview mode with the second boolean parameter of the hook (Line 8). In most cases, you would only want to load the bridge if you're loading your page inside of Storyblok. Delete Line 8 and uncomment Line 10 to enable loading the Storyblok Bridge only inside the preview mode.

    On Line 14, we're passing our language locale to our Layout.js component and are automatically loading the right components, depending on the story content.

    pages/[[..slug]].js
    import React from 'react'
    import Layout from '../components/Layout'
    import DynamicComponent from '../components/DynamicComponent'
    
    import Storyblok, { useStoryblok } from "../utils/storyblok"
    
    export default function Page({ story, preview, locale }) {
      const enableBridge = true; // use preview 
      // use the preview variable to enable the bridge only in preview mode
      // const enableBridge = preview;
      story = useStoryblok(story, enableBridge)
    
      return (
        <Layout language={locale}>
          <DynamicComponent blok={story.content} />
        </Layout>
      )
    }

    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 a caption field to the Teaser component. Inside of your Home Story click on the Teaser component {1} and then on Define Schema in the right sidebar. Enter a new field caption {2} and click Add {3}. Then click on Save Schema.

    Define Schema in Storyblok

    Click on the created caption field and enter a caption for the image {1} and click Save {2} to make the changes available in the API.

    Cpation Field

    We have to update our Teaser component ./components/Teaser.js with the following code to add the caption:

    ./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 mb-4">{blok.headline}</h2>
              <figure>
                <img src={blok.image.filename}
                alt={blok.image.alt} className="w-full" />
                <figcaption class="mt-2 text-md">{blok.caption}</figcaption>
              </figure>
            </div>
          </div>
        </SbEditable>
      )
    }
    
    export default Teaser
    

    After adding the field you should see the caption below the image {1}.

    Teaser Edit

    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.

    Post Content Type in Storyblok

    To create new blog posts, we already have a folder called blog with a default content type of Post. This was created by making use of Storyblok blueprints by following the steps in the image below.

    Create blog

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

    Post Content Type

    Post Component in Next

    If we take a look at our BlogPost.js component file, we can see it's really similar to the other components and also loaded in the components/DynamicComponent.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 in new projects we need to install the package from npm. You don't have to do this for the cloned project, since it's already installed.

    Bash
    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

    You have to enter a Title {1} and click Publish {2} once to make the entry available as a static page.

    New Entry

    Resolving Relations on Multi-Options fields

    If you open the /blog/home story, you will see the selected-posts component. This component is set up with a multi-option field-type {1}, that allows referencing other story entries {2}. In this example, since we only want blog posts, we're limiting it to the content type post {3}. Open the following link with your preview token to see what it returns:

    https://api.storyblok.com/v1/cdn/stories/blog/?version=draft&token=YOUR_PREVIEW_TOKEN

    If you take a look into story.content.body[0].posts, you will see, that it includes a list of uuids. To actually get the full story objects, we have to resolve the relations first. Take a look at the following link with your preview token attached. This link is resolving the relations with the resolve_relations parameter:

    https://api.storyblok.com/v1/cdn/stories/blog/?version=draft&resolve_relations=selected-posts.posts&token=YOUR_PREVIEW_TOKEN

    By using the resolve_relations option of the Storyblok API, we can get the full story objects of those related objects.

    Featured Posts

    Inside of the utils/storyblok.js file you can find multiple places, where the relations are already resolved. Once directly on the Storyblok Bridge:

    const storyblokInstance = new StoryblokBridge({
            resolveRelations: ["featured-posts.posts", "selected-posts.posts"]
          });

    And once on the client call:

    Storyblok
              .get(`cdn/stories/${event.storyId}`, {
                version: 'draft',
                resolve_relations: ["featured-posts.posts", "selected-posts.posts"]
              })

    It's also used on the pages/[[...slug]].js when we're requesting the content:

     let sbParams = {
        version: "draft", 
        resolve_relations: ["featured-posts.posts", "selected-posts.posts"],
        language: locale,
      }

    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 in this example.

    All you need to do on the Storyblok side is go to the Settings {1} of your space and define a new language in the Languages {2} tab. Select a language {3} and click Add Language {3}. You should then see your languages below and can choose a Label for the language.

    Add Language

    If you open any story in the Visual Editor now, you will see the language dropdown in the header {1} and can switch to another language by clicking on them {2}.

    Selecting a language

    In order for fields to be translatable, we have to define that on each field, that we want to translate. To do that open the field headline of the Teaser and click Define Schema. Then click the headline field check the box translatable {2}.

    Set field as translatable

    Change the language to Spanish and you will see a Translate {2} 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 change. If the checkbox stays false, the default language value will be used.

    Translate Field

    To load the correct language version, we will also need to pass the locale to our Storyblok Bridge, similar to the resolveRelations. In the pages/[[...slug]].js we pass the locale to the useStoryblok hook on Line 3 below.

    pages/[[...slug]].js
    export default function Page({ story, preview, locale, locales }) {
      const enableBridge = preview;
      story = useStoryblok(story, enableBridge, locale)
    
      return (
        <Layout locale={locale} locales={locales}>
          <DynamicComponent blok={story.content} />
        </Layout>
      )
    }

    Then in the utils/storyblok.js file, we take that locale to load the correct version with the StoryblokBridge and the client.

    ...
    
    const storyblokInstance = new StoryblokBridge({
            resolveRelations: ["featured-posts.posts", "selected-posts.posts"],
            language: locale,
          });
    
    ...
    
    Storyblok
              .get(`cdn/stories/${event.storyId}`, {
                version: 'draft',
                resolve_relations: ["featured-posts.posts", "selected-posts.posts"],
                language: locale,
              })
    ...
    Hint:

    The language attribute is used in the Version 2 of the Content Delivery API. In Version 1 this attribute was called lang.

    Multi-language Navigation

    To show the correct Navigation depending on the language, we can pass our Next.js locale and locales to our Layout on Line 6 below.

    pages/[[...slug]].js
    export default function Page({ story, preview, locale, locales }) {
      const enableBridge = preview;
      story = useStoryblok(story, enableBridge, locale)
    
      return (
        <Layout locale={locale} locales={locales}>
          <DynamicComponent blok={story.content} />
        </Layout>
      )
    }
    

    The Layout file then passes the active locale, as well as all active locales on to the Navigation component on Line 8.

    components/Layout.js
    import Head from '../components/Head'
    import Navigation from '../components/Navigation'
    import Footer from '../components/Footer'
    
    const Layout = ({ children, locale, locales }) => (
      <div className="bg-gray-300">
        <Head />
        <Navigation locale={locale} locales={locales} />
        {children}
        <Footer />
      </div>
    )
    
    export default Layout
    

    Finally, we can build our multi-language navigation in the components/Navigation.js file. On Line 2 to 9, we're resolving the current locale to get the correct language label. If you have a lot of navigation items, we recommend using a settings object in Storyblok to build your Navigation.

    components/Navigation.js
    const Navigation = ({ locale, locales }) => {
      const resolveHome = {
        en: 'Home',
        es: 'Página principal',
      }
      const resolveAbout = {
        en: 'About',
        es: 'Acerca',
      }
      const defaultLocale = locale === 'en' ? '/' : `/${locale}/`
      return (
      <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={`${defaultLocale}`} className="block px-4 py-1 md:p-2 lg:px-8">{resolveHome[locale]}</a>
                </li>
                <li>
                  <a href={`${defaultLocale}blog`} className="block px-4 py-1 md:p-2 lg:px-8">Blog</a>
                </li>
                 <li>
                  <a href={`${defaultLocale}about`} className="block px-4 py-1 md:p-2 lg:px-8">{resolveAbout[locale]}</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">
              {
              locales.map(loc => {
                return(<li key={loc}>
                  <a href={`/${loc}`} className={`block px-4 py-1 md:p-2 rounded-lg lg:px-4 
                    ${locale === loc ? "bg-black text-white" : ""}`}>{loc}</a>
                </li>)
              })
              }
              </ul>
            </div>
          </div>
        </nav>
      </header>
    )}
    
    export default Navigation
    

    On Line 53, we're iterating over all the locales that are set in Next.js and showing an entry to switch the locale {1}. We're also highlighting the active locale with a black background.

    Switch language

    If you want to add another language, you will need to add an entry to the next-config.js file, as well as the language Settings in your Storyblok space.

    module.exports = {
        i18n: {
            locales: ['en', 'es', 'de'], 
            defaultLocale: 'en',
        },
    }

    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 the CLI application.

    Bash
    npm i -g vercel

    Deploy your website by running the vercel command in your console.

    Bash
    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