How to create multilingual sitemaps in TanStack Start v1

Organize sitemaps by language for scale

Problem

Sitemaps help search engines discover and crawl all pages on a site. A multilingual site with hundreds or thousands of pages per language can quickly generate enormous sitemaps. A single file listing every URL for every language becomes unwieldy and may exceed the 50,000 URL or 50MB size limits defined by the sitemap protocol. When you update the structure of one language, you must regenerate and revalidate the entire file. Large monolithic sitemaps are difficult to maintain, slow to process, and do not scale as you add languages or content.

Solution

Split your sitemap into multiple files and use a sitemap index file to submit many sitemaps at once. Create a top-level sitemap index at /sitemap.xml that points to separate, language-specific sitemaps such as /sitemap-en.xml and /sitemap-es.xml. The XML format of a sitemap index file is similar to a regular sitemap and is defined by the Sitemap Protocol. This keeps individual files manageable, lets you update each language independently, and scales well as you add new languages or pages.

Steps

1. Create a helper to generate sitemap XML

Build a utility function that generates valid sitemap XML from an array of URL entries.

export function generateSitemapXML(urls: Array<{ loc: string; lastmod?: string; changefreq?: string; priority?: number }>): string {
const entries = urls.map(url => {
  let entry = `  <url>
    <loc>${url.loc}</loc>`
  if (url.lastmod) entry += `
    <lastmod>${url.lastmod}</lastmod>`
  if (url.changefreq) entry += `
    <changefreq>${url.changefreq}</changefreq>`
  if (url.priority !== undefined) entry += `
    <priority>${url.priority}</priority>`
  entry += `
  </url>`
  return entry
}).join('
')

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

This function accepts an array of URL objects and returns a properly formatted XML string with the required namespace and structure.

2. Create a helper to generate sitemap index XML

Build a second utility function that generates a sitemap index pointing to multiple child sitemaps.

export function generateSitemapIndexXML(sitemaps: Array<{ loc: string; lastmod?: string }>): string {
const entries = sitemaps.map(sitemap => {
  let entry = `  <sitemap>
    <loc>${sitemap.loc}</loc>`
  if (sitemap.lastmod) entry += `
    <lastmod>${sitemap.lastmod}</lastmod>`
  entry += `
  </sitemap>`
  return entry
}).join('
')

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

The sitemap index uses a <sitemapindex> root tag and includes a <sitemap> entry for each sitemap, with a <loc> child entry for each parent tag.

3. Define a server route for the main sitemap index

Create a server route at /sitemap.xml that returns the sitemap index listing all language-specific sitemaps.

import { createFileRoute } from "@tanstack/react-router";
import { generateSitemapIndexXML } from "~/utils/sitemap";

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

export const Route = createFileRoute("/sitemap")({
  server: {
    handlers: {
      GET: async () => {
        const sitemaps = SUPPORTED_LOCALES.map((locale) => ({
          loc: `${BASE_URL}/sitemap-${locale}.xml`,
          lastmod: new Date().toISOString().split("T")[0],
        }));

        const xml = generateSitemapIndexXML(sitemaps);

        return new Response(xml, {
          headers: {
            "Content-Type": "application/xml",
            "Cache-Control": "public, max-age=3600",
          },
        });
      },
    },
  },
});

This route generates an index that points to one sitemap per language and serves it as XML with appropriate caching headers.

4. Define server routes for language-specific sitemaps

Create a dynamic server route that generates a sitemap for each language based on the locale parameter.

import { createFileRoute } from "@tanstack/react-router";
import { generateSitemapXML } from "~/utils/sitemap";

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

async function getUrlsForLocale(locale: string) {
  return [
    { loc: `${BASE_URL}/${locale}`, changefreq: "daily", priority: 1.0 },
    {
      loc: `${BASE_URL}/${locale}/about`,
      changefreq: "monthly",
      priority: 0.8,
    },
    {
      loc: `${BASE_URL}/${locale}/contact`,
      changefreq: "monthly",
      priority: 0.8,
    },
  ];
}

export const Route = createFileRoute("/sitemap-$locale")({
  server: {
    handlers: {
      GET: async ({ params }) => {
        const { locale } = params;
        const urls = await getUrlsForLocale(locale);
        const xml = generateSitemapXML(urls);

        return new Response(xml, {
          headers: {
            "Content-Type": "application/xml",
            "Cache-Control": "public, max-age=3600",
          },
        });
      },
    },
  },
});

Server routes support dynamic path parameters in the same way as TanStack Router, so a file named with $locale creates a route that accepts a dynamic locale parameter. Each language-specific sitemap is generated independently and can be updated without affecting other languages.

5. Fetch URLs from your data source

Replace the placeholder getUrlsForLocale function with logic that fetches actual URLs from your database, CMS, or route definitions.

async function getUrlsForLocale(locale: string) {
  const pages = await db.page.findMany({
    where: { locale, published: true },
    select: { slug: true, updatedAt: true },
  });

  return pages.map((page) => ({
    loc: `${BASE_URL}/${locale}/${page.slug}`,
    lastmod: page.updatedAt.toISOString().split("T")[0],
    changefreq: "weekly",
    priority: 0.7,
  }));
}

This example queries a database for published pages in the given locale and maps them to sitemap entries with metadata. Adjust the query and mapping logic to match your data model and URL structure.