Files
spsays-training/app/sitemap.ts
T
2026-06-15 14:04:00 +05:30

85 lines
2.7 KiB
TypeScript

import type { MetadataRoute } from "next";
import { bff } from "@/lib/bff";
/**
* /sitemap.xml — one file for the whole public corpus (~33k URLs, well under
* the 50,000-URL / 50 MB sitemap limits; the content is finite, so a single
* file is correct for the foreseeable future).
*
* Why a single file (not generateSitemaps): Next's generateSitemaps() serves
* shards at /sitemap/<id>.xml and does NOT expose an index at /sitemap.xml —
* but /sitemap.xml is exactly where robots.txt + search engines look. A plain
* default export is served at /sitemap.xml. (If the URL count ever nears 50k,
* switch to a sitemap index referencing sharded children.)
*
* How this stays cheap on the BFF:
* • Two projection-only BFF calls (`/sitemap/content`, `/sitemap/users`)
* return every URL in a single sweep each — no per-document fan-out.
* • The fetches are cached 24h (revalidate) AND the rendered XML is
* ISR-cached, so crawlers hit Next's cache, not the BFF (~one query/day).
* Tag "sitemap" lets the BFF force-refresh on new signups / re-seeds.
*/
export const revalidate = 86400; // 24h
const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL || "https://prabhupadasays.org";
// Top-level hubs that aren't content leaves but should be indexed.
const STATIC_PATHS = [
"/",
"/about",
"/books",
"/letters",
"/transcriptions",
"/disciples",
"/glossary",
"/essays",
"/magazines",
"/journals",
"/other-documents",
];
async function fetchUrls(path: string): Promise<string[]> {
try {
const res = await bff<{ urls: string[] }>(path, {
revalidate,
tags: ["sitemap"],
});
return res?.urls ?? [];
} catch {
return [];
}
}
/**
* All indexable paths: static hubs + book/division landing pages (derived
* from the content leaves) + every content leaf + public profiles. Resilient —
* a failed BFF call still yields the static hubs, so /sitemap.xml never 500s.
*/
async function allPaths(): Promise<string[]> {
const [content, users] = await Promise.all([
fetchUrls("/sitemap/content"),
fetchUrls("/sitemap/users"),
]);
const hubs = new Set<string>();
for (const p of content) {
const book = p.match(/^\/books\/([^/]+)/);
if (book) hubs.add(`/books/${book[1]}`);
const division = p.match(/^\/books\/(sb|cc)\/([^/]+)\/\d+\/\d+$/);
if (division) hubs.add(`/books/${division[1]}/${division[2]}`);
}
return [...new Set([...STATIC_PATHS, ...hubs, ...content, ...users])];
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const paths = await allPaths();
return paths.map((path) => ({
url: `${SITE_URL}${path}`,
changeFrequency: "weekly",
priority: path === "/" ? 1 : STATIC_PATHS.includes(path) ? 0.8 : 0.7,
}));
}