import 'server-only';
import { api, safe } from '@/lib/api';
import { routes, SITE_URL, TYPE_PATH } from '@/lib/site';
export const SITEMAP_HEADERS = { 'content-type': 'application/xml; charset=utf-8', 'cache-control': 'public, s-maxage=3600, stale-while-revalidate=86400' };
export const SHARD = 5000;
/** Static public routes (1.1 surface). Query-driven pages are listed once at their canonical default URL. */
const STATIC = [
'/',
'/models',
'/companies',
'/papers',
'/providers',
'/benchmarks',
'/benchmarks?view=matrix',
'/hardware',
'/hardware/fit',
'/frameworks',
'/datasets',
'/tools',
'/agents',
'/families',
'/licenses',
'/open',
'/frontier',
'/prices',
'/pulse',
'/calculator',
'/run-locally',
'/find-a-model',
'/compare',
'/graph',
'/changes',
'/timeline',
'/time-machine',
'/diff',
'/explore',
'/methodology',
'/sources',
'/about',
'/developers',
'/bot',
];
/** Types with their own sitemap series (in this order); everything else goes into the "other" series. */
const SERIES = ['model', 'company', 'paper', 'provider', 'benchmark', 'hardware', 'framework', 'dataset', 'tool', 'repository', 'model_family', 'artifact', 'license'] as const;
function esc(s: string): string {
return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
export function toIndex(locs: string[]): string {
return `\n\n${locs.map((l) => ` ${esc(SITE_URL + l)}`).join('\n')}\n\n`;
}
export function toUrlset(entries: { loc: string; lastmod?: string }[]): string {
return `\n\n${entries.map((e) => ` ${esc(SITE_URL + e.loc)}${e.lastmod ? `${e.lastmod.slice(0, 10)}` : ''}`).join('\n')}\n\n`;
}
/** Shard ids: `static`, `changes` (daily digests of the observation history), then `-` per type with entities. */
export async function shardIds(): Promise {
const ids = ['static', 'changes'];
const stats = await safe(api.stats());
for (const t of SERIES) {
const n = Number(stats?.entities?.[t] ?? 0);
const shards = n > 0 ? Math.ceil(n / SHARD) : 0;
for (let i = 0; i < shards; i++) ids.push(`${t}-${i}`);
}
return ids;
}
/** One /changes/ URL per UTC day from the first entity to today (bounded to 3 years). */
async function changesEntries(): Promise<{ loc: string; lastmod?: string }[]> {
const stats = await safe(api.stats());
const first = stats?.first_entity_at ? new Date(stats.first_entity_at) : null;
if (!first || Number.isNaN(first.getTime())) return [];
const today = new Date();
const start = Math.max(first.getTime(), today.getTime() - 3 * 365 * 86400000);
const out: { loc: string; lastmod?: string }[] = [];
for (let t = start; t <= today.getTime(); t += 86400000) {
const day = new Date(t).toISOString().slice(0, 10);
out.push({ loc: routes.changesDay(day) });
}
return out;
}
export async function shardEntries(id: string): Promise<{ loc: string; lastmod?: string }[] | null> {
if (id === 'static') return STATIC.map((loc) => ({ loc }));
if (id === 'changes') return changesEntries();
const m = /^([a-z_]+)-(\d+)$/.exec(id);
if (!m) return null;
const type = m[1] as string;
const n = Number(m[2]);
if (!(SERIES as readonly string[]).includes(type) || !TYPE_PATH[type]) return null;
const res = await safe(api.sitemap(type, SHARD, n * SHARD));
if (!res) return [];
return res.items.map((it) => ({ loc: routes.entity({ entity_type: it.entity_type, slug: it.slug }), lastmod: it.updated_at }));
}