19-7-2021

Nuxt Content & Nuxt i18n

#code#nuxt#markdown

Wanneer je een website onderhoudt die bezocht wordt door mensen uit verschillende landen, is het een goed idee om de content in meerdere talen aan te bieden. Op deze manier is de content toegangelijk voor zoveel mogelijk mensen zonder dat zij afhanekelijk zijn van externe tools zoals Google Translate. Om dit mogelijk te maken voor deze website is er gebruik gemaakt van @Nuxt/Content om de content te schrijven en weer te geven en nuxt-i18n om de content beschikbaar te maken in meedere talen.

Wat is Nuxt Content?

@Nuxt/Content is een NuxtJS module waarmee je gemakkelijk Git kunt gebruiken als een headless CMS om zo bijvoorbeeld een blog of case studie archief te bouwen voor je website. Dit houdt in dat in plaats van een CMS zoals Wordpress of Strapi waar je normalieter artikelen in zou schrijven, je simpelweg het artikel kan schrijven in een .md bestand, deze vervolgens toe te voegen aan je Git repository en je NuxtJS app de inhoud kan ophalen alsof het met een volwaardig CMS communiceert. Dit zorgt voor een makkelijk te onderhouden compacte web app waar de layout en content op een plek bestaan.

Wat is Nuxt i18n?

i18n of internationalization is het process waarbij de structuur van een website zo wordt ingericht dat de content die gemaakt wordt voor de website makkelijk in verschillende talen kan worden gepresenteerd. Het geschikt maken van de content voor een locale wordt ook wel localization i10n genoemd. nuxt-i18n is een NuxtJS module die het zware werk voor ons doet. Deze module zorgt voor de landcode in de URL ('/nl', '/en' etc.), taaldetectie gebaseerd op de browser van de bezoeker en SEO. Samen met de juiste app structuur en een vertaling van de content op de website maakt deze module het makkelijk om een website te localiseren.

Het project

Setup NuxtJS

Allereerst moeten we een nieuw NuxtJS project opzetten. Open je favoriete CLI en zorg dat je Node en NPM geΓ―nstalleerd hebt. Daarna kun je het volgende command uitvoeren om de setup te beginnen:

npm init nuxt-app <project-name>

Er worden nu een aantal vragen gesteld (name, Nuxt options, UI framework, TypeScript, linter, testing framework, etc), kies de opties die je nodig hebt.

Ga nu naar de nieuwe map met je project bestanden en start de development server:

cd <project-name>
npm run dev

Je Nuxt web app staat nu live op http://localhost:3000 πŸš€

Setup de @nuxt/content module

Nu dat de basis staat, kunnen we de @Nuxt/Content module installeren

npm install @nuxt/content

Nadat de installatie compleet is moet je de module toevoegen aan het nuxt.config.js bestand

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

Setup de nuxt-i18n module

Nu kunnen we nuxt-i18n module installeren

npm install nuxt-i18n

Ook deze moet toegevoegd worden aan het nuxt.config.js bestand

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

Nuxt/Content & Nuxt-i18n

Statische content

Om te beginnen moeten we eerst nuxt-i18n configureren voor de verschillende talen die wij willen ondersteunen. In dit geval zijn dat Nederlands πŸ‡³πŸ‡± en Engels πŸ‡ΊπŸ‡Έ waarbij Nederlands de standaard (default) taal is. Voeg de volgende opties to aan 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'
    } 
  },
}

Hier stellen wij Nederlands en Engels in als de twee "locales", beide met hun eigen landcode, iso code en een verwijzing naar een .json bestand. Dit bestand bevat de vertaling voor de statische pagina's van de website zoals bijvoorbeeld de thuispagina en contact pagina. Vertalingen voor niet statische pagina's, zoals blog posts, zullen op een andere plek te vinden zijn. Hier komen wij zo op terug.

Nu dat de nuxt-i18n opties zijn geupdate kunnen we een nieuwe map aanmaken in het project genaamd locales met daar in de .json bestanden voor de statische Engelse en Nederlands tekst.

.
β”œβ”€β”€ .github
β”œβ”€β”€ .nuxt
β”œβ”€β”€ components
β”œβ”€β”€ content
β”œβ”€β”€ locales <--- Deze map
β”‚   β”œβ”€β”€ en-US.json <--- Engelse tekst
β”‚   └── nl-NL.json <--- Nederlandse tekst
β”œβ”€β”€ node_modules
β”œβ”€β”€ pages
β”œβ”€β”€ static
β”œβ”€β”€ store
β”œβ”€β”€ .editconfig
β”œβ”€β”€ .eslintrc.js
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .prettierrc
β”œβ”€β”€ jsconfig.json
β”œβ”€β”€ nuxt.config.js
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
└── .README.md

Open nu het en-US.json bestand en voeg het volgende toe:

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

Doe hetzelfde voor het nl-NL.json bestand:

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

Nu dat de vertalingsbestanden voor de statische pagina's klaar zijn, kunnen we deze gaan gebruiken. Begin met het verwijderen van components/Tutorial.vue. Open hierna pages/index.vue en vervang hier het <Tutorial /> component door <h1>{{ $t('welcome') }}</h1>.

Als je nu de development server opstart en naar http://localhost:3000 gaat zou je nu "Hallo, Wereld! 🌍" moeten zien! Niet alleen dat, als je naar http://localhost:3000/en gaat zou je "Hello, World! 🌍" zien. De statische content is nu in beide talen beschikbaar!

Dynamische content - Individuele blog posts

Naast het internationalizeren van de statische content, is het ook belangrijk om de dynamische content in meerdere talen aan te bieden. In het geval van Studio Terabyte zijn dit de case studies en de blog (die je nu leest πŸ˜‰).

Om gebruik te maken van Nuxt/Content moet er eerst een /content map worden gemaakt. Daarbinnen moet de volgende structuur worden gebouwd:

.
β”œβ”€β”€ .github
β”œβ”€β”€ .nuxt
β”œβ”€β”€ components
β”œβ”€β”€ content <--- Deze map
β”‚   β”œβ”€β”€ en <--- Een map per taal
β”‚   β”‚   └── blog <--- Een map voor de content catagorie
β”‚   β”‚       └── blog-post.md <--- Een bestand voor het artikel
β”‚   └── nl <--- Een map per taal
β”‚       └── blog <--- Een map voor de content catagorie
β”‚           └── blog-post.md <--- Een bestand voor het artikel
β”œβ”€β”€ 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
  • De /content map is waar Nuxt/Content zoekt naar de content voor de artikelen
  • De /en en /nl mappen zijn om de verschillende talen uit elkaar te houden
  • De /blog map is voor de specefieke soort content die we willen schijven
  • Het blog-post.md bestand is de daadwerkelijke blog post die op de website te zien in

Open nu het blog-post.md bestand in de /en folder en voeg het volgende toe:

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

Open nu ook het blog-post.md bestand in de /nl folder en voeg het volgende toe:

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

De structuur is nu klaar voor blog posts in meedere talen, maar nu moeten de blog posts nog een eigen pagina krijgen. Hiervoor hebben we een nieuwe map en twee nieuwe bestanden nodig:

.
β”œβ”€β”€ .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 <-- Deze map
β”‚       β”œβ”€β”€ _slug.vue <-- Dit bestand
β”‚       └── index.vue <-- Dit bestand
β”œβ”€β”€ static
β”œβ”€β”€ store
β”œβ”€β”€ .editconfig
β”œβ”€β”€ .eslintrc.js
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .prettierrc
β”œβ”€β”€ jsconfig.json
β”œβ”€β”€ nuxt.config.js
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
└── .README.md

De pages/blog map in combinatie met het index.vue bestand vertelt NuxtJS dat wij een pagin willen maken voor www.example.com/blog. Het _slug.vue bestand in dezelfde map vertelt NuxtJS dat wij ook pagina's willen maken voor www.example.com/blog/blog-post waar _slug.vue de indiviudele blog posts representeert.

Open nu het _slug.vue bestand en voeg de volgende code toe:

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

Oke, laten we de code ontleden. Het eerste gedeelte, tussen de <template> tags is verantwoordelijk voor wat er daadwerkelijk te zien is op het scherm. In dit geval staat er een <h1> tag (een heading) die de titel van de blog post laat zien door deze uit het post object te halen. Daaronder staat een tag die je misschien niet kent, <nuxt-content :document="post" />. Dit is een tag die aan de @Nuxt/Content module vertelt dat wij de inhoud van de blog post hier willen laten zien.

Het tweede gedeelte van de code, tussen de <script> tags, is de code die verantwoordelijk is voor het ophalen van de blog post zelf. Allereerst definieren wij hier de naam van het bestand zodat indien nodig deze intern gebruikt kan worden name: 'BlogSlug',.

Hierna maken we gebruik van de asyncData functie die door NuxtJS wordt aangeroepen wanneer de pagina geladen wordt. Binnen deze functie hebben wij toegang tot de /content map door middel van de $content variabelen, die automatisch beschikbaar is. Hier geven wij de landcode mee van de taal die op dit moment geselecteerd is (nl/en) via app.i18n.locale, de naam van de map waarin wij willen zoeken via blog en de URL parameter (www.example.com/blog/dit-stukje-hier) via params.slug. Nu dat alle data die wij willen ophalen is ingevuld, halen wij de blog post data op door middel van de .fetch() functie. Eventuele fouten worden opgevangen en afgehandeled door de .catch() functie.

Start nu de development server en open http://localhost:3000/blog/blog-post in je browser. Je zou nu je eerste blog post moeten zien! πŸŽ‰. Als je de Engelse versie wilt zien kun je naar http://localhost:3000/en/blog/blog-post

Dynamische content - Alle blog posts

Het laten zien van een specefieke blog post is fantastich, nu kunnen bezoekers immers de content lezen, maar een overzichtspagina mag niet ontbreken. Dit helpt bezoekers content te ontdekken en maakt de website een stuk overzichtelijker.

Open nu het index.vue bestand en voeg de volgende code toe:

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

De code hierboven lijkt op de code die wij gebruiken om een individuele blogpost op te halen, we halen immers in beide gevallen blog post data op, maar er zijn een aantal dingen anders genoeg om uit te lichten. Laten we weer beginnen met de code tussen de <template>. Om te voorkomen dat we voor elke blog post die wij willen laten zien op de overzichtspagina een nieuw stukje code toe moeten voegen, bouwen wij de content dynamisch op. Dit doen wij door middel van een for loop: <div v-for="(post, index) in posts" :key="index">. Deze pakt alle posts die wij hebben opgehaald en maakt een <div> element aan voor elke post die gevonden wordt. Op deze manier maakt het niet uit hoeveel posts je schrijft, ze zullen allemaal automatisch meegenomen worden. Nu dat wij een element hebben voor elke post, moet er natuurlijk iets te zien zijn op de pagina. In dit geval laten wij twee dingen zien: de samenvatting van de post (de short) en een link naar de post toe:

<!-- De short van de post -->
<p>{{ post.short }}</p> 
<!-- De link naar de post toe -->
<nuxt-link :to="localePath(post.path)">LINK</nuxt-link>

Zoals te zien in de code roepen wij de localePath functie aan voordat wij de url van de post aan de <nuxt-link> tag geven. Deze functie zorgt dat de link naar de post automatisch wordt aangepast aan de hand van de actieve taal. Dus als de site in het Engels staat wordt er automatisch /en toegevoegd aan de URL.

Het stuk code tussen de <script> tags is ook hier verantwoordelijk voor het ophalen van de blog posts data. Net zoals bij de individuele blog posts maken wij gebruik van de asyncData functie van NuxtJS. Het verschil hier zijn de .only(['short']) functie die aangeeft welke data wij willen hebben, wij hebben natuurlijk niet de gehele blog post nodig als we alleen maar de samenvatting willen laten zien, en de .sortBy('createdAt', 'asc') functie die de blog posts sorteerd op aanmaakdatum met de nieuwste bovenaan.

Als laatste nemen wij de blog posts die opgehaald zijn en verwijderen wij de locale uit de URL (/nl of /en). Dit doen wij omdat @Nuxt/Content de URL naar de blog post als www.example.com/nl/blog/post-naam teruggeeft, wat geen geldige URL is omdat wij ervoor gekozen hebben niet /nl te laten zien omdat dit de standaard taal is.

Start nu de development server en open http://localhost:3000/blog in je browser. Je zou nu je een overzicht met je eerste blog post moeten zien! πŸŽ‰. Als je de Engelse versie wilt zien kun je naar http://localhost:3000/en/blog.

Taal switcher

Nu dat wij zowel een overzicht van blog posts en de blog posts zelf in meerdere talen kunnen laten zien, mist er alleen nog maar de mogelijkheid om gemakkelijk te switchen tussen de talen. om deze functionaliteit in te bouwen hebben wij eerst een nieuw component nodig. voeg LanguageSwitcher.vue toe aan de /components map toe:

.
β”œβ”€β”€ .github
β”œβ”€β”€ .nuxt
β”œβ”€β”€ components
β”‚   └── LanguageSwitcher.vue <-- Dit bestand
β”œβ”€β”€ 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

Open nu LanguageSwitcher.vue en voeg de volgende code toe:

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

Hier hebben wij tussen de <template> tags twee nuxt-link tags toegevoegd, beide met een v-if conditie. De eerste nuxt-link verandert de huidige pagina naar de Engelse versie en is alleen te zien als de huidige locale niet Engels is. De tweede link verandert de huidige pagina naar de Nederlandse versie en is alleen te zien als de huidige locale niet Nederlands is.

Om deze link bovenaan elke pagina te laten zien moeten we een nieuwe map aanmaken, /layouts met een nieuw bestand default.vue:

.
β”œβ”€β”€ .github
β”œβ”€β”€ .nuxt
β”œβ”€β”€ components
β”‚   └── LanguageSwitcher.vue 
β”œβ”€β”€ content 
β”‚   β”œβ”€β”€ en 
β”‚   β”‚   └── blog 
β”‚   β”‚       └── blog-post.md 
β”‚   └── nl 
β”‚       └── blog 
β”‚           └── blog-post.md 
β”œβ”€β”€ layouts <-- Deze map
β”‚   └── default.vue <-- Dit bestand
β”œβ”€β”€ 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 geeft aan hoe de layout van de website eruit moet zien. Op deze manier kun je bijvoorbeeld makkelijk een menu of een footer toevoegen aan elke pagina zonder de code op elke individuele pagina te kopieΓ«ren.

Open nu default.vue en voeg de volgende code toe:

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

Hier vertellen wij NuxtJS dat elke pagina eerste het taal switcher component moet laten zien en daaronder de rest van de content.

Start nu de development server en open http://localhost:3000/blog in je browser. Jou zou nu EN boven aan de pagina moeten zien. Als je hier op klikt verander de taal, en de content op de pagina, automatisch naar het Engels!

Gefeliciteerd, je hebt nu een basis voor een moderne, meertallige website! πŸŽ‰

Credits