Creating a sitemap index for multiple languages

Managing large, multi-language sites for SEO

Problem

A site has a large number of pages across many languages. A single, massive sitemap.xml file listing every URL for every language is inefficient, hard to manage, and may exceed file size limits. This makes it difficult to update one language's URLs or for search engines to efficiently discover all content.

Solution

Create a sitemap.xml file that acts as a "sitemap index," pointing to other, language-specific sitemaps (e.g., sitemap-en.xml, sitemap-es.xml). This approach organizes content, is easier to manage, and scales efficiently as new languages or pages are added.

Steps

1. Create the sitemap index route

Instead of the sitemap.ts file, you must create a standard route handler. This gives you full control to generate a sitemap index.

Create app/sitemap.xml/route.ts.

// app/sitemap.xml/route.ts
import { locales, siteBaseUrl } from '@/i18n-config';

export async function GET() {
  const sitemapIndex = `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="[http://www.sitemaps.org/schemas/sitemap/0.9](http://www.sitemaps.org/schemas/sitemap/0.9)">
  ${locales
    .map((locale) => {
      return `
    <sitemap>
      <loc>${siteBaseUrl}/sitemap-${locale}.xml</loc>
      <lastmod>${new Date().toISOString()}</lastmod>
    </sitemap>`;
    })
    .join('')}
</sitemapindex>
`;

  return new Response(sitemapIndex, {
    headers: {
      'Content-Type': 'application/xml',
    },
  });
}

This file tells search engines that your site's sitemap is an index that points to other files, like sitemap-en.xml, sitemap-es.xml, etc.

2. Create the dynamic route for language sitemaps

Next, create the dynamic route that will generate each language-specific sitemap.

Create app/sitemap-[lang].xml/route.ts.

// app/sitemap-[lang].xml/route.ts
import { locales, siteBaseUrl } from '@/i18n-config';

// This tells Next.js which sitemaps to build at build time
export async function generateStaticParams() {
  return locales.map((lang) => ({
    lang,
  }));
}

// A helper function to get all pages for a language
// In a real app, this would fetch from a CMS or database
async function getPagesForLanguage(lang: string): Promise<string[]> {
  // These are relative paths, *without* the lang prefix
  // e.g., '/', '/about', '/blog/my-post'
  return ['/', '/about', '/contact'];
}

export async function GET(
  request: Request,
  { params }: { params: { lang: string } }
) {
  const { lang } = params;

  if (!locales.includes(lang)) {
    return new Response('Not Found', { status: 404 });
  }

  const pages = await getPagesForLanguage(lang);

  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="[http://www.sitemaps.org/schemas/sitemap/0.9](http://www.sitemaps.org/schemas/sitemap/0.9)">
  ${pages
    .map((page) => {
      const path = page === '/' ? '' : page;
      return `
    <url>
      <loc>${siteBaseUrl}/${lang}${path}</loc>
      <lastmod>${new Date().toISOString()}</lastmod>
    </url>`;
    })
    .join('')}
</urlset>
`;

  return new Response(sitemap, {
    headers: {
      'Content-Type': 'application/xml',
    },
  });
}

Now, your sitemap.xml file is a clean index, and each language's URLs are neatly organized in their own files (e.g., /sitemap-en.xml), which are generated by this dynamic route.