import type { MetadataRoute } from 'next'; import type { SitemapEntry } from '#web-runtime/types'; import type { PublicPathResolver } from './page-metadata'; /** * Turns the backend's page listing into sitemap entries. * * Pages that share a `groupKey` are the same page in different languages, so each entry links * to its siblings through `alternates.languages` — the sitemap equivalent of the `hreflang` * tags on the page itself, and the form Google prefers for multilingual sites. * * Kept free of `next/headers` and routing setup so it can be tested directly: the caller * supplies the path resolver and the absolute origin. */ export function buildSitemap( entries: SitemapEntry[], origin: URL, publicPath: PublicPathResolver ): MetadataRoute.Sitemap { const absolute = (locale: string, path: string) => new URL(publicPath(locale, path), origin).href; const byGroupKey = new Map(); for (const entry of entries) { if (!entry.groupKey) continue; const group = byGroupKey.get(entry.groupKey); if (group) group.push(entry); else byGroupKey.set(entry.groupKey, [entry]); } return entries.map((entry) => { const siblings = byGroupKey.get(entry.groupKey) ?? []; const lastModified = entry.updatedAt ?? entry.publishedAt; const item: MetadataRoute.Sitemap[number] = { url: absolute(entry.locale, entry.path) }; if (lastModified) { item.lastModified = new Date(lastModified); } // A single-locale group has nothing to point at but itself; omit the noise. if (siblings.length > 1) { item.alternates = { languages: Object.fromEntries( siblings.map((sibling) => [sibling.locale, absolute(sibling.locale, sibling.path)]) ) }; } return item; }); }