import type { MetadataRoute } from 'next'; import { api, safe } from '@/lib/api'; import { apiCompare } from '@/lib/api-compare'; import { apiExplore } from '@/lib/api-explore'; import { SITE_URL, routes } from '@/lib/site'; import { STORIES } from '@/lib/stories'; import { TOPICS } from '@/lib/topics'; /** * Sharded sitemap (Next `generateSitemaps`): one file per URL family so no shard grows past a few thousand URLs. * Emitted at /sitemap/.xml; robots.ts lists every shard. Parameterised analytical views (/explore?…, * /scatter?…, /finder?…) are deliberately absent: they are noindex and their canonical is the bare path. */ export const SITEMAP_IDS = ['core', 'countries', 'country-topics', 'indicators', 'rankings', 'regions', 'stories'] as const; export type SitemapId = (typeof SITEMAP_IDS)[number]; export async function generateSitemaps(): Promise> { return SITEMAP_IDS.map((id) => ({ id })); } export default async function sitemap(props: { id: Promise | string }): Promise { const id = (await props.id) as SitemapId; const health = await safe(api.health()); const lastModified = health?.built_at ? new Date(health.built_at) : new Date(); const entry = (path: string, changeFrequency: MetadataRoute.Sitemap[number]['changeFrequency'], priority: number): MetadataRoute.Sitemap[number] => ({ url: `${SITE_URL}${path}`, lastModified, changeFrequency, priority }); switch (id) { case 'core': return [ entry('/', 'daily', 1), entry(routes.explore(), 'daily', 0.9), entry(routes.countries(), 'daily', 0.9), entry(routes.compare(), 'monthly', 0.7), entry(routes.rankings(), 'daily', 0.9), entry(routes.indicators(), 'daily', 0.9), entry(routes.changes(), 'daily', 0.6), entry(routes.regions(), 'weekly', 0.7), entry(routes.trajectories(), 'monthly', 0.6), entry(routes.scatter(), 'monthly', 0.6), entry(routes.finder(), 'monthly', 0.6), entry(routes.extremes(), 'weekly', 0.6), entry(routes.peers(), 'monthly', 0.5), entry(routes.stories(), 'weekly', 0.7), entry(routes.download(), 'monthly', 0.5), entry(routes.updates(), 'daily', 0.4), entry(routes.sources(), 'weekly', 0.5), entry(routes.methodology(), 'monthly', 0.5), entry(routes.api(), 'monthly', 0.5), ...((await safe(apiExplore.sources()))?.items ?? []).map((s) => entry(routes.source(s.id), 'weekly', 0.4)), ]; case 'countries': { const res = await safe(api.countries()); return (res?.items ?? []).map((c) => entry(routes.country(c.slug ?? c.id), 'weekly', 0.8)); } case 'country-topics': { const res = await safe(api.countries()); return (res?.items ?? []).flatMap((c) => TOPICS.map((tp) => entry(routes.countryTopic(c.slug ?? c.id, tp.id), 'weekly', 0.6))); } case 'indicators': { const res = await safe(apiExplore.indicators()); return (res?.items ?? []).map((i) => entry(routes.indicator(i.slug), 'weekly', 0.7)); } case 'rankings': { const res = await safe(apiCompare.rankings()); return (res?.items ?? []).map((i) => entry(routes.ranking(i.slug), 'weekly', 0.7)); } case 'regions': { const res = await safe(apiExplore.regions()); return (res?.items ?? []).map((g) => entry(routes.region(g.slug ?? g.id), 'weekly', 0.6)); } case 'stories': return STORIES.map((s) => entry(routes.story(s.slug), 'weekly', 0.7)); default: return []; } }