import type { APIRoute } from "astro"; import { api, type DictionaryItem } from "../lib/api"; const escapeXml = (value: string) => value.replace(/[<>&'"]/g, c => ({ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ })[c] ?? c); let lastGood: { origin: string; xml: string; at: number } | undefined; export const GET: APIRoute = async ({ site }) => { const origin = site?.origin ?? "https://rf4spotter.ru"; const output = (xml: string) => new Response(xml, { headers: { "Content-Type": "application/xml; charset=utf-8", "Cache-Control": "public, max-age=1800" } }); if (lastGood?.origin === origin && Date.now() - lastGood.at < 1800000) return output(lastGood.xml); try { const paths = new Set(["/", "/records", "/report", "/status", "/rules", "/privacy", "/fish", "/waterbodies", "/media"]); for (const [endpoint, prefix] of [["fishes", "fish"], ["waterbodies", "waterbodies"]]) { for (let offset = 0; ; offset += 500) { const rows = await api(`/api/v1/${endpoint}?limit=500&offset=${offset}`); rows.forEach(row => paths.add(`/${prefix}/${row.slug}`)); if (paths.size > 49000) throw new Error("Sitemap index required"); if (rows.length < 500) break; } } for (let offset = 0; ; offset += 500) { const rows = await api(`/api/v1/public-spot-pages?limit=500&offset=${offset}`); rows.forEach(path => paths.add(path)); if (paths.size > 49000) throw new Error("Sitemap index required"); if (rows.length < 1000) break; } // Normalize paths: never use trailing slash (S03) const normalized = [...paths].map(p => p.replace(/\/$/, "") || "/"); const xml = `${normalized.map(path => `${escapeXml(new URL(path, origin).toString())}`).join("")}`; lastGood = { origin, xml, at: Date.now() }; return output(xml); } catch { if (lastGood?.origin === origin) return output(lastGood.xml); return new Response("Sitemap temporarily unavailable", { status: 503, headers: { "Retry-After": "60", "Cache-Control": "no-store" } }); } };