// b32 — Layer 1 pure helper: render a sitemaps.org 0.9 urlset. // No I/O. Deterministic: same input array → same output bytes. // Caller (d21_app.ts) composes the URL list from ALL_POSTS, // ALL_SAMA, ALL_GUIDES, and STATIC_PATHS, then asks this module // for the XML string. Sibling test pins escape behaviour and // shape; the verifier's §4.3 modeled-tests check requires it. export interface SitemapUrl { readonly loc: string; readonly lastmod?: string; } // The eleven load-bearing static URLs that don't come from a // registry. Each must correspond to a literal route registered // in d21_app.ts; the handler iterates this list verbatim. export const STATIC_PATHS: ReadonlyArray = [ "/", "/blog", "/contributing", "/games", "/goals", "/leaderboard", "/sama", "/sama/v2", "/sama/v2/verify", "/sama/v2/example-crud", "/sama/v2/example-wordpress", "/sama/skill", "/guides", ]; // Minimal XML 1.0 escape for character data + attribute values. // The five named entities are the canonical set; anything else // is left as-is (the sitemap spec requires UTF-8, not ASCII). export const escapeXml = (s: string): string => s .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); const renderUrl = (u: SitemapUrl): string => { const loc = `${escapeXml(u.loc)}`; const lastmod = u.lastmod !== undefined ? `${escapeXml(u.lastmod)}` : ""; return ` ${loc}${lastmod}`; }; export const renderSitemap = (urls: ReadonlyArray): string => { const body = urls.map(renderUrl).join("\n"); const inner = body.length > 0 ? `\n${body}\n` : "\n"; return ` ${inner}`; };