How to create multilingual sitemaps in Next.js (Pages Router) v16

Organize sitemaps by language for scale

Problem

Sitemaps help search engines discover and index pages on a website. For a multilingual site with hundreds or thousands of pages per language, a single sitemap listing every URL for every locale quickly becomes unmanageable. Large monolithic sitemaps can exceed the 50,000 URL or 50MB size limits defined by the sitemap protocol, making them invalid. Even when they remain within limits, regenerating and validating a massive file every time content changes in one language is inefficient. As the site grows and adds more languages or pages, this approach does not scale.

Solution

Organize sitemaps into a hierarchy using a sitemap index file. The index file lists separate language-specific sitemaps, each containing URLs for a single locale. This structure keeps individual sitemap files manageable and within protocol limits. When content changes in one language, only that language's sitemap needs regeneration. The approach scales naturally as new languages are added—each gets its own sitemap referenced in the index. Search engines crawl the index first, then follow links to individual language sitemaps.

Steps

1. Create a sitemap index page

Create a page in the pages directory to generate the sitemap index dynamically using getServerSideProps.

import { GetServerSideProps } from "next";

const SITE_URL = "https://example.com";
const LOCALES = ["en", "es", "fr", "de"];

function generateSitemapIndex(locales: string[]): string {
  const sitemapEntries = locales
    .map((locale) => {
      return `
<sitemap>
  <loc>${SITE_URL}/sitemap-${locale}.xml</loc>
  <lastmod>${new Date().toISOString()}</lastmod>
</sitemap>`;
    })
    .join("");

  return `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${sitemapEntries}
</sitemapindex>`;
}

export const getServerSideProps: GetServerSideProps = async ({ res }) => {
  const sitemap = generateSitemapIndex(LOCALES);

  res.setHeader("Content-Type", "text/xml");
  res.write(sitemap);
  res.end();

  return {
    props: {},
  };
};

export default function SitemapIndex() {}

The index uses the <sitemapindex> root element with <sitemap> entries, each containing a <loc> child pointing to a language-specific sitemap. The getServerSideProps function sets the Content-Type header to text/xml and writes the XML response directly.

2. Create language-specific sitemap pages

Create a dynamic route page to generate individual sitemaps for each language.

import { GetServerSideProps } from "next";

const SITE_URL = "https://example.com";

interface PageData {
  slug: string;
  lastModified: string;
}

async function getPagesByLocale(locale: string): Promise<PageData[]> {
  return [
    { slug: "about", lastModified: "2024-01-15" },
    { slug: "contact", lastModified: "2024-01-20" },
  ];
}

function generateSitemap(locale: string, pages: PageData[]): string {
  const urlEntries = pages
    .map((page) => {
      return `
<url>
  <loc>${SITE_URL}/${locale}/${page.slug}</loc>
  <lastmod>${page.lastModified}</lastmod>
</url>`;
    })
    .join("");

  return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urlEntries}
</urlset>`;
}

export const getServerSideProps: GetServerSideProps = async ({
  params,
  res,
}) => {
  const locale = params?.locale as string;

  const pages = await getPagesByLocale(locale);
  const sitemap = generateSitemap(locale, pages);

  res.setHeader("Content-Type", "text/xml");
  res.write(sitemap);
  res.end();

  return {
    props: {},
  };
};

export default function LocaleSitemap() {}

The dynamic route extracts the locale from URL parameters and generates XML containing only URLs for that language. Each sitemap remains focused on a single locale's content.

3. Fetch locale-specific content

Replace the placeholder getPagesByLocale function with your actual data source.

async function getPagesByLocale(locale: string): Promise<PageData[]> {
  const response = await fetch(
    `https://api.example.com/pages?locale=${locale}`,
  );
  const data = await response.json();

  return data.pages.map((page: any) => ({
    slug: page.slug,
    lastModified: page.updatedAt,
  }));
}

This function queries your CMS, database, or API to retrieve pages for the specified locale. It returns structured data that the sitemap generator transforms into XML entries.

4. Add static pages to each sitemap

Include static routes that exist in every language alongside dynamic content.

function generateSitemap(locale: string, pages: PageData[]): string {
  const staticPages = [
    { slug: "", lastModified: new Date().toISOString() },
    { slug: "about", lastModified: new Date().toISOString() },
  ];

  const allPages = [...staticPages, ...pages];

  const urlEntries = allPages
    .map((page) => {
      const path = page.slug ? `/${locale}/${page.slug}` : `/${locale}`;
      return `
<url>
  <loc>${SITE_URL}${path}</loc>
  <lastmod>${page.lastModified}</lastmod>
</url>`;
    })
    .join("");

  return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urlEntries}
</urlset>`;
}

Combining static and dynamic pages ensures each language sitemap is complete. Static pages use the current timestamp as their last modification date.

5. Reference the index in robots.txt

Add the sitemap index location to your robots.txt file so search engines discover it.

User-agent: *
Allow: /

Sitemap: https://example.com/sitemap.xml

You only need to list the index file; search engines will follow links to individual language sitemaps automatically. Place this file in the public directory.