Multi-Language Blog with Nuxt content module.
Tags: nuxt, webdev, tutorial, i18n
Published: 2020-07-02 11:45:45

Last week I managed to refactor my personal portfolio done using Nuxt [alvarosaburido.com](https://alvarosaburido.com/) with 3 major features that the awesome team from [Nuxt.js](https://twitter.com/nuxt_js) released recently:

- Nuxt Full Static `v2.13`
- `@nuxt/content` module [here](https://content.nuxtjs.org/)
- `@nxut/components` module [Github repo](https://github.com/nuxt/components)

The most game-changing is the `Content Module` that allows you to `fetch` your markdown files through a MongoDB like API, acting as a **Git-based Headless CMS**.

Today in this tutorial I gonna help you create your own multi-language blog using `@nuxt/content`.

## Setup

Let's create our new project using the official scaffolding tool `create-nuxt-app`.

With [yarn](https://yarnpkg.com/en/):

```bash
yarn create nuxt-app nuxt-i18n-blog
```

It will ask you some config questions, when you prompt with Nuxt.js modules option, you can select the content module directly:

![Nuxt-create-app](https://res.cloudinary.com/alvarosaburido/image/upload/v1593414669/blog/Multi-language%20blog%20with%20Nuxt%20content/nuxt-create-app_rmwdr6.png)

Note: I will be using [TailwindCSS](https://tailwindcss.com/) to prototype faster the components used in this tutorial, if you need more info about it here are two nice articles about it: [Building my site with Tailwind CSS](https://dev.to/samanthaming/building-my-site-with-tailwind-css-5787) & [How to use Tailwind CSS with Nuxt.js](https://regenrek.com/posts/how-to-use-tailwind-css-with-nuxt/)

After all the installation:

```bash
cd nuxt-i18n-blog

npm run dev
```

## Nuxt-i18n

To do a multi-language blog the first thing you need is to add `i18n` (Internationalization) module to your app. Of course. Nuxt already has an excellent module [Nuxt-i18n](https://github.com/nuxt-community/nuxt-i18n) which handles all the heavy lifting such as automatically generate routes prefixed with locale code and SEO support.

```bash
yarn add nuxt-i18n
```

Then add the module to `nuxt.config.js`:

```javascript
{
  modules: [
    [
      'nuxt-i18n',
       { /* module options */ }
    ]
  ],

  // Or with global options
  i18n: {}
}
```

Now let's add the languages we want to support in the config, for this exercise we will create content in English (default), Spanish & French.

```javascript
{
  modules: [
    [
      'nuxt-i18n',
       {
         locales: [
           {
              code: 'es',
              iso: 'en-ES',
              name: 'Español',
            },
            {
              code: 'en',
              iso: 'en-US',
              name: 'English',
            },
            {
              code: 'fr',
              iso: 'fr-fr',
              name: 'Français',
            },
         ],
         defaultLocale: 'en',
         noPrefixDefaultLocale: true,
       }
    ]
  ],
}
```

The `noPrefixDefaultLocale` means that the module won't generate local prefixed routes, example `defaultLocale: 'en'` `yourblog.com/en/articles` would be `yourblog.com/articles` instead.

Next thing is to add the `json` files for the locale messages, under `locales` folder and we add them to the `nuxt-config.js` like this:

```javascript
'nuxt-i18n',
  {
    //  locales: [..].
    //
        vueI18n: {
      fallbackLocale: 'en',
      messages: {
        en: require('../locales/en-us.json'),
        es: require('../locales/es-es.json'),
        fr: require('../locales/fr-fr.json'),
      },
    },
  },
```

## Content

Before digging in the directory structure for a multi-language blog that I used for my [portfolio](https://alvarosaburido.com/blog), I must say there is no standard way to do it, my recommendations are based on previous experience working with CMS technologies for the past 6 years and how they organize the content for several languages.

For the sake of this tutorial I recommend you follow the same structure so, it makes sense and is easy to follow, but for your own blog find the best way that adapts to your preferences and use cases.

The core idea is to have routes following:

```
<domain>/<local>/<parent-route>/<child-route>
```

Like for example `nuxt-i18n-blog/es/blog/code-article` . It mimics the structure in the `content` directory so everything comes together smoothly when we proceed to fetch the data.

```
nuxt-i18n-blog
└── 📁 content
    ├── 📁 en
    │   └── 📁 blog
    │       └── 📄 my-first-article.md
            └── 📄 my-second-article.md
    ├── 📁 es
    │   └── 📁 blog
    │       └── 📄 mi-primer-articulo.md
    ├── 📁 fr
    │   └── 📁 blog
    │       └── 📄 mon-premier-article.md
    └── 📁 ca
        └── 📁 blog
            └── 📄 meu-primer-article.md
```

The cool thing is that `@nuxt/content` supports `FrontMatter` by default, meaning you can add metadata to your article like this:

```markdown
---
title: My first article
category: web
description: Step by step tutorial on how to stop being sad and being awesome instead.
media: https://images.unsplash.com/photo-1592500103620-1ab8f2c666a5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=3000&q=80
---

Your content
```

For the article's media images, I found a really cool account inside [unsplash.com](https://unsplash.com) called [@morningbrew](https://unsplash.com/@morningbrew) with awesome quality images that will look nice as placeholders for your tests.

## The Blog

So now that we have configured the **locales** and the **content structure**, we can start creating our pages. The idea will be to have:

- `/blog` view containing our blog feed.
- A header with language switching links
- `/blog/_slug` with the article itself.

![Blog Feed](https://res.cloudinary.com/alvarosaburido/image/upload/v1593511852/blog/Multi-language%20blog%20with%20Nuxt%20content/blog-feed_odjyjj.png)

Let's dig in.

### Feed

For the blog page, under `/pages` create a `blog/index.vue` (In Nuxt, instead of creating `Blog.vue` we create a `index.vue` inside of a directory called `blog`, as it's explained [here](https://nuxtjs.org/guide/routing/))

```javascript
//  blog/index.vue

<script>
export default {
  name: 'Blog',
  async asyncData(context) {
    const { $content, app } = context;
    const posts = await $content(`${app.i18n.locale}/blog'`).fetch();

    return {
      posts,
    }
  },
}
</script>
```

The `asyncData` is called every time before loading the page component, it receives [the context](https://nuxtjs.org/api/context). It's pretty handy when it comes to fetch some data before the lifecycle hooks.

Inside the context, you will notice there is a `$content` instance, (Same as `this.$content` if you decide to use it outside of `asyncData`).

```javascript
const posts = await $content(`${app.i18n.locale}/blog'`).fetch();
```

Since is an async function, you need to add `await`, it accept a `path` which is essentially the same path structure we used for the route.

You might think, why not use the route inside `the context` instead of computing the route using the current locale?. You can totally use `await $content(context.route.path).fetch()` but you will need to **deactivate** the `noPrefixDefaultLocale` in the `nuxt.config.js` because you will not have the locale prefix in the route for the default locale. Or, remove the `currentLocale` directory inside of `/content` directory, but is not really scalable in case you want to change you default locale later.

There are a lot of cool options like filtering and sorting methods to use along `fetch()` so make sure you check them out [here](https://content.nuxtjs.org/fetching) 😜.

As a result we will have an array with all the articles fetched, the cool thing is that you will have all the metadata from your `.md` as properties in each post instance.

![Content fetch using @nuxt/content](https://res.cloudinary.com/alvarosaburido/image/upload/v1593518425/blog/Multi-language%20blog%20with%20Nuxt%20content/content-fetch_gmelfg.png)

![Excited](https://media.giphy.com/media/Ge86XF8AVY1KE/source.gif)

Now let's add a template to show some cards with the articles we just fetch

```javascript
<template>
  <div class="blog container mx-auto">
    <section class="grid grid-cols-3 gap-4 pt-12">
      <article
        class="post max-w-sm rounded overflow-hidden shadow-lg flex flex-col"
        v-for="(post, $index) in posts"
        :key="`post-${$index}`"
      >
        <img class="w-full" :src="post.media" :alt="post.title" />
        <div class="px-6 py-4 flex-2">
          <h3>{{ post.title }}</h3>
          <p class="text-gray-700 text-base">
            {{ post.description }}
          </p>
        </div>
        <footer class="p-4">
          <nuxt-link :to="post.path" class="font-bold text-xl mb-2">
            <button :to="post.path" class="btn btn-teal">
              {{ $t('read-more') }}
            </button>
          </nuxt-link>
        </footer>
      </article>
    </section>
  </div>
</template>
```

PD: For the sake of this tutorial, I added a second article in the default locale to actually see more posts on the Blog page so it looks better.

### Language Switching

Under the `/components` directory, create a `Header.vue`:

```javascript
<template>
  <header class="topnav h-54px p-2 border-b-2 border-gray-200">
    <div class="container mx-auto flex justify-between items-center">
      <div class="right flex justify-start items-center">
        <Logo class="w-8 h-8 mr-3" />
        <h1 class="text-lg font-bold text-gray-700">{{ title }}</h1>
      </div>
      <div class="left self-end">
        <ul class="flex lang-switch justify-around">
          <!-- ...Here goes the language-switch -->
        </ul>
      </div>
    </div>
  </header>
</template>
```

What is pretty important is what it's inside of the `language-switch` using `switchLocalePath('en')` in the `:to` prop for `<nuxt-link>`

<!--prettier-ignore-start-->

```html
<ul class="flex lang-switch justify-around">
  <li v-if="$i18n.locale !== 'en'">
    <nuxt-link class="text-md ...":to="switchLocalePath('en')">EN</nuxt-link>
  </li>
  <li v-if="$i18n.locale !== 'es'">
    <nuxt-link class="text-md ..." :to="switchLocalePath('es')">ES</nuxt-link>
  </li>
  <li v-if="$i18n.locale !== 'fr'">
    <nuxt-link class="text-md ..." :to="switchLocalePath('fr')">FR</nuxt-link>
  </li>
</ul>
```
<!--prettier-ignore-end-->

Then this component is ready to be added globally in `layouts/default.vue` just above the `<Nuxt />`. This way you will have it available in all your routes that use this layout.

```javascript
<template>
  <div>
    <Header />
    <Nuxt />
  </div>
</template>

<script>
export default {
  name:'default'
}
</script>
```

Try clicking each link to see how your route changes to the selected locale. You should see how the page shows the correct posts for each language like this:

![Switching Languages](https://res.cloudinary.com/alvarosaburido/image/upload/v1593527685/blog/Multi-language%20blog%20with%20Nuxt%20content/switching-language_a35bnk.gif)

If we click any of the posts **Read more** buttons we will be routed to a child page that we are going to build in the next section. But before continuing there is a catch.

When we define the button:

```html
<nuxt-link :to="post.path" class="font-bold text-xl mb-2">
  <button :to="post.path" class="btn btn-teal">
    {{ $t('read-more') }}
  </button>
</nuxt-link>
```

We are passing the **Post's** path directly, if you are using `noPrefixDefaultLocale`, `@nuxt/content` will still give you the path with the corresponding locale prefix (this is because we build our content directory this way) so it will try to access a route it doesn't exist whenever you are in the default locale, leading to a page like this:

![Page not found](https://res.cloudinary.com/alvarosaburido/image/upload/v1593623249/blog/Multi-language%20blog%20with%20Nuxt%20content/page-not-found_y6uawr.png)

Don't worry, it's pretty easy to solve. Go back to the `Blog` component and map the posts returning from the `$content` fetch like this:

```javascript
const defaultLocale = app.i18n.locale;
const posts = await $content(`${defaultLocale}/blog`).fetch();

return {
  posts: posts.map(post => ({
    ...post,
    path: post.path.replace(`/${defaultLocale}`, ''),
  })),
};
```

This should be enough to replace all the post paths containing the default locale.

## Content Page

Now comes the fun part 😜. Until now we have created the basic structure and config + our `Blog` page and a nice language switcher, it's time to create the actual content page for our article.

Inside of `pages/blog/` create a folder `_slug` with an `index.vue` inside:

```javascript
<script>
export default {
  name: 'Post',
  async asyncData(context) {
    const { $content, params, app, route, redirect } = context;
    const slug = params.slug;
    const post = await $content(`${app.i18n.locale}/blog`, slug).fetch();

    return {
      post,
    }
  },
}
</script>
```

Similar to what we did on the Blog component, we fetch the post trough a `$content` instance, but this time passing the slug (`es/blog/<slug>`) obtained from the route parameters.

For the template, we have an special component called `<nuxt-content />` that requires the post to pass using the prop `:document` like this:

```html
<template>
  <div class="container mx-auto pt-6">
    <article v-if="post">
      <nuxt-content class="text-gray-800" :document="post" />
    </article>
  </div>
</template>
```

`<nuxt-content>` component will automatically add a `.nuxt-content` class, you can use it to customize your styles:

```css
.nuxt-content h1 {
  /* my custom h1 style */
}
```

To add more juice to the content page let's take advantage of the props available using `Frontmatter`.

```html
<template>
  <div class="container mx-auto pt-6">
    <article v-if="post">
      <header class="grid grid-cols-2 gap-4 mb-12 rounded shadow-lg p-4">
        <img :src="post.media" alt="post.title" />
        <div class="">
          <h2 class="text-lg font-bold text-gray-800 mb-2">{{ post.title }}</h2>
          <p class="text-sm text-gray-700">
            {{ $t('published-at') }} {{ getDate }}
          </p>
        </div>
      </header>
      <nuxt-content class="text-gray-800" :document="post" />
    </article>
  </div>
</template>
```

This will give us a nice header with the image, the title, and the `createdAt` we get from a computed property called `getDate`, for this, `date-fns` will be our best friend-

```javascript
import { format } from 'date-fns';

const computed = {
  getDate() {
    return format(new Date(this.post.createdAt), 'dd/MM');
  },
};
```

![Post article](https://res.cloudinary.com/alvarosaburido/image/upload/v1593620028/blog/Multi-language%20blog%20with%20Nuxt%20content/post-article_q06e6w.png)

But how about metadata? Nuxt.js uses [vue-meta](https://github.com/nuxt/vue-meta) under the hood to update `headers` and `html attributes` in each page.

To do this in the Content Page add the `head` method to your component, take notice of the `htmlAttrs` `lang` where we set the current locale. The rest is just Open Graph meta.

```javascript
export default {
  name: 'post',
  head() {
    return {
      title: this.post.title,
      htmlAttrs: {
        lang: this.$i18n.locale,
      },
      meta: [
        {
          hid: 'og:description',
          property: 'og:description',
          content: this.post.description,
        },
        {
          property: 'og:title',
          hid: 'og:title',
          content: this.post.title,
        },
        {
          hid: 'og:image',
          property: 'og:image',
          content: this.post.media,
        },
      ],
    };
  },
};
```

Inspecting the social preview of your page (I use a chrome extension called [Social Share Preview](https://chrome.google.com/webstore/detail/social-share-preview/ggnikicjfklimmffbkhknndafpdlabib)) we can see the metadata is correctly working with the post information.

![](https://res.cloudinary.com/alvarosaburido/image/upload/v1593662474/blog/Multi-language%20blog%20with%20Nuxt%20content/Social_preview_ycvtrq.png)

### Language switching inside post

But wait? What happens if I try to change the language when I'm inside of the `Content Page`? Yep, you guessed right, I will not work, and here is why.

If you remember well, we use `<nuxt-link class="text-md ..." :to="switchLocalePath('es')">ES</nuxt-link>` for the `language-switcher` which will take the current path and change the prefix of the route. The problem is that if you change `blog/my-first-article` to, let's say, Spanish, the route would be `/es/blog/my-first-article`, this route doesn't exist in our content structure because we the slug corresponds to the file name, so for Spanish version should be `/es/blog/mi-primer-articulo`.

There are many strategies to solve this:

- Adding the other languages routes in the `Frontmatter` of each article and use a `.catch()` when fetching data to redirect to current locale version.
- Adding a `uid` in the `Frontmatter` unique for all the locales version of the article, and then use this to fetch the correct article
- Same as the first one but instead of using catch, just hide the language switcher on Content Pages and add the links manually on the post.

For my blog I opted for the second solution which I think is cleaner, **step 1** you just need to add in your `Header` component a condition to hide at content pages (you can detect it by checking if the route has a slug param).

```javascript
// in the template
<template>
  <ul class="flex lang-switch justify-around" v-if="!isContentPage">
    <!-- ...-->
  </ul>
</template>;

// in the component script
export default {
  name: 'Header',
  data,
  computed: {
    isContentPage() {
      return this.$route.name.includes('slug');
    },
  },
};
```

In your articles `Frontmatter` section, add an array of objects containing the paths of the other locales versions of the article

```
//content/en/blog/my-first-article.md

title: My first article
category: web
description: Step by step tutorial on how to stop being sad and being awesome instead.
media: https://images.unsplash.com/photo-1592500103620-1ab8f2c666a5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=3000&q=80
otherLanguages:

- locale: es
  path: /es/mi-primer-articulo
- locale: fr
  path: /fr/mon-premier-article

---
```

Next let's add the logic to `pages/_slug/index`

```javascript
// in the template
<template>
<!-- ... -->
  <p class="text-sm text-gray-700">
    {{ $t('also-available-in') }}
    <nuxt-link
      class="uppercase text-teal-600 hover:text-teal-800"
      v-for="lang in otherLanguages"
      :key="lang.locale"
      :to="lang.path"
    >
      {{ lang.locale }}
    </nuxt-link>
  </p>
<!-- ... -->
</template>

// in the component script
export default {
  name: 'Header',
  data,
  computed: {
    // ...
    otherLanguages() {
      return this.post.otherLanguages || []
    },
  }
}
```

And voilá. Now you have a nice multi-language blog.

![Change language inside post](https://res.cloudinary.com/alvarosaburido/image/upload/v1593665309/blog/Multi-language%20blog%20with%20Nuxt%20content/change-language-inside-post.gif)

If you enjoy the article, you have questions or any feedback let's talk in the comments 💬.

Happy coding!.