The Complete Guide to Build a Full Blown Multilanguage Website with Nuxt.js
Tags: vue, nuxt, storyblok, javascript
Published: 2020-01-10 13:50:05

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

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

1.  Introduction
2.  [Environment setup](#environment-setup)
3.  [Build a homepage](#build-a-homepage)
4.  [Build a navigation menu](#build-a-navigation-menu)
5.  [Build a blog section](#build-a-blog-section)
6.  [Build a sitemap](#build-a-sitemap)
7.  [Adding another language](#adding-another-language)
8.  [Deploy to live](#deploy-to-live)

Environment setup
--------------------------------------------------------------------------------------

### Requirements

-   Basic understanding of [NuxtJs](https://www.nuxtjs.org/)

-   [VueJs](https://vuejs.org/) & their [CLI](https://github.com/vuejs/vue-cli)

-   [NodeJS](https://nodejs.org/)

-   [NPM](https://www.npmjs.com/)

-   [NPX](https://www.npmjs.com/package/npx)

-   The CLI of [now.sh](https://zeit.co/download) for hosting

-   An account on [Storyblok.com](https://app.storyblok.com/) to manage content

If not done yet install NodeJs, NPM and NPX.\
We will start with initializing project with the nuxt.js starter template.

```bash
npx create-nuxt-app mywebsite
cd mywebsite && npm build
npm run dev
```

Nuxt.js starts it's server on port 3000 by default so after running npm run dev open your browser at [http://localhost:3000](http://localhost:3000/).

![Screenshot of Nuxt.js](https://img.storyblok.com/brYp8NzMqblLZ6XgDbHKI17obMI=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/39898/2270x1348/8bd0b38ebd/screenshot-jpg-2412x1526-2018-01-09-15-07-42.jpg)

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

```bash
npm install --save-dev sass-loader node-sass css-loader
```

To track the changes we make over the time we will also initialize the git repository.

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

Build a skeleton
-------------------------------------------------------------------------------------

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

### Global SCSS in Nuxt.js

In step 1. 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/`

```bash
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

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

```css
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 css components and helper classes.\
Create the file `assets/scss/components/util.scss` to define the global utility classes.

assets/scss/components/util.scss

```css
.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 `body.scss` file we defined Zilla Slab as font. As this is not a system font we need to add it to the head section of our document. There the Nuxt.js configuration file comes into play.\
Open `nuxt.config.js` and add the font stylesheet to the head section.

nuxt.config.js

```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 `layouts/default.vue` with following.

layouts/default.vue

```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 yet. So let's create them too.

### Create the header component

Notice the attribute lang="scss" at the style tag. This allows you to use SCSS in your Vue.js components.

components/TopHeader.vue

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

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

Currently, the website should look similar to the following screenshot. In the next step, I'll show you how to create the homepage with a teaser and a feature section.

![Nuxtjs Screenshot Tutorial](https://img.storyblok.com/8m-IZ9Ur2fJ1DVu7kGaGoNokU_w=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/39898/2294x1354/423e7be38b/screenshot-nuxtjs-jpg-2696x1684-2018-01-09-15-04-19.jpg)

Now let's commit that to git. See [my GitHub commit](https://github.com/storyblok/nuxtjs-multilanguage-website/commit/ea143131aad0609de96685ea4060f90102542778) for reference.

```bash
$ 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.

```bash
$ npm install storyblok-nuxt --save
```

After installing the module you need to initialize it with the preview token of your Storyblok space. [Signup or Login at app.storyblok.com](https://app.storyblok.com/) and create a new space. Add the following to your `nuxt.config.js` and replace `PREVIEW_TOKEN` with your preview token.

nuxt.config.js

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

```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 () {
    // Load the JSON from the API
    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()
      }
    })
  },
  asyncData (context) {
    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](https://nuxtjs.org/guide/async-data) will load a JSON that defines which components we will render on the homepage.

### Creating the homepage components

To render the full homepage we will need to create some components. Add the file `components.js` to the `plugins` folder.

plugins/components.js

```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 automatically so we need to add the `components.js` to the `nuxt.config.js`.

nuxt.config.js

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

Then create the Vue components inside the `components` folder.

### Page.vue

components/Page.vue

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

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

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

### Grid.vue

components/Grid.vue

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

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

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

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

![Components](https://img.storyblok.com/YHUmauIfMSZ0Cdqr0rKdvVRGjmw=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/39898/2368x1538/9e82095d55/mywebsite-2018-01-08-21-30-04-1.jpg)

Create your first block in Storyblok
---------------------------------------------------------------------------------------------------------

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

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

![Add localhost](https://img.storyblok.com/LQnB2Zfx15m7aeJnXzLMcdqYYnU=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/51376/2872x1426/4a146e3492/asset-port-access.png)

### Changing the real path field

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

![Real Path for your entry](https://img.storyblok.com/0otGzbhKfYxeC0nJIg5o0UgOxGY=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/51376/2296x1424/4965f6fefc/asset-realpath.png)

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

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

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

components/Slide.vue

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

Add the new component to your `component.js` file.

plugins/components.js

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

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

Of course, we don't want to show all slides at once. So let's extend the `Teaser.vue` with some logic to show a 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

```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 saving you should have the following result.

![First Result](https://img.storyblok.com/wdQHPnJs8JuyHlXF6C5oZTQRekk=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/51376/2876x1438/4bd0010142/asset-home-slide.png)

### Extending the feature section

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

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

![Extending the feature section](https://img.storyblok.com/xzj332yvAHe1wtaBRH7ZSflhqZ8=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/51376/2872x1432/50116ed0e0/asset-define-feature.png)

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

components/Feature.vue

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

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

![More content for features](https://img.storyblok.com/8FV0TbdIkRYZbIJHYAITWu8MgTE=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/51376/2868x1432/2f27d01218/asset-edit-feature.png)

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](https://www.storyblok.com/docs/Delivery-Api/Links) to generate the navigation automatically from your content tree. We will implement the first method in this tutorial.

As we are creating a multi-language website we create a global configuration for each language. Let's start with creating a **folder** for English `en`.

![creating english folder](https://img.storyblok.com/jSqkRdqRP9rcbpTgjDUoq8oEehw=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/51376/2868x1430/ff810aba14/asset-newfolder.png)

### Create a global settings content item

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

![Settings in english](https://img.storyblok.com/Ahf31ki8iyGb-WCqr7KDBpwxXb8=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/51376/2866x1416/8b91bcd33c/asset-new-settings.png)

Change the [real path](#changing-the-real-path-field) to / and create the schema for the main navigation defining the key `main_navi` with the type `Blocks`.

Add a block for the nav item with the `name` of the type `Text` and `link` of the type `Link`. At the end your `Settings` content item should look like following:

![Blocks in Nav item](https://img.storyblok.com/nFOd6qig6hIIQTAcWMw5vloLNV4=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/51376/2872x1428/6e49b5e3ae/asset-nav-item.png)

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 navigation configuration as well as the current language.

After dispatching the action `loadSettings` in a middleware we will have the navigation items available at `$store.state.settings.main_navi`.

store/index.js

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

```

Add a middleware

A `middleware` in Nuxt.js lets you define a function that runs before rendering the page. The function can be asynchronous and return a Promise so it's ideal for loading our settings from the API.

middleware/languageDetection.js

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

```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 them in `components/TopHeader.vue`.

components/TopHeader.vue

```js
<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>

...
```

Reloading the page we should see now the header navigation with the configurable navigation items from Storyblok.

![Menu is now visible](https://img.storyblok.com/n_-yMgw-0jUHF7cJLN4so_zUpyk=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/51376/2860x1424/41ffe72478/asset-nav-item-finished.png)

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 following folder structure.

```bash
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 API and then render the blog post with markdown using `marked` as a parser.

So first we will need to install the markdown parser.

```bash
$ npm install marked --save
```

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

pages/_language/blog/_slug.vue

```vue
<template>
  <section class="util__container">
    <div v-editable="story.content" class="blog">
      <h1>{{ story.content.name }}</h1>
      <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 () {
    // Load the JSON from the API
    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()
      }
    })
  },
  async asyncData (context) {
    return await context.app.$storyapi.get('cdn/stories/home', {
      version: 'draft'
    }).then((res) => {
      return res.response
    }).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>

<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's API can list all content items of a specific folder with the parameter `starts_with`. The number of content items you get back is by default 25 but you can change that with the `per_page` parameter and jump to the other pages with the `page` parameter.

pages/_language/blog/index.vue

```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 folder

After creating the Vue.js components for showing the blog we need to create a new folder in Storyblok to create the blog pages.

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

![creating the blog folder](https://img.storyblok.com/81Mb61r9TS2y0jAkU0nFlG2qNb4=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/51376/2872x1426/54ce7cb432/asset-create_new_entryblog.png)

### Create the blog article

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

![Creating the first blog entry](https://img.storyblok.com/7F1dmTfGP4D1N3ZXjE-3NpBNqG8=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/51376/2878x1434/a9851ddbee/asset-new-article-blog.png)

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

![Blog overview](https://img.storyblok.com/rVi3FSZqT-GD4vZycynGvwmfElo=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/39898/2220x1288/ae4d4eaa1e/mywebsite-2018-01-11-11-05-17.jpg)

Build a sitemap
------------------------------------------------------------------------------------

To generate a sitemap or navigation tree with Nuxt.js of all our pages we will call [Storyblok's links API](https://www.storyblok.com/docs/Delivery-Api/Links). The API includes the parent-child relationships through the `parent_id` and therefore we just need to generate a tree using a computed property.

pages/_language/sitemap.vue

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

```

To the sitemap as a tree with infinite nodes we create a `SitemapItem.vue` component and include itself when looping over the children of the tree.

components/SitemapItem.vue

```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 forget to add the new SitemapItem component to your `components.js` file.

plugins/components.js

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

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

At the end, we should have the following page.

![Nuxt.js Sitemap](https://img.storyblok.com/6YHeK8jVqYoNE8m7HpmcQKpOt0Y=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/39898/1946x1128/b37db40399/mywebsite-2018-01-11-15-17-34.jpg)

Adding another language
--------------------------------------------------------------------------------------------

With Storyblok you have two options to make multi-language projects - 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](https://www.storyblok.com/docs/internationalization).

![Adding language in Storyblok](https://img.storyblok.com/gZ4hjjq4DPEUBXm-eGrNu-ohDlg=/840x0/filters:filters:fill(FFFFFF):filters:format(jpeg)/f/39898/999x593/e856cf3d2e/field-level-translations.jpg)

Deploy to live
-----------------------------------------------------------------------------------

[Now](https://zeit.co/now) it's time to show your project to the world.

For easy, zero configuration, deployment you can use [now](https://zeit.co/now). After you have downloaded and installed their [desktop application](https://zeit.co/download) you can deploy Nuxt.js with a single command.

```bash
now
```

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

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 a little bit like 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.