7/19/2021

Nuxt Content & Nuxt i18n

#code#nuxt#markdown

When you manage a website which is being visited by people from multiple countries, it is a good idea to offer you content in multiple languages. This way you make your content accessible for as many people as possible without them being depenedent on external tools such as Google Translate. To make this possible for this website @Nuxt/Content is being used to write and display the content and nuxt-i18n to make the content availble in multiple languages.

What is Nuxt Content?

@Nuxt/Content is a NuxtJS module which you can use to easily use Git as an headless CMS to build a blog or case study archive for your website. This means that instead of a CMS like Wordpress or Strapi where you would write your content normally, you can simply write the article in Markdown (a .md file), add this file to your Git repository and your NuxtJS app can retrieve the content as if it communicates with a fullblown CMS. This makes for an easy to maintain compact web app where the layout and content live in one place.

What is Nuxt i18n?

i18n or internationalization is the process where the structure of a website is designed in such a way that the content being made for the website can easily be presented in multiple different languages. Making the content itself ready for different locales is also known as localization or i10n. nuxt-i18n is a NuxtJS module that will take of the heavy lifting for us. This module will add the correct country code to the URL ('/nl', '/en' etc.), detect the visitors language based on the browser and will do SEO optimizations. Together with the correct app structure and a translation of the content on this modules makes it easy to localise the website.

The project

Setup NuxtJS

First of all we need to setup a new NuxtJS project. Open your favorite CLI and make sure that Node and NPM are installed. Now you can run the following command to start the setup process:

npm init nuxt-app <project-name>

Now you need to answer a number of questions for the setup (name, Nuxt options, UI framework, TypeScript, linter, testing framework, etc), choose the option you need for your project.

Now navigate to the new folder with your project files and start the development server:

cd <project-name>
npm run dev

Your Nuxt web app is now live at http://localhost:3000 πŸš€

Setup the @nuxt/content module

Now that the basis is built, we can install the @Nuxt/Content module

npm install @nuxt/content

After the instalation is complete you have to add the module to the nuxt.config.js file

{
  modules: [
    '@nuxt/content'
  ],
  content: {
    // Options
  }
}

Setup the nuxt-i18n module

Now we can install the nuxt-i18n modules

npm install nuxt-i18n

This also has to be added to the nuxt.config.js file

{
  modules: [
    '@nuxt/content',
    'nuxt-i18n'
  ],
  
  i18n: {
    // Options  
  },
}

Nuxt/Content & Nuxt-i18n

Static content

To start we first need to configure nuxt-i18n for the different languages we want to support. In this case it's Dutch πŸ‡³πŸ‡± and English πŸ‡ΊπŸ‡Έ where Dutch will be the default language. Add the following settings to nuxt-config.js:

{
  modules: [
    '@nuxt/content',
    'nuxt-i18n'
  ],
  
  i18n: {
    defaultLocale: 'nl',
    locales: [{
      code: 'en',
      iso: 'en-US',
      file: 'en-US.json'
    },
    {
      code: 'nl',
      iso: 'nl-NL',
      file: 'nl-NL.json'
    }
    ],
    langDir: 'locales/',
    vueI18n: {
      fallbackLocale: 'nl'
    } 
  },
}

Here we set Dutch and English as the two "locales", both with their own country code, iso code and a reference to a .json file. This file contains the translations of the static pages of the website such as the homepage and the contact page. Translations for non-static pages, such as blog posts, will be saved in a different location. We will get back to those.

Now that the nuxt-i18n options have been updated we can create a new folder in the project root named locales containing the .json files for the static English and Dutch text.

.
β”œβ”€β”€ .github
β”œβ”€β”€ .nuxt
β”œβ”€β”€ components
β”œβ”€β”€ content
β”œβ”€β”€ locales <--- This folder
β”‚   β”œβ”€β”€ en-US.json <--- English text
β”‚   └── nl-NL.json <--- Dutch text
β”œβ”€β”€ node_modules
β”œβ”€β”€ pages
β”œβ”€β”€ static
β”œβ”€β”€ store
β”œβ”€β”€ .editconfig
β”œβ”€β”€ .eslintrc.js
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .prettierrc
β”œβ”€β”€ jsconfig.json
β”œβ”€β”€ nuxt.config.js
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
└── .README.md

Now open the en-US.json file and add the following:

{
    "welcome": "Hello, World! 🌍"
}

Do the same for the nl-NL.json file:

{
    "welcome": "Hallo, Wereld! 🌍"
}

Now that the translation file for the statich page are finished, we can start using them. Start by deleting components/Tutorial.vue. Next, open pages/index.vue and replace here the <Tutorial /> component by <h1>{{ $t('welcome') }}</h1>.

If you now start the development server and navigate to http://localhost:3000 you should see "Hallo, Wereld! 🌍"! Not just that, if you navigate to http://localhost:3000/en you should see "Hello, World! 🌍". The static content is now availble in both languages!

Dynamic content - Individual blog posts

Besides the internationalization of the static content, it is also important to offer the dynamic content in multiple languages. In the case of Studio Terabye these are the cases studies and the blog posts (the one you are reading now πŸ˜‰).

To make use of Nuxt/Content to write the dynamic content you first need to create a /content directory. Within you need to create the following file structure:

.
β”œβ”€β”€ .github
β”œβ”€β”€ .nuxt
β”œβ”€β”€ components
β”œβ”€β”€ content <--- This folder
β”‚   β”œβ”€β”€ en <--- A folder per language
β”‚   β”‚   └── blog <--- A folder for the content catagory
β”‚   β”‚       └── blog-post.md <--- A file for the blog post
β”‚   └── nl <--- A folder per language
β”‚       └── blog <--- A folder for the content catagory
β”‚           └── blog-post.md <--- A file for the blog post
β”œβ”€β”€ locales
β”‚   β”œβ”€β”€ en-US.json
β”‚   └── nl-NL.json
β”œβ”€β”€ node_modules
β”œβ”€β”€ pages
β”œβ”€β”€ static
β”œβ”€β”€ store
β”œβ”€β”€ .editconfig
β”œβ”€β”€ .eslintrc.js
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .prettierrc
β”œβ”€β”€ jsconfig.json
β”œβ”€β”€ nuxt.config.js
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
└── .README.md
  • The /content folder is where Nuxt/Content will look for the articles we've written
  • The /en en /nl folders are used to keep the languages seperate
  • The /blog folder is for the specfic content we want to write
  • The blog-post.md file is the actual blog post that will be visible on the website

Now open the blog-post.md file in the /en folder and add the following:

---
title: The first post
short: This is the first blog post
tags:
  - content
  - i18n
  - markdown
---
## This is the first blog post on your brand new website in multiple languages

Now open the blog-post.md file in the /nl folder and add the following:

---
title: De eerste post
short: Dit is de eerste blog post
tags:
  - content
  - i18n
  - markdown
---
## Dit is de eerste blog post op je gloednieuwe website in meerdere talen

The structure is now ready to support blog posts in multiple languages, however they still need their own page. For this we need a new folder and two new files:

.
β”œβ”€β”€ .github
β”œβ”€β”€ .nuxt
β”œβ”€β”€ components
β”œβ”€β”€ content 
β”‚   β”œβ”€β”€ en 
β”‚   β”‚   └── blog 
β”‚   β”‚       └── blog-post.md 
β”‚   └── nl 
β”‚       └── blog 
β”‚           └── blog-post.md 
β”œβ”€β”€ locales
β”‚   β”œβ”€β”€ en-US.json
β”‚   └── nl-NL.json
β”œβ”€β”€ node_modules
β”œβ”€β”€ pages
β”‚   └── blog <-- This folder
β”‚       β”œβ”€β”€ _slug.vue <-- This file
β”‚       └── index.vue <-- This file
β”œβ”€β”€ static
β”œβ”€β”€ store
β”œβ”€β”€ .editconfig
β”œβ”€β”€ .eslintrc.js
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .prettierrc
β”œβ”€β”€ jsconfig.json
β”œβ”€β”€ nuxt.config.js
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
└── .README.md

The pages/blog folder in combination with the index.vue file tells NuxtJS that we want a page for www.example.com/blog. The _slug.vue file in the same folder tells NuxtJS that we also want to create a page for the individual blog posts like www.example.com/blog/blog-post where _slug.vue represents the individual posts

Now open the _slug.vue file and add the following code:

<template>
  <div>
    <h1>{{ post.title }}</h1>
    <nuxt-content :document="post" />
  </div>
</template>

<script>
export default {
  name: 'BlogSlug',
  async asyncData({ $content, params, app, error }) {
    const post = await $content(app.i18n.locale, 'blog', params.slug)
      .fetch()
      .catch(() => {
        error({ statusCode: 404, message: 'Page not found' })
      })
    return { post }
  },
}
</script>

Okay, let's go through the code. The first part, between the <template> tags is responsible for what is visisble on screen. In this case there is an <h1> tag (a heading) which shows the title of the blog post by retriving it from the post object. Below this tag is another tag which you might not recognize: <nuxt-content :document="post" />. This is a tag which lets the @Nuxt/Content module know that we want to show the blog post content here.

The second part of the code, between the <script> tags, is the code responsible for retrieving the blog post itself. First we define the name of the file so that it can be used internally if needed: name: 'BlogSlug',.

Next we use the asyncData function which is called by NuxtJS when the page is being loaded. Within this function we can access the /content folder by using the $content variable, which is automatically availble. Here we pass the country code of the locale which is currently selected (nl/en) via app.i18n.locale, the name of the folder where we want to look for blog content via blog and the URL parameters (www.example.com/blog/this-text-here) via params.slug. Now that all the data that we want to retrieve has been filled in, we fetch the blog post data via the .fetch() function. Possible errors are caught and handled by the .catch() function.

Now start the development server and in your browser navigate to http://localhost:3000/blog/blog-post. You should see your fist blog post! πŸŽ‰ If you want to see the English version you can navigate to http://localhost:3000/en/blog/blog-post.

Dynamic content - All blog posts

It's great that we can show the individual blog posts, now your visitors can read your content, but an overview page for your blog posts can't be missing. This will help your visitors discover your content and will make the website more clear.

Open now the index.vue file and add the following code:

<template>
  <div>
    <div v-for="(post, index) in posts" :key="index">
      <p>{{ post.short }}</p>
      <nuxt-link :to="localePath(post.path)">LINK</nuxt-link>
    </div>
  </div>
</template>

<script>
export default {
  name: 'BlogOverview',
  async asyncData({ $content, app, error }) {
    const posts = await $content(app.i18n.locale, 'blog')
      .only(['short', 'path'])
      .sortBy('createdAt', 'asc')
      .fetch()
      .catch(() => {
        error({ statusCode: 404, message: 'Page not found' })
      })

    return {
      posts: posts.map((posts) => ({
        ...posts,
        path: posts.path.replace(`/${app.i18n.locale}`, ''),
      }))
    }
  },
}
</script>

The code above looks a lot like the code we've used to retrieve the individual blog posts, in both cases we retrieve blog posts data, but there are enough differences here for it to be worth it to go through the code again. Let's start again with the code between the <template> tags. To prevent that we need to manually add a seperate piece of code for each blog post that we want to show on the overview page, we will build the layout dynamically. We do this by using a for loop: <div v-for="(post, index) in posts" :key="index">. This will get all the posts we've retrieved and makes a <div> element for each one automatically. This way it doesn't matter how many blog posts you write, they will automatically be displayed. Now that we have an element on the page for each blog post, something should of course be displayed there. In this case we show two things: a short summary of the post (the short) and a link to the post itself:

<!-- The short of the post -->
<p>{{ post.short }}</p> 
<!-- The link to the post -->
<nuxt-link :to="localePath(post.path)">LINK</nuxt-link>

As you can see in the code we call the localePath function before we give the post URL to the <nuxt-link> tag. This function will take care of adjusting the link so that it matched the currently selected language. So if the selected website language is English, /en will be added to the URL.

The piece of code between the <script> tags is, just as the code for the individual blog posts, responsible for retrieving the blog posts data. We make use of the asyncData function provided by NuxtJS to do this. The differences here are the .only(['short', 'path']) function which indicates what blog specific data we want to retrieve and the .sortBy('createdAt', 'asc') function wich sorts the blog posts in ascending order based on the creation date.

Finally we take the blog posts which have been retrieved and remove the locale from the URL (/nl or /en). We do this because @Nuxt/Content returns that URL to the blog post as www.example.com/nl/blog/post-naam which isn't a valid URL because in our settings we've indicated that we don't want to show /nl since this is the default language.

Now start the development server and open http://localhost:3000/blog in your browser. You should see an overview with your first blog post! πŸŽ‰ If you want to see the English version you can navigate to http://localhost:3000/en/blog.

Language switcher

Now that we have an overview of the blog posts and the individual blog posts themselves availble in multiple languages, we are only missing the option to easiliy switch between the languages. To build this functionality we need a new component. Add LanguageSwitcher.vue to the /components folder:

.
β”œβ”€β”€ .github
β”œβ”€β”€ .nuxt
β”œβ”€β”€ components
β”‚   └── LanguageSwitcher.vue <-- This file
β”œβ”€β”€ content 
β”‚   β”œβ”€β”€ en 
β”‚   β”‚   └── blog 
β”‚   β”‚       └── blog-post.md 
β”‚   └── nl 
β”‚       └── blog 
β”‚           └── blog-post.md 
β”œβ”€β”€ locales
β”‚   β”œβ”€β”€ en-US.json
β”‚   └── nl-NL.json
β”œβ”€β”€ node_modules
β”œβ”€β”€ pages
β”‚   └── blog <-- Deze map
β”‚       β”œβ”€β”€ _slug.vue
β”‚       └── index.vue
β”œβ”€β”€ static
β”œβ”€β”€ store
β”œβ”€β”€ .editconfig
β”œβ”€β”€ .eslintrc.js
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .prettierrc
β”œβ”€β”€ jsconfig.json
β”œβ”€β”€ nuxt.config.js
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
└── .README.md

Now open LanguageSwitcher.vue and add the following code:

<template>
  <div>
    <nuxt-link v-if="$i18n.locale !== 'en'" :to="switchLocalePath('en')">
      EN
    </nuxt-link>
    <nuxt-link v-if="$i18n.locale !== 'nl'" :to="switchLocalePath('nl')">
      NL
    </nuxt-link>
  </div>
</template>

<script>
export default {
  name: 'LanguageSwitcher',
}
</script>

Between the <template> tags you will see that we've added two nuxt-link tags, both with a v-if condition. The first nuxt-link will change the current page to the English version and will only be visible if the current locale isn't English. The second nuxt-link change the current page to the Dutch version and is only visisble if the current locale isn't Dutch.

To show this link above each page we need to create a new folder, /layouts containing a new file default.vue:

.
β”œβ”€β”€ .github
β”œβ”€β”€ .nuxt
β”œβ”€β”€ components
β”‚   └── LanguageSwitcher.vue 
β”œβ”€β”€ content 
β”‚   β”œβ”€β”€ en 
β”‚   β”‚   └── blog 
β”‚   β”‚       └── blog-post.md 
β”‚   └── nl 
β”‚       └── blog 
β”‚           └── blog-post.md 
β”œβ”€β”€ layouts <-- This folder
β”‚   └── default.vue <-- This file
β”œβ”€β”€ locales
β”‚   β”œβ”€β”€ en-US.json
β”‚   └── nl-NL.json
β”œβ”€β”€ node_modules
β”œβ”€β”€ pages
β”‚   └── blog
β”‚       β”œβ”€β”€ _slug.vue
β”‚       └── index.vue
β”œβ”€β”€ static
β”œβ”€β”€ store
β”œβ”€β”€ .editconfig
β”œβ”€β”€ .eslintrc.js
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .prettierrc
β”œβ”€β”€ jsconfig.json
β”œβ”€β”€ nuxt.config.js
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
└── .README.md

default.vue indicates what the layout of the website should look like. This way you can for example easily add a menu or a footer to each page without copying the code to all the individual pages.

Now open default.vue and add the following code:

<template>
  <div>
    <LanguageSwitcher />
    <Nuxt />
  </div>
</template>

Here we tell NuxtJS that for every page we first want to show the language switcher component and below the rest of the content.

Now start the development server and navigate to http://localhost:3000/blog in your browser. You should see EN above the page. When you click on this the language, and thus the content on the page, will be automatically changed to English!

Congratulations, you now have a basis for a modern multi-language website! πŸŽ‰

Credits