Nuxt - how to create a sitemap Google Search will like!

Bart MartinBart Martin
4 min read

Hello!

I recently implemented Sitemap in Nuxt, including i18n (internationalization) and dynamic links. I didn't find any tutorial online about implementing a sitemap combining these options, so I hope this guide will help some other devs.

Internationalization plugin

@nuxtjs/i18n provides functionality that enables having i18n in your app. I will assume that you already have some setup for it. That's how it looks for me:

export default defineNuxtConfig({
    //..Other config sections
    modules: [
        //..Other modules 
        ['@nuxtjs/i18n',{
            locales: [
              { code: 'en', iso: 'en-US' },
              { code: 'sv', iso: 'sv-SE' },
              // Add more locales here
            ],
            defaultLocale: 'en',
            strategy: 'prefix_except_default',
            vueI18n: './i18n.config.ts', 
            detectBrowserLanguage: false,
          }
        ],
    ]
})

locales specifies which locales you want to have in your sitemap.

strategy meaning is explained in the documentation.

detectBrowserLanguage is annoying, I recommend setting it to false.

vueI18n is my file with my messages, it looks approximately like this

export default defineI18nConfig(() => ({
  legacy: false,
  locale: 'en',
  messages: {

    // ADD COMA AFTER EACH LINE BELOW, OTHERWISE IT CRASHES WITH UNSPECIFIC ERROR

    en: {
      // NAVBAR
      navbarHome: 'Home',
      ..., //your other messages
    },

    sv: {
      // NAVBAR
      navbarHome: 'Hem',
      ..., //your other messages    
    },
  }
}))

Sitemap and defining dynamic routes

I chose to use @nuxtjs/sitemap

Part of my setup in nuxt.config.ts that focuses on sitemap module looks like this:

export default defineNuxtConfig({
    //..Other config sections
    modules: [
        //..Other modules 
        ['@nuxtjs/sitemap',
            {
              sources: ['/api/sitemap'],
              xslColumns: [
                { label: 'URL', width: '50%' },
                { label: 'Last Modified', select: 'sitemap:lastmod', width: '25%' },
                { label: 'Hreflangs', select: 'count(xhtml:link)', width: '25%' },
              ],
            }],
    ],
})

xslColumns is defining how the sitemap is presented, check the documentation to learn more.

The sources are actually where we specify the dynamic links that we want to add to our sitemap. More info in the documentation.

In the contents constant, I receive a list of all the articles that I have in my backend. Then I proceed to extract slugs for all of them and prepare them

My file looks approximately like this, it's not perfectly optimized but should give you an idea:

export default defineSitemapEventHandler(async () => {

    // get all the slugs
    async function fetchContents() {
        try {
            const contents = await $fetch<ApiResponseContent>('link_to_api_endpoint');

            // Extract the slugs. 
            const sitemapEntries = contents.data.map(content => {
                const mainSlug = content.attributes.slug

                // Some blog articles will have localizations, if not return []
                const localizationEntries = content.attributes.localizations?.data.map(localization => {
                    return {
                        link: [
                            {
                                hreflang: localization.attributes.locale,
                                href: localization.attributes.slug
                            },
                            {
                                hreflang: 'x-default', // Assuming 'en' is the default locale
                                href: content.attributes.locale == 'en' ? mainSlug : localization.attributes.slug}
                            }
                        ]
                    };
                }) || [];

                return [
                    {
                        loc: mainSlug,
                        _sitemap: content.attributes.locale == 'en' ? "en-US" : "sv-SE",
                        link: [
                            {
                                hreflang: content.attributes.locale,
                                href: mainSlug
                            },
                            ...localizationEntries.map(entry => entry.link).flat()
                        ]
                    },
                ];
            });

            return sitemapEntries;

        } catch (error) {
            console.error("Error fetching pages:", error);
            return [];
        }
    }

    // Call the function and handle the slugs
    const slugsContents = await fetchContents()

    const allSlugs = [...slugsContents]
    const flattenedSlugs = allSlugs.flat();

    return [
        ...flattenedSlugs.map(content => asSitemapUrl({
            loc: content.loc,
            alternatives: content.link,
            _sitemap: content._sitemap
        }))
    ]

This way we end up with a list of slugs in an asSitemapUrl format. We specify the link (loc) of every item and its alternatives. Each item WITHOUT translation should have 1 alternative (the link to the item itself), and each item WITH translation should have 3 alternatives (the item's link, its other language version link, and its default language).

Some slugs should end up in an English sitemap, some in the Swedish one. You specify in which sitemap they should be by specifying the _sitemap parameter. Make sure you use the correct name that i18n is using for your locales.

Result

If you follow my description, you should be able to see the resulting sitemaps at yourwebsitelink.com/sitemap.xml after starting your application. You can then proceed to change the look of each of the sitemaps by editing the xslColumns parameter.

Hope this guide gave you some sense of direction on how to implement i18n and dynamic links in the sitemap in Nuxt. If you have any questions feel free to reach out to me, I'll be happy to help!

Cheers!

Bart

0
Subscribe to my newsletter

Read articles from Bart Martin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Bart Martin
Bart Martin