Split Sitemaps and a Sitemap Index
Hard · Metadata & SEO
Google accepts at most 50,000 URLs per sitemap file. For bigger shops, Next.js lets app/product/sitemap.ts export generateSitemaps(), which returns the chunk ids ([{ id: 0 }, { id: 1 }]). Next.js then serves every chunk at /product/sitemap/<id>.xml and calls the default sitemap function once per chunk. Since Next.js 16 the id arrives as a Promise of a string: const id = await props.id. A sitemap index file (often a small route handler) lists all chunk files so crawlers find them.
Next.js turns the objects you return into XML for you. Here you write the XML yourself (simplified), so you see exactly what ends up in the files. Lines are joined with \n, indentation is 2 spaces per level, and the driver splits the result into lines for the tests.
- generateSitemaps(total, perSitemap) returns { id } objects for Math.ceil(total / perSitemap) chunks (none for 0 products).
- sitemapIndex(baseUrl, total, perSitemap) returns the index: the line <?xml version="1.0" encoding="UTF-8"?>, then <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">, then for every chunk <sitemap> (2 spaces), <loc>{base}/product/sitemap/{id}.xml</loc> (4 spaces) and </sitemap> (2 spaces), and finally </sitemapindex>. The examples show the exact text.
- sitemap(props, baseUrl, products, perSitemap) returns the chunk XML in the same shape: the same first line, <urlset xmlns="..."> with the same namespace, one <url> block per product, then </urlset>. Chunk id holds products id * perSitemap up to (id + 1) * perSitemap - 1. Each becomes <url> with <loc>{base}/product/{slug}</loc> and, if updatedAt is a valid date, <lastmod>YYYY-MM-DD</lastmod> (the UTC date).
- The id must be a whole number written without extra zeros ("0", "1", not "01") and belong to an existing chunk; otherwise return null (a 404).
- Remove trailing slashes from baseUrl. Escape &, <, >, " and ' in URLs as &, <, >, ", ': a raw & makes the whole file invalid XML.