Vue Static Site Generator with Nuxt and Mardown. Let's Create a Serverless Blog. Part 2


This website you are visiting have been created using Nuxt and Markdown and is serving SEO-friendly blog posts without the need of a server. Want to learn how to do it? Keep reading...


Published date: March 24 2020

In Part 1 of this article we talked about what are static site generators and what are the advantages of using them when working with Javascript frameworks, we learned the basics of Nuxt, created our nuxt project and added a navbar and a footer component, learned how to use sass, and started to create our serverless blog using Markdown and FrontMatter.

In this part we will add internationalization.

If you don't need a multilingual site, you can go to Part 3 where I'm going to create the site home page or, if you are only interested in how to build the blog, feel free to jump to Part 4

Or you can go directly to Part 5 if you want to know how to generate and deploy the static site.

Index

Complete Project Code: You can get the source code on GitHub

Prerequisite: HTML, CSS, JavaScript, Vue.js, and Markdown.

Internationalization with nuxt-i18n

I'll have the content of my blog posts written in separate files, one for English and one for Spanish. And for the content of the home page and other small pieces of text, such as buttons or menu items, I'm going to use localization files where I'm going to store the text in English and its Spanish translation.

Let's create those localization files in a new folder called locales and add the translations in json objects.

I'm going to use one single json file for each language because I don’t have a lot of text to translate. To organize those translations, I'm going to group them by area.

nuxt internationalization

As I said in Part 1 of this article of this article, I'm going to use Nuxt-i18n for the internationalization. Let me tell you why again: As the internationalization package I'm used to work with when creating Vuejs app is Vue-i18n, my first though was to use it with this Nuxt app. But I found one main issue. I want to add the language to the the posts' URL in order to have clean URLs from which retrieving the language (something I 'll need in order to call the correct content when the user switches languages). With Vue.i18n we have to do it manually, what is kind of a hazzle. However, Nuxt-i18n provides a function to do this automatically. Other minor reason is that to use Vue.i18n, you have to add it to your project as a plugin, so you have to create the plugin, what it is not a big deal but if you can avoid doing it, much better.

So I decided to use Nuxt-i18n, which is built on top of Vue-i18n and solves those two problems for us. Let's install it with:

npm i nuxt-i18n

Now we can register it as a module in nuxtconfig.json

   [
      'nuxt-i18n',
       { /* module options */ }
    ]

We add an array with the module name and the configuration. We can directly create here the object with the options, but I prefer to add it in an external file for the sake of clarity.

I'm going to create the configuration file in the locales folder and import it to nuxtconfig.json.

const i18nOptions = {
  useCookie: false,
  alwaysRedirect: true,
  locales: [
    {
      code: 'en',
      iso: 'en-US',
      name: 'English',
      file: 'content-en.json'
    },
    {
      code: 'es',
      iso: 'es-ES',
      name: 'Español',
      file: 'content-es.json'
    }
  ],
  lazy: true,
  seo: false,
  langDir: '/locales/',
  defaultLocale: 'en',
  parsePages: false
}
module.exports = {
  i18nOptions
}

One important thing here is to define our locales and reference the files where the translations will be placed.

More about the options in nuxt-i18n page.

nuxt blog nuxt-i18n

Nuxt-i18n is ready to be used in our app.

To show the text form the translation file in the corresponding language, we use $t('property-to -display')

For example, in our home page (pages/index.vue), let's show the title text, by using the $t() method withing the interpolation syntax.

internationalization with nuxt-i18n

As I set my default locale as EN in the nuxt-i18n configuration object, the title will appear in English.

Now we need a language switcher.

Creating a language switcher

All we have to do is create a function that changes the current locale and updates the URL. This is where Nuxt-i18n became a winner over Vue-i18n by providing the switchLocalePath() function.

To create the language switcher, we'll need to have access to the current locale and to all the locales defined in the configuration object. In Nuxt-i18 we can access them like this:

  • this.$i18n.locale (for the current locale) this.$i18n.locales (for all the locales) if we are in a component.
  • $i18n.locale (for the current locales) $i18n.locales (for all the locales) from a template
  • context.app.i18n.locale (for the current locales) and context.app.$i18n.locales (for all the locales) if you have access to Nuxt context.

With this is mind, let's create the language switcher component. I will call it LangSwitcher.vue and place it in the components folder.

<template>
    <div class="lang-switcher" >
      <button class="btn btn-default"
              v-bind:class="{'selected-lang': lang.code === $i18n.locale }"
              v-for="(lang, i) in $i18n.locales" :key="`Lang${i}`"
              @click="changeLanguage(lang.code)">{{lang.name}}
      </button>
    </div>
</template>

<script> export default {   data () {     return {     }   },

methods: {     changeLanguage(lang) {       console.log("CURRRENT LANG: " + this.$i18n.locale)       this.$router.push(this.switchLocalePath(lang));     }   } }; </script>

<style scoped lang="scss">   .lang-switcher {     display: flex;     flex-direction: row;     justify-content: center;   }

.selected-lang {      color: red;   } </style>

The code is pretty straight forward. In the template, we are:

  • Dynamically creating a button for each language in the locales
    v-for="(lang, i) in $i18n.locales" :key="\`Lang${i}\`"
  • Dynamically adding the selected class to the language button whose locale.code equals the current locale ($i18n.locale).
   v-bind:class="{'selected-lang': lang.code === $i18n.locale }"
  • Adding a click event to the button binded to the changeLanguage(lang.code) method to switch the locale
 @click="changeLanguage(lang.code)"

We need to create the chageLanguage(lang.code) method in the JavaScript section. I will use the route's push method to navigate to the url with the selected locale, which we can get with Nuxt's switchLocalePath() method.

  changeLanguage(lang) {
      console.log("CURRRENT LANG: " + this.$i18n.locale)
      this.$router.push(this.switchLocalePath(lang));
  }

In the style, I have just defined a couple of css rules.


Let's now add the Language Switcher component to the default layout because we want it to be displayed in all the pages.

nuxt markdown blog language switcher nuxti18n

Making The Blog multilingual

Now, we need to be able to show the blog in the corresponding language. To do so, we need to dynamically change the blog related urls. Those are blog/index.vue for the blog home page and blog/_slug.vue for the post pages.

Let's start by adding a link to the blog home page in the NavBar component.

To create links in nuxt, we use <nuxt-link>. The link's url in out navigation bar needs to include the locale. And this is where Nuxt-i18 comes very handy again. It provides two useful functions:

  • localePath(), that will add the a locale to the given url.
  • switchLocalePath(), that returns the current url with a given locale. You should be already familiar with this function because it is the one we used in the language switcher to match the locales in the url with the selected language.

The syntax of the nuxt-link should be:

<nuxt-link :to="localePath('index', 'en')">Homepage in English</nuxt-link>
<nuxt-link :to="localePath('index', 'es')">Homepage in Spanish</nuxt-link>

The first parameter is the route name and the second the locale. What we'll do is to pass the current locale using a variable instead of hardcoding it. As you might remember, the variable where the current locale is stored is $i18n.locale, so

 <nuxt-link :to="localePath('index', $i18n.locale)">Blog</nuxt-link>

Actually, you can omit the $i18n.locale, and the current locale will be added to the route.

Let's also add a link to the site's home page. Here we'll need to pass the anchor text with the $t() method to show it in the correct language.

In order to maintain the consistency with the other links in the navbar, add the nav-link class to the nuxt-link element

  <li class="nav-item active">
      <nuxt-link :to="localePath('index')" class="nav-link">
          {{ $t('navbar.home') }}
      </nuxt-link>
  </li>
static site blog

It you haven't done it yet, create the markdown files in your second language:

nuxt multilingual blog

Let's now go the blog's home page file (blog/index.vue).

We already saw how to retrieve the content of all the .md files in the content/EN folder using require.context.

   let postsContent = require.context("~/content/blog/EN/", true, /\.md$/);

Now we need to retrieve also the ones form the content/ES folder. The ideal thing here would be to add the language segment dynamically. The problem is that, at the time of writing this tutorial, we cannot use variables in require.context.

One workaround will be using a conditional structure:

       let postsContent;    
       switch (app.i18n.locale) {
         case 'en':
           postsContent = require.context("~/content/blog/EN/", true, /\.md$/)
           break;
          case 'es':
            postsContent = require.context("~/content/blog/ES/", true, /\.md$/)
           break;
       }
locale in context variable

Let's modify the links to the posts in the template to localize them. What we have now is a regular nuxt-link to the post's slug

nuxt links locale

We need to make this link go to the post in the current language. This is the code

   <nuxt-link  :to="localePath({ name: 'blog-slug', params: { slug: post.attributes.slug } })">
                 {{ post.attributes.title }}
   </nuxt-link>

We are using localePath() passing in an object instead of the route's name. This object contains the name, and the params.

'params' is used to replace dynamic parts of the route, the slug in our case.

For the name, as we don't have custom routes because they are created dynamically, we need to pass the name that Nuxt generates from the url or the app's directory structure, which, according to Nuxt's convention, for blog/_slug would be blog-slug.

nuxt links translation

Now, the links to the posts should match the selected language.

nuxt-i18n blog

But they don't work yet. Before testing the links we need to go back to our _slug.vue page and get the content for both languages. As we have it now, it returns only the EN content:

Replace the hardcoded language in the path with the current locale: ${app.i18n.locale.toUpperCase()}

Don't forget to pass the 'app' property to asyncData()

app.i18n.locale

Now we can navigate the posts in the EN and in ES, but there is one more thing to fix.

If I click on the language switcher from a blog post, the same url is reloaded but adding the corresponding lang-switch. We don’t want that for our blog, we want to load different slugs for each language in order to be able to navigate to the corresponding route. To do it, let's modify our changeLanguage() function.

This is what we have now

 changeLanguage(lang) {
      this.$router.push(this.switchLocalePath(lang));
    }

I plan to do something like:

 if(this.$route.name.includes('blog')){
        this.$router.push(this.localePath({ name: 'blog-slug', params: { slug: post.attributes.slug } }));
  } else{
      this.$router.push(this.switchLocalePath(lang));
 }

For blog, instead of using switchLocale(), I'm using localePath(), which adds the locale to the URL. But here we need to pass the slug in the new language. To do so, I´m going to create a new attribute in the posts' markdown files for the translated slug, and use it here.

nuxt markdown blog

As the _slug page and the LangSwitcher are different components, we have to pass the translated slug from one component to another —Or we can just use another language switcher for the blog.

I'm going to stick to just one language switcher, so I need to share data between components. As the _slug and the LangSwitcher components are not parent-child component, I will use an Event Bus to share the data.

In a regular Vue app we'll place the event bus in main.js. With Nuxt, we need to create a plugin.


A note about plugins

When we want to extend Vue/Nuxt functionality with external libraries/packages or our own functions, we can add them as plugins or as modules. Modules are the easier way. We just install them and then import and use them in any component we want to. But this is only possible if the package exist in the form of a Nuxt module.

If the module does not exist we need to create a plugin. Then, we'll be able to use the plugin anywhere in the app without importing it.

Here you can read more about plugins.


Let's create our event bus plugin. As you may remember from our introduction to Nuxtjs, plugins go in the plugins folder. Let's create there an eventbus.js file and add the following code to create the plugin.

import Vue from 'vue'
// Create the plugin object, which is a new vue instance
const eventBus = new Vue();
// A plugin should expose an install method.
// It is the only required method for a plugin
eventBus.install = function (Vue) {
 // Here we add the $eventbus method, defining it on the prototype so that it is available to
 // all components without needed to be injected
 Vue.prototype.$eventBus = eventBus;
}
// Make the plugin available
Vue.use(eventBus)

Then, we have to register it in the nuxt.config file

  plugins: [
    '~/plugins/eventbus',
  ],

Now we can use it when we need it.

We want to emit the event from the _slug page. Here is where we can access the data we want to share.

So, import the eventbus

  import { eventBus } from '../../plugins/eventbus';

And use it to emit an event passing the data we want to send to the receiver, the slug translation.

Let's do it in the created lifecycle hook, so it will be called when all reactive data and events are available.

  created(){
      // Event emit passing the data we want to send to the receiver
      this.$eventBus.$emit('inPost', this.slugTrans);
  }

The $emit() method accepts two parameters, the name of the event and the data we want to pass.

the $emit method accept two parameters, the name of the event and the data we want to pass.

nuxt event emitter plugin

Now, any component that need this data can subscribe to this event bus. In our case, this will be the LangSwitcher component.

Import the event bus and get the slug translation by listening to the event. Let's also do it in the created hook:

  import { eventBus } from '../plugins/eventbus';

created( ) {         this.$eventBus.$on("inPost", (slugTrans) => {           console.log("I have been notified that the slug changed: " + slugTrans);           this.slugTrans = slugTrans         });   }

Here we are using the $on() method to listen to the inPost event. Then, we pass a callback function that received the data we get from the event (slugTrans) and stores it in the slugTrans variable we have created in the component data() function.

 data () {
    return {
       slugTrans:''
    }
  },
nuxt plugin

Now we have access to the translation slug, so we can use it in the changeLanguage() method to build the link's route.

Then, add a conditional to the changeLanguage() method to manually change the locale and go to the route containing the translated slug, if we are in the blog.


<script>
import { eventBus } from '../plugins/eventbus';

export default { data () { return { slugTrans:’’ } },

created( ) { this.$eventBus.$on("inPost", (slugTrans) => { console.log("I have been notified that the slug changed: " + slugTrans); this.slugTrans = slugTrans }); },

methods: { changeLanguage(lang) { if(this.$route.path.includes(’/blog/’)){ // if in blog, change the locale manually and the navigate to the new route this.$i18n.locale = lang; this.$router.push(this.localePath({ name: ‘blog-slug’, params: { slug: this.slugTrans } })); } else{ this.$router.push(this.switchLocalePath(lang)); } } }

}; </script>

You can now test it and see that you can change the language from a post blog.

nuxt language switcher

In Part 3 I talk about a coupe of things to consider when A couple of things to consider when injecting text from the locales. You might want to check it.

Let's recap:

We have created the basic structure for our web, created a couple of Markdown files for our blog, seen how to create dynamic content and routes, and added localization for both the home page and the blog.

That means that we have done most of the hard work. There is just one crucial thing missing, generating the static pages and publishing the content so that it can be accessed by the world. But before doing so, I'm going to finish my home page and add some style to the blog.

As what with we have learned so far you have all the crucial content to create a static site, feel free to skip to the final steps: generating the static sites and deployment inPart 5.

If you would like to see how I created the home page of this website, I'm going to show you in Part 3 of this article. Otherwise, ypu can go to Part 4.