syntaxai/tdd.md · main · src / b32_sitemap.ts

b32_sitemap.ts 56 lines · 1859 bytes raw
// 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<string> = [
  "/",
  "/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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");

const renderUrl = (u: SitemapUrl): string => {
  const loc = `<loc>${escapeXml(u.loc)}</loc>`;
  const lastmod =
    u.lastmod !== undefined ? `<lastmod>${escapeXml(u.lastmod)}</lastmod>` : "";
  return `  <url>${loc}${lastmod}</url>`;
};

export const renderSitemap = (urls: ReadonlyArray<SitemapUrl>): string => {
  const body = urls.map(renderUrl).join("\n");
  const inner = body.length > 0 ? `\n${body}\n` : "\n";
  return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${inner}</urlset>`;
};