Files
rf4-spotter/apps/web/src/pages/sitemap.xml.ts
T
ik 3ea08fa706 S03: Consistent site origin — trailingSlash never, canonical URLs
- Add trailingSlash: 'never' to Astro config
- Normalize sitemap paths to never use trailing slash
- Ensures consistent canonical URLs across all pages
- Prevents duplicate content from / vs /path/ variants
2026-09-10 05:53:36 +07:00

37 lines
2.2 KiB
TypeScript

import type { APIRoute } from "astro";
import { api, type DictionaryItem } from "../lib/api";
const escapeXml = (value: string) => value.replace(/[<>&'"]/g, c => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", "'": "&apos;", '"': "&quot;" })[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"]);
for (const [endpoint, prefix] of [["fishes", "fish"], ["waterbodies", "waterbodies"]]) {
for (let offset = 0; ; offset += 500) {
const rows = await api<DictionaryItem[]>(`/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<string[]>(`/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 = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${normalized.map(path => `<url><loc>${escapeXml(new URL(path, origin).toString())}</loc></url>`).join("")}</urlset>`;
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" } });
}
};