/** * Sitemap index endpoint * * GET /sitemap.xml - Sitemap index listing one sitemap per collection. * * Each collection with published, indexable content gets its own * child sitemap at /sitemap-{collection}.xml. The index includes * a per child derived from the most recently updated entry. */ import type { APIRoute } from "astro"; import { handleSitemapData } from "#api/handlers/seo.js"; import { getPublicOrigin } from "#api/public-url.js"; import { getSiteSettingsWithDb } from "#settings/index.js"; export const prerender = false; const TRAILING_SLASH_RE = /\/$/; const AMP_RE = /&/g; const LT_RE = //g; const QUOT_RE = /"/g; const APOS_RE = /'/g; export const GET: APIRoute = async ({ locals, url }) => { const { emdash } = locals; if (!emdash?.db) { return new Response("", { status: 500, headers: { "Content-Type": "application/xml" }, }); } try { const settings = await getSiteSettingsWithDb(emdash.db); const siteUrl = (settings.url || getPublicOrigin(url, emdash?.config)).replace( TRAILING_SLASH_RE, "", ); const result = await handleSitemapData(emdash.db); if (!result.success || !result.data) { return new Response("", { status: 500, headers: { "Content-Type": "application/xml" }, }); } const { collections } = result.data; const lines: string[] = [ '', '', ]; for (const col of collections) { const loc = `${siteUrl}/sitemap-${encodeURIComponent(col.collection)}.xml`; lines.push(" "); lines.push(` ${escapeXml(loc)}`); lines.push(` ${escapeXml(col.lastmod)}`); lines.push(" "); } lines.push(""); return new Response(lines.join("\n"), { status: 200, headers: { "Content-Type": "application/xml; charset=utf-8", "Cache-Control": "public, max-age=3600", }, }); } catch { return new Response("", { status: 500, headers: { "Content-Type": "application/xml" }, }); } }; /** Escape special XML characters in a string */ function escapeXml(str: string): string { return str .replace(AMP_RE, "&") .replace(LT_RE, "<") .replace(GT_RE, ">") .replace(QUOT_RE, """) .replace(APOS_RE, "'"); }