Create sitemaps for a multi-language, multi-domain, URL-translated, static Next.js site
Tags: nextjs, sitemap, i18n, ssg
Published: 2022-10-19 00:37:10

Yes, I know the title post is a bit long 😅 but each listed feature is necessary to understand the solution that I adopted to achieve my goal: create a sitemap file for each of my domains pointing to a single multilingual, static, URL-translated Next.js site.

## Why static?
First of all, I 💘 the [jamstack](https://jamstack.org/) architecture that solves so many problems in such simple ways... and that is why in this context I speak of a `static` site: <u>all</u> pages are pre-generated at build time (known as SSG).

## Multi-language
Next.js give us a basic but powerful [i18n support](https://nextjs.org/docs/advanced-features/i18n-routing#domain-routing) with two strategies: sub-path routing (`example.com/it`, `example.com/en`) and domain routing (`example.it`, `example.com`): I chose the last one, but you can adopt this solution also for the first strategy easily.

## URLs translated
Translate the URLs too in my opinion is a killer SEO feature but, at the moment, [is not so simple support it in a Next.js site](https://github.com/vercel/next.js/discussions/18485): there are many solutions which vary by your data source (headless CMS, file system based, ...) but the important thing is that in your final build you'll have each page collected for its localization code:

```
.next/server/pages/it/chi-siamo.html
.next/server/pages/en/about-us.html
```

Thanks to [one of Lee Robinson's post](https://leerob.io/blog/nextjs-sitemap-robots) I have found a suggestion for an efficient way to generate all the necessary sitemap files without further queries or REST calls or server requests, but simply by looking in the created `.next` directory (instead of in the `pages` folder as in the original post).

```js
import { writeFileSync } from "fs"
import { globby } from "globby"
import prettier from "prettier"

async function generate() {
  ;["it", "en"].forEach(async (lang) => {
    const pages = await globby([
      `.next/server/pages/${lang}/**/*.html`,
      `!.next/server/pages/${lang}/404.html`,
      `!.next/server/pages/${lang}/500.html`      
    ])

    const siteUrl = lang === "it" ? "https://example.it" : "https://example.com"

    const sitemap = `
    <?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
        <url>
            <loc>${siteUrl}</loc>            
            <lastmod>${new Date().toISOString()}</lastmod>
        </url>
        ${pages
          .map((page) => {
            const route = page.replace(`.next/server/pages/${lang}`, "").replace(".html", "")

            return `<url>
                  <loc>${siteUrl}${route}</loc>
                  <lastmod>${new Date().toISOString()}</lastmod>                  
              </url>
            `
          })
          .join("")}
    </urlset>
    `

    const formatted = prettier.format(sitemap, {
      parser: "html",
    })

    writeFileSync(`public/sitemap-${lang}.xml`, formatted)
  })
}

generate()
```

Executing the `generate-sitemap.mjs` script in the `postbuild` step as described in the linked post, we'll have at the end two (or more as your site's language 😏) xml sitemap file:

```
example.com/sitemap-en.xml
example.it/sitemap-it.xml
```

Usually with a non-localized site I'm very happy to use the [next-sitemap](https://www.npmjs.com/package/next-sitemap) library but for a specific case like this I think that rolling up your sleeves is always the best solution 😉