HOW TO: Include dynamic routes in sitemap with Nuxt.js

Share on Facebook
Share on Twitter
A sitemap is commonly an XML file where you provide information about pages and other content available on a website. It is beneficial for the site crawling as search bots can more efficiently discover content on the website. Therefore, it improves your chances that the bots index all the pages you want and none of them are missed. Consequently, an up-to-date sitemap also has an indirect effect on the site’s SEO.
Share on Facebook
Share on Twitter

Sitemaps in Nuxt.js

In Nuxt.js, there is a dedicated and easy-to-use sitemap module for this very purpose. However, the module ignores any dynamic route - files and folders (in the page folder) prefixed with an underscore - which can oftentimes account for a significant number of pages. On our web, every page resides on a dynamic route and thus not a single page would be present in the module’s output. Well and that’s no good, is it.

One possible solution is to manually list all the dynamic routes that exist on the website. Well… that’s not great either. Imagine we write blog posts regularly and publish one every week. Each time we would have to add the one new entry to the sitemap property in the nuxt.config.js file.


// nuxt.config.js
sitemap: {
    routes: [
      '/blog/week-1',
      '/blog/week-2',
      '/blog/week-3'
      ...
    ]
  }

        

This is burdensome, annoying and highly prone to errors. Things would get even worse when there would be multiple language variations.


// nuxt.config.js
sitemap: {
    routes: [
      '/en/blog/week-1',
      '/cs/blog/week-1',
      '/en/blog/week-2'.
      '/cs/blog/week-2',
      '/en/blog/week-3',
      '/cs/blog/week-3'
      ...
    ]
  }

        

As you can see, it adds up really quickly and soon enough it would not be possible to reasonably maintain the dynamic routes list in an accurate and errorless state. Luckily, there is a better solution. As a brief search on Google suggests, we need to asynchronously retrieve all the pages we have on the website and process the data into a format required by the sitemap module.

The problem

Dynamic routes and sitemap

I’ll demonstrate the idea behind that approach we’re also using in Elision to keep our sitemap up-to-date with an each new release. First, we’ll create a routes.json file in the root folder. This is the only thing we’ll have to maintain manually. However, due to the way Nuxt handles dynamic routes, you won’t need to modify this as long as the new pages belong to the existing dynamic routes. If you add a new page type to the page folder, you’ll need to update this file as well. Still, this will happen exponentially less often than updating the route file with each new page.


// routes.json
[
  "/:lang",
  "/:lang/blog",
  "/:lang/blog/:title"
]

        

Such a list of routes is enough to include in the sitemap a homepage, blog page and any number of blog posts in any number of languages. The :lang string corresponds to the language parameter and the :title for the blog post's title. So for example, these could stand for paths such as:


/en
/en/blog
/fr/blog
/cs/blog/business-post
/en/blog/it-post

        

Second, we'll install the sitemap module and add a getter function to routes in the sitemap property within the nuxt.config.js file. Additionally, import the routes getter function from a util folder as well.


npm install @nuxtjs/sitemap

        

// nuxt.config.js
const getAppRoutes = require('./utils/getRoutes.js');

module.exports = {
  // ...previous code

    modules: [
     '@nuxtjs/sitemap'
    ],

    sitemap: {
      routes() {
        return getRoutes();
      },
      path: '/sitemap.xml',
      gzip: true,
      generate: false,
    },

  // ... following code
}

        

The getRoutes function in general returns a string array of available routes. The specific implementation will vary from site to site, however the general idea will likely remain the same. That is, we need to retrieve metadata from a server or an administration system such as Wordpress, Kentico, October or our beloved Directus. We could use a SDK that commonly ships with the CMS, however, to keep the things as generic as possible, let's use, for the demonstration purposes, good old axios.

The approach

Building dynamic sitemap

In the code below we fetch the available languages and published blog posts from the server. For the blog posts, we check the available languages of each blog post as some of them may be published in all languages while others only in some. Then we iterate over all available languages and predefined route types from the routes.json.


// utils/getRoutes.js
const axios = require('axios);
const appRoutes = require('../routes.json');

module.exports = async function getAppRoutes() {
  // Initiate axios
  const instance = axios.create({
    baseURL: 'https://endpoint.com/api',
    headers: {'Authorization': 'Bearer '+ yourToken}
  });

  // Fetch available languages and store their codes (fr, en, ...) in array
  const availableLanguages = await axios.get('/languages').then(res => res.data.map(language => language.id));

  // Fetch blogPosts as object with languages as attributes and slugs as their values
  const blogPosts = fetchBlogPosts();

  const routes = [];

  // for every language
  for (let i = 0; i < availableLanguages.length; i += 1) {
    // and every route defined in routes.json
    for (let k = 0; k < appRoutes.length; k += 1) {
      let routePath = appRoutes[k];
      const language = availableLanguages[i];

      // replace the language placeholder with current language
      routePath = routePath.replace(/:lang/, language);

      // If the route includes 'blog', iterate over all fetched posts in current language
      if (routePath.includes('/blog/')) {
        for (let postIndex = 0; postIndex < blogPosts[language].length; postIndex += 1) {
          routes.push(routePath.replace(/:title/, blogPosts[language][postIndex]));
        }
      }

      // could add more 'else if' conditions to account for other type of pages (than just blog)

      // Otherwise it's home page and just push it
      else {
        routes.push(routePath);
      }
    }
  }

  // Return all available routes
  return routes;
};

        

The exact way how would you fetch the and process blog posts will certainly vary. In general, we need to retrieve all the blog posts in all their language variations and have it prepared for the second part of the function. It this example, it expects the blog posts to be an object which contains language codes as their attributes and array of slugs as their values. In our case, the implementation looks similar to the following code.


// fetchBlogPosts()
// Iterate over each blog post
await axios.get('/blog').then(res => res.data.forEach((post) => {
    // ... and its every language variation
  post.translations.data.forEach((language) => {
    // If the language is new, add it as a new array attribute
    if (blogPosts[language.language_code] === undefined) {
    // for instance blogPosts[en] = [business-post]
      blogPosts[language.language_code] = [language.postSlug];
    }
    // otherwise push the slug of the post to the given language array
    else {
      blogPosts[language.language_code].push(language.postSlug);
    }
  });
}));

// In the end, the blogPosts object looks something like this
//  {
//     en: ['it-post', 'business-post', 'only-in-english']
//     cs: ['it-post', 'business-post']
//  }

        

That's it. Now if you use nuxt generate and release static HTML, than with each new release you'll have the sitemap up-to-date. If you serve the sitemap through middleware and set generate: false in the module's configuration, it'll be up-to-date all the time. As is ours. Give it a shot and I do hope this guide helps you.

The solution

< back