DEV Community

Marco Valsecchi
Marco Valsecchi

Posted on

Create sitemaps for a multi-language, multi-domain, URL-translated, static Next.js site

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 architecture that solves so many problems in such simple ways... and that is why in this context I speak of a static site: all pages are pre-generated at build time (known as SSG).

Multi-language

Next.js give us a basic but powerful i18n support 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: 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
Enter fullscreen mode Exit fullscreen mode

Thanks to one of Lee Robinson's post 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).

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Usually with a non-localized site I'm very happy to use the next-sitemap library but for a specific case like this I think that rolling up your sleeves is always the best solution πŸ˜‰

Oldest comments (2)

Collapse
 
thatgriffith profile image
Kevin H Griffith

Wow! Awesome solution. Huge thanks for sharing this. πŸ™πŸ½

Collapse
 
pariola_droid profile image
Pariola

Hi Marco, tried this and i wasn't getting it right

Here is what i got:

`node:internal/fs/utils:921
throw new ERR_INVALID_ARG_TYPE(
^

TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Promise`

tried using for...of loop instead too, still the same