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

Contents

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

If you are in a hurry you can download the whole project (nuxtblok.now.sh) at Github https://github.com/storyblok/nuxtjs-multilanguage-website

  1. Introduction

  2. Environment setup

  3. Build a homepage

  4. Build a navigation menu

  5. Build a blog section

  6. Build a sitemap

  7. Adding another language

  8. Deploy to live

Environment setup

Requirements

If not done yet install NodeJS, NPM, and NPX.


We will start by initializing the project with the Nuxt.js create app template. Navigate through the guide and choose the following options:

  • package manager of your choice

  • none UI Framework

  • none custom server framework

  • no modules needed

  • choose Universal rendering mode (SSR)

npx create-nuxt-app mywebsite // yarn create nuxt-app mywebsite
cd mywebsite
npm run dev // yarn dev

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

As we will use SCSS to organize our CSS we also need to install the sass-loader.

npm install --save-dev sass-loader node-sass css-loader // yarn add -D sass-loader node-sass css-loader

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. In the end, you will have a header, a main and a footer section and some useful global utility CSS classes.

Global SCSS in Nuxt.js

In the first step we installed the SCSS loader so let's create some global stylings and define SCSS variables. We will create a folder for styling general HTML tags assets/scss/elements/ and one for our utility component assets/scss/components/

assets/
--| scss/
-----| elements/
--------| body.scss
--------| ...
-----| components/
--------| util.scss
--------| ...
--| styles.scss

Create the file assets/scss/styles.scss and add the following content.

assets/scss/styles.scss
$brand-color: #357F8A;
$breakpoint-small: 480px;
$breakpoint-medium: 768px;
$breakpoint-large: 960px;
$breakpoint-xlarge: 1220px;
$breakpoint-mini-max: ($breakpoint-small - 1);
$breakpoint-small-max: ($breakpoint-medium - 1);
$breakpoint-medium-max: ($breakpoint-large - 1);
$breakpoint-large-max: ($breakpoint-xlarge - 1);

@import 'elements/body.scss';
@import 'components/util.scss';

Instead of putting the stylings of all HTML elements in one file I prefer to make separate files to keep the project structured and scalable. Create the file assets/scss/elements/body.scss to define the base font stylings.

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

In the components folder, we manage the global SCSS components and helper classes. Create the file assets/scss/components/util.scss to define the global utility classes.

assets/scss/components/util.scss
.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;
}

Add a google font to Nuxt.js

In the file body.scss, we defined font Zilla Slab. As this is not a system font we need to add it to the head section of our document. Open the Nuxt.js configuration file (nuxt.config.js ) and add the font stylesheet to the head section.

nuxt.config.js
head: {
    ...
    link: [
      ...
      {
        rel: 'stylesheet',
        href: 'https://fonts.googleapis.com/css?family=Zilla+Slab:400,700'
      }
    ]
},
...

Define the default layout

Now that we have our SCSS in place we need to add it to the project. Make sure you have installed the sass loader in step one and replace the code of file layouts/default.vue with the following code.

layouts/default.vue
<template>
  <div>
    <top-header/>
    <main id="main" role="main">
      <nuxt/>
    </main>
    <bottom-footer/>
  </div>
</template>

<script>
import TopHeader from '~/components/TopHeader.vue'
import BottomFooter from '~/components/BottomFooter.vue'

export default {
  components: {
    TopHeader,
    BottomFooter
  }
}
</script>

<style lang="scss">
@import '../assets/scss/styles.scss';
</style>

You will see an error that the components TopHeader.vue and BottomFooter.vue don't exist. So let's create them.

Create the header component

Create a file ./components/TopHeader.vue with the following code. Notice the attribute lang="scss" in the style tag, which allows you to use SCSS code in the style section of the Vue.js single file component.

components/TopHeader.vue
<template>
  <header class="top-header util__flex util__container">
    <nav class="top-header__col">
      <ul class="nav">
        <li>
          <nuxt-link class="nav__item" to="/">Home</nuxt-link>
        </li>
        <li>
          <nuxt-link class="nav__item" to="/en/blog">Blog</nuxt-link>
        </li>
      </ul>
    </nav>
    <a href="/" class="top-header__col top-header__logo">
      <img src="http://a.storyblok.com/f/42016/1096x313/0353bf6654/logo2.png">
    </a>
    <nav class="top-header__col top-header__second-navi">
      <ul class="nav">
        <li>
          <nuxt-link class="nav__item" to="/en/blog">English</nuxt-link>
        </li>
        <li>
          <nuxt-link class="nav__item" to="/de/blog">German</nuxt-link>
        </li>
      </ul>
    </nav>
  </header>
</template>

<style lang="scss">
  .top-header {
    justify-content: space-between;
    padding-top: 30px;
    padding-bottom: 30px;
  }

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

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

  .top-header__second-navi {
    text-align: right;
  }
</style>

Create the footer component

Add BottomFooter.vue to your ./components folder.

components/BottomFooter.vue
<template>
  <footer class="bottom-footer">
    <div class="util__container">
      <nuxt-link class="bottom-footer__link" to="/en/sitemap">Sitemap</nuxt-link>
    </div>
  </footer>
</template>

<style lang="scss">
.bottom-footer {
  background: #e3f2ed;
  padding: 40px 0 120px 0;
  text-align: center;
}

.bottom-footer__link {
  color: #8ba19a;
  text-decoration: none;
}
</style>

At this moment, 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.

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 Nuxt.js module

The Storyblok module will install $storyapi and $storyblok on the Vue instance.

$ npm install storyblok-nuxt --save // yarn add -D storyblok-nuxt

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.

Add the following to your nuxt.config.js file and replace PREVIEW_TOKEN with your preview token.

nuxt.config.js
module.exports = {
  modules: [
    [
      'storyblok-nuxt',
      {
        accessToken: 'YOUR_PREVIEW_TOKEN',
        cacheProvider: 'memory'
      }
    ]
  ],
  ...
}

Update the homepage component

Now replace the default content of pages/index.vue with following:

pages/index.vue
<template>
  <section class="util__container">
    <component v-if="story.content.component" :key="story.content._uid" :blok="story.content" :is="story.content.component"></component>
  </section>
</template>

<script>

export default {
  data () {
    return {
      story: { content: {} }
    }
  },
  mounted () {
    // use the bridge to listen to events
    this.$storybridge.on(['input', 'published', 'change'], (event) => {
      if (event.action == 'input') {
        if (event.story.id === this.story.id) {
          this.story.content = event.story.content
        }
      } else {
        // window.location.reload()
        this.$nuxt.$router.go({
          path: this.$nuxt.$router.currentRoute,
          force: true,
        })
      }
    })
  },
  asyncData (context) {
    // Load the JSON from the API
    return context.app.$storyapi.get('cdn/stories/home', {
      version: 'draft'
    }).then((res) => {
      return res.data
    }).catch((res) => {
      if (!res.response) {
        console.error(res)
        context.error({ statusCode: 404, message: 'Failed to receive content form api' })
      } else {
        console.error(res.response.data)
        context.error({ statusCode: res.response.status, message: res.response.data })
      }
    })
  }
}
</script>

The asyncData method will load a JSON that defines which components we will render on the homepage. After this update, your website should look similar to the following screenshot.

Creating the homepage components

To render the full homepage we will need to create some components. Add the file components.js with following content into the plugins folder.

plugins/components.js
import Vue from 'vue'
import Page from '~/components/Page.vue'
import Teaser from '~/components/Teaser.vue'
import Grid from '~/components/Grid.vue'
import Feature from '~/components/Feature.vue'

Vue.component('page', Page)
Vue.component('teaser', Teaser)
Vue.component('grid', Grid)
Vue.component('feature', Feature)

Nuxt.js doesn't pick up the files in plugins folder automatically so we need to add the components.js to the nuxt.config.js.

nuxt.config.js
module.exports = {
  plugins: [
    '~/plugins/components'
  ],
  ...

Then create the Vue components inside the components folder.

Page.vue

components/Page.vue
<template>
  <div v-editable="blok" class="page">
    <component :key="blok._uid" v-for="blok in blok.body" :blok="blok" :is="blok.component"></component>
  </div>
</template>

<script>
export default {
  props: ['blok']
}
</script>

Teaser.vue

components/Teaser.vue
<template>
  <div v-editable="blok">
    {{ blok.headline }}
  </div>
</template>

<script>
export default {
  props: ['blok']
}
</script>

Grid.vue

components/Grid.vue
<template>
  <div v-editable="blok" class="util__flex">
    <component :key="blok._uid" v-for="blok in blok.columns" :blok="blok" :is="blok.component"></component>
  </div>
</template>

<script>
export default {
  props: ['blok']
}
</script>

Feature.vue

components/Feature.vue
<template>
  <div v-editable="blok" class="util__flex-eq">
    <h1>{{ blok.name }}</h1>
  </div>
</template>

<script>
export default {
  props: ['blok']
}
</script>

After the reload of http://localhost:3000/ you should see the following design.

You can see created components in the source code using the Vue.js devtools.

Create your first block in Storyblok

We just loaded the demo content of Storyblok (added by default at the creation of the new space) and now we will extend the teaser component with interactive slides.

Setup of the Nuxt.js environment preview

At first, we need to connect your Nuxt.js environment to the Storyblok visual composer by inserting your development host http://localhost:3000/ in the default environment location. To do so, open Settings of your space and insert http://localhost:3000/ in the default environment location. Don't forget to save the changes.

You can also define multiple preview links for the Storyblok visual composer. We will cover this topic later by the deployment of the website.

Now you can go to the Content section of Storyblok and open the Home page of our website. At first, you will see an error. This is caused by resolving the URL path, as the website we opened is called /home, Nuxt.js doesn't know that this is homepage (index.html).

IMPORTANT: You have to change the real path field to / (see screenshot) otherwise, you will get a error This page could not be found from Nuxt.js.

Don't forget to save your changes in the top right corner. After this change, you should see the preview of your page in the left part of the application.

Let's define the schema of a new slide block/component

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

After updating the schema and adding the content to Storyblok we will need to add the Vue.js component for the slide into the Nuxt.js project. Create components/Slide.vue with the following content.

components/Slide.vue
<template>
  <div class="slide" v-editable="blok">
    <img :src="blok.image">
  </div>
</template>

<script>
export default {
  props: ['blok']
}
</script>

<style lang="scss">
.slide img {
  width: 100%;
}
</style>

Don't forget to add the new component to your component.js file, otherwise will be the component not registered in Vue.

plugins/components.js
import Vue from 'vue'
...
import Slide from '~/components/Slide.vue'

...
Vue.component('slide', Slide)

Of course, we don't want to show all the slides at once. So let's extend the Teaser.vue with some logic to show the dot navigation. You can use any Vue.js slider plugin for a more advanced slider but let's keep it simple here.

components/Teaser.vue
<template>
  <div v-editable="blok" class="teaser">
    <component v-if="slide" :blok="slide" :is="slide.component"></component>
    <div class="teaser__pag">
      <button @click="handleDotClick(index)"
              :key="index"
              v-for="(blok, index) in blok.body"
              :class="{'teaser__pag-dot--current': index == currentSlide}"
              class="teaser__pag-dot">Next</button>
    </div>
  </div>
</template>

<script>
export default {
  props: ['blok'],

  data () {
    return {
      currentSlide: 0
    }
  },

  computed: {
    slide () {
      let slides = this.blok.body.filter((slide, index) => {
        return this.currentSlide === index
      })
      if (slides.length) {
        return slides[0]
      }
      return null
    }
  },

  methods: {
    handleDotClick (index) {
      this.currentSlide = index
    }
  }
}
</script>

<style lang="scss">
.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;

  &--current {
    background-color: #000;
  }
}
</style>

After updating all the mentioned files you should see the following result.

Extending the feature component

Every feature component has currently only a title. Let's extend the feature component/block with a description text and icon.

Now you can use the visual composer and click directly on the Feature 1. As you hover with your mouse above the visual composer, you should see dashed border around the clickable components/blocks. If you click/select on one of the components you should see green highlight around that component. The right panel should show the content of the selected component.

Now we need to define two new fields in the Feature component. Click on the Define schema button in the top right corner of the right panel and add a field named description of the type textarea, and a field named icon of the type image.

Open the feature component (components/Feature.vue) and extend it with the new fields as well as some basic SCSS style.

components/Feature.vue
<template>
  <div v-editable="blok" class="feature util__flex-eq">
    <img :src="resizedIcon" class="feature__icon">
    <h1>{{ blok.name }}</h1>
    <div class="feature__description">
      {{ blok.description }}
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    resizedIcon () {
      if (typeof this.blok.icon !== 'undefined') {
        return '//img2.storyblok.com/80x80' + this.blok.icon.replace('//a.storyblok.com', '')
      }
      return null
    }
  },
  props: ['blok']
}
</script>

<style lang="scss">
.feature {
  text-align: center;
  padding: 30px 10px 100px;
}

.feature__icon {
  max-width: 80px;
}
</style>

You should have fully editable homepage at this moment. Feel free to experiment with the content and see how the live visual editor behaves.

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 the English language named en.

Navigate back to the content section of your space and create a new folder by clicking the button in the top right corner. Name the folder en and don't change the slug of the folder.

Create global settings

Inside the folder en we create a new content entry/item called Settings with the new content type settings. Click on + Entry in the top right corner. Fill the inputs according to the following screen and create settings component. This settings item will contain all the navigation items and other global configurations of our website.

Change the real path in the config of Settings to / and create the schema for the main navigation defining the field main_navi with the type Blocks. After changing the real path don't forget to reload the website to update the preview section of the application.

Add a block for the nav item name nav_item with the schema containing a filed name of the type Text and a filed link of the type Link. At the end your Settings content item should look like on the following screenshot:

Getting global settings with the Vuex store

As Nuxt.js comes with built-in support for Vuex we will use it to retrieve and store the configuration of the navigation as well as the current language. To initialize Vuex in Nuxt.js you have to only create a index.js file in your store folder. Copy & paste the following code into the store/index.js and save the file.

store/index.js
export const state = () => ({
  cacheVersion: '',
  language: 'en',
  settings: {
    main_navi: []
  }
})

export const mutations = {
  setSettings(state, settings) {
    state.settings = settings
  },
  setLanguage(state, language) {
    state.language = language
  },
  setCacheVersion(state, version) {
    state.cacheVersion = version
  }
}

export const actions = {
  loadSettings({ commit }, context) {
    return this.$storyapi.get(`cdn/stories/${context.language}/settings`, {
      version: context.version
    }).then((res) => {
      commit('setSettings', res.data.story.content)
    })
  }
}

After dispatching the action loadSettings in the middleware of the Nuxt.js, we will have the navigation items available at $store.state.settings.main_navi.

Add a middleware

A middleware in Nuxt.js allows you to define a function that runs before rendering the page. The function can be asynchronous and return a Promise so it is ideal for loading our settings from the API. Create it by adding the following code into the middleware/languageDetection.js file.

middleware/languageDetection.js
export default function ({ app, isServer, route, store, isDev }) {
  let version = route.query._storyblok || isDev ? 'draft' : 'published'
  let language = route.params.language || 'en'

  if (isServer) {
    store.commit('setCacheVersion', app.$storyapi.cacheVersion)
  }

  if (!store.state.settings._uid || language !== store.state.language) {
    store.commit('setLanguage', language)

    return store.dispatch('loadSettings', {version: version, language: language})
  }
}

Additionally, the middleware needs to be registered in the nuxt.config.js.

nuxt.config.js
module.exports = {
  ...
  router: {
    middleware: 'languageDetection'
  },

Access the data in the TopHeader component

With $store.state.settings.main_navi we can now easily access the navigation items and loop over them to render the navigation in components/TopHeader.vue. Replace the template part of the TopHeader.vue with the following code.

components/TopHeader.vue
<template>
  <header class="top-header util__flex util__container">
    <nav class="top-header__col">
      <ul class="top-header__nav">
        <li :key="index" v-for="(navitem, index) in $store.state.settings.main_navi">
          <nuxt-link class="top-header__link" :to="navitem.link.cached_url">
            {{ navitem.name }}
          </nuxt-link>
        </li>
      </ul>
    </nav>
    <a href="/" class="top-header__col top-header__logo">
      <img src="http://a.storyblok.com/f/42016/1096x313/0353bf6654/logo2.png">
    </a>
    <nav class="top-header__col top-header__second-navi">
      <ul class="top-header__nav top-header__nav--right">
        <li>
          <nuxt-link class="top-header__link" to="/en/blog">English</nuxt-link>
        </li>
        <li>
          <nuxt-link class="top-header__link" to="/de/blog">German</nuxt-link>
        </li>
      </ul>
    </nav>
  </header>
</template>

...

At this moment you should be able to edit the navigation using the Settings in Storyblok. You can add new navigation items or edit current ones. After the saving, the preview should be reloaded and the changes should be visible on your screen similar to the following screenshot.

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 Nuxt.js you can define dynamic routes creating folders with the underscore prepended _ and Nuxt will automatically resolve them to Vue.js routes.

Our final URL should look like /:language/blog/:slug so we will need to create the following folder structure in our Nuxt.js project.

pages/
--| _language/
-----| blog/
--------| _slug.vue
--------| index.vue
--| index.vue

Add a blog detail page

We start with the blog detail page at pages/_language/blog/_slug.vue which will fetch the content from the Storyblok 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 // yarn add -D marked

Then we will create the file pages/_language/blog/_slug.vue for the dynamic route of the blog posts.

pages/_language/blog/_slug.vue
<template>
  <section class="util__container">
    <div v-editable="story.content" class="blog">
      <h1>{{ story.content.name }}</h1>
      <p><strong>{{ story.content.intro }}</strong></p>
      <div class="blog__body" v-html="body">
      </div>
    </div>
  </section>
</template>

<script>
import marked from 'marked'

export default {
  data () {
    return {
      story: { content: { body: '' } }
    }
  },
  computed: {
    body () {
      return marked(this.story.content.body)
    }
  },
  mounted () {
    // use the bridge to listen to events
    this.$storybridge.on(['input', 'published', 'change'], (event) => {
      if (event.action == 'input') {
        if (event.story.id === this.story.id) {
          this.story.content = event.story.content
        }
      } else {
        // window.location.reload()
        this.$nuxt.$router.go({
          path: this.$nuxt.$router.currentRoute,
          force: true,
        })
      }
    })
  },
  asyncData (context) {
    // Load the JSON from the API
    let version = context.query._storyblok || context.isDev ? 'draft' : 'published'

    return context.app.$storyapi.get(`cdn/stories/${context.params.language}/blog/${context.params.slug}`, {
      version: version,
      cv: context.store.state.cacheVersion
    }).then((res) => {
      return res.data
    }).catch((res) => {
      context.error({ statusCode: res.response.status, message: res.response.data })
    })
  }
}
</script>


<style lang="scss">
.blog {
  padding: 0 20px;
  max-width: 600px;
  margin: 40px auto 100px;

  img {
    width: 100%;
    height: auto;
  }
}

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

Create the overview page

To list the blog posts we will create a route on /:language/blog simply by saving the file index.vue into the blog folder.

Storyblok 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/_language/blog/index.vue
<template>
  <section class="util__container">
    <div :key="blogPost.content._uid" v-for="blogPost in data.stories" class="blog__overview">
      <h2>
        <nuxt-link class="blog__detail-link" :to="'/' + blogPost.full_slug">
          {{ blogPost.content.name }}
        </nuxt-link>
      </h2>
      <small>
        {{ blogPost.published_at }}
      </small>
      <p>
        {{ blogPost.content.intro }}
      </p>
    </div>
  </section>
</template>

<script>
export default {
  data () {
    return { total: 0, data: { stories: [] } }
  },
  asyncData (context) {
    let version = context.query._storyblok || context.isDev ? 'draft' : 'published'

    return context.app.$storyapi.get('cdn/stories', {
      version: version,
      starts_with: `${context.store.state.language}/blog`,
      cv: context.store.state.cacheVersion
    }).then((res) => {
      return res
    }).catch((res) => {
      context.error({ statusCode: res.response.status, message: res.response.data })
    })
  }
}
</script>

<style lang="scss">
.blog__overview {
  padding: 0 20px;
  max-width: 600px;
  margin: 40px auto 60px;

  p {
    line-height: 1.6;
  }
}

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

Create the blog content in Storyblok

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.

Create the blog article

Go inside the blog folder and create a new content item/entry it will now automatically choose blog as content type.

Add the schema fields intro (Textarea), name (Text) and body (Markdown) to the blog component and create any demo content like on this screenshot. You should be able to see live preview during the editing of your content.

If you navigate to http://localhost:3000/en/blog you should see also a listing of your blog posts similar to the following screenshot.

Build a sitemap

To generate a sitemap or navigation tree with Nuxt.js of all your pages we will call Storyblok´s links API. The API includes the parent-child relationships through the parent_id and therefore we just need to generate a tree using a computed property. Create pages/_language/sitemap.vue file with the following code.

pages/_language/sitemap.vue
<template>
  <section class="util__container">
    <div class="sitemap">
      <h1>Sitemap</h1>

      <div v-for="language in tree" :key="language.id">
        <ul>
          <sitemap-item
            v-show="item.item.name !== 'Settings'"
            :model="item"
            v-for="item in language.children"
            :key="item.id">
          </sitemap-item>
        </ul>
      </div>
    </div>
  </section>
</template>

<script>
export default {
  data () {
    return {
      links: {}
    }
  },
  computed: {
    tree () {
      let parentChilds = this.parentChildMap(this.links)

      return this.generateTree(0, parentChilds)
    }
  },
  asyncData (context) {
    let version = context.query._storyblok || context.isDev ? 'draft' : 'published'

    return context.app.$storyapi.get('cdn/links', {
      version: version,
      starts_with: context.store.state.language,
      cv: context.store.state.cacheVersion
    }).then((res) => {
      return res.data
    }).catch((res) => {
      context.error(res)
    })
  },
  methods: {
    parentChildMap (links) {
      let tree = {}
      let linksArray = Object.keys(links).map(e => links[e])

      linksArray.forEach((link) => {
        if (!tree[link.parent_id]) {
          tree[link.parent_id] = []
        }

        tree[link.parent_id].push(link)
      })

      return tree
    },
    generateTree (parent, items) {
      let tree = {}

      if (items[parent]) {
        let result = items[parent]

        result.forEach((cat) => {
          if (!tree[cat.id]) {
            tree[cat.id] = {item: {}, children: []}
          }
          tree[cat.id].item = cat
          tree[cat.id].children = this.generateTree(cat.id, items)
        })
      }

      return Object.keys(tree).map(e => tree[e])
    }
  }
}
</script>

<style lang="scss">
.sitemap {
  max-width: 600px;
  margin: 20px auto 60px;
}
</style>

Create a components/SitemapItem.vue component for the sitemap nodes, which will be used to generate the children of the sitemap tree.

components/SitemapItem.vue
<template>
  <li class="sitemap-item">
    <nuxt-link :to="'/' + model.item.slug">
      {{model.item.name}}
    </nuxt-link>
    <ul v-if="model.children.length > 0">
      <sitemap-item
        :key="item.item.id"
        :model="item"
        v-for="item in model.children">
      </sitemap-item>
    </ul>
  </li>
</template>

<script>
export default {
  props: ['model']
}
</script>

<style lang="scss">
.sitemap-item {
  padding: 5px 0;

  a {
    color: #8ba19a;
  }

  ul {
    margin-top: 10px;
    margin-bottom: 10px;
  }
}
</style>

Don't add to register the new SitemapItem component to your components.js file.

plugins/components.js
...
import SitemapItem from '~/components/SitemapItem.vue'

...
Vue.component('sitemap-item', SitemapItem)

At the end, we should have the following page.

Adding another language

With Storyblok you have two options to make multi-language projects - a field-level translation and multi-tree translation. Field level translation is a good decision if you have most of your content translated. Consider using the multi-tree translation if the content tree is different in every language. If you are not sure what to choose read our guide about i18n.

Publish the content

Don't forget to publish your content as we are going to deploy with published content. You can do it on each content item in top right corner or globally in the content tree,

Deployment to Now.sh

You have multiple options for the deployment of your website/application to live or preview environment. One of the easiest ways is to use Now.sh and deployment using the command line.

We will use Now.sh version 2. At first, you should create an account on Now.sh and install their desktop application. Now create now.json file with the following content and change ACCOUNT_NAME to your account name.

now.json
{
  "version": 2,
  "name": "storyblok-nuxtjs",
  "builds": [
    {
      "src": "nuxt.config.js",
      "use": "@nuxtjs/now-builder",
      "config": {}
    }
  ],
  "scope": "ACCOUNT_NAME",
  "regions": ["bru1"],
  "public": false
}

Don't forget to define also .nowignore file.

.nowignore
.nuxt
node_modules
*.log

You can now test your Now.sh setup by writing now whoami into the command line, which should return your account name. If all works correctly you can run the deployment by simple runing now command in your terminal.

now

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

IMPORTANT: For the deployment of the Nuxt.js application on the Now.sh v2 you need to use @nuxtjs/now-builder provided by Nuxt.js core team.

Conclusion

It's incredibly easy to build a full-blown website with Nuxt.js and it comes with a great ecosystem. I really like the way Nuxt.js abstracts common tasks you normally do in the Webpack configuration. It feels like a little bit Ruby on Rails where conventions go over configuration. For big projects, these conventions make it easy to onboard new team members and make the projects a lot more maintainable.

ResourceLink
Github repository of this Tutorialgithub.com/storyblok/nuxtjs-multilanguage-website
The project livenuxtblok.now.sh
NowNow
Nuxt.jsNuxt.js
Vue.jsVue.js
Storyblok AppStoryblok

About the author

Alexander Feiglstorfer

Alexander Feiglstorfer

Passionate developer and always in search of the most effective way to resolve a problem. After working 13 years for agencies and SaaS companies using almost every CMS out there he founded Storyblok to solve the problem of being forced to a technology, proprietary template languages and the plugin hell of monolithic systems.


More to read...