spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import 'server-only';2import { api, safe } from '@/lib/api';3import { SITE_URL, routes } from '@/lib/site';45/**6 * Sharded sitemap data. Shard 0 = static pages + non-satellite entities; shards 1..N = on-orbit satellites,7 * SATELLITES_PER_SHARD per shard (N from the API total). Served by `app/sitemap.xml/route.ts` (index) and8 * `app/sitemap/[id]/route.ts` (shards) — Next 16's `generateSitemaps` cannot emit an index at /sitemap.xml, which robots.txt requires.9 * If the API is unreachable a shard degrades to static pages only; URLs are never fabricated.10 */11export const SATELLITES_PER_SHARD = 5000;1213type ChangeFreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';14export interface SitemapEntry {15 url: string;16 lastModified?: Date;17 changeFrequency?: ChangeFreq;18 priority?: number;19}2021const STATIC: { path: string; priority: number; changeFrequency: ChangeFreq }[] = [22 { path: routes.home(), priority: 1, changeFrequency: 'hourly' },23 { path: routes.explore(), priority: 0.9, changeFrequency: 'hourly' },24 { path: routes.satellites(), priority: 0.9, changeFrequency: 'hourly' },25 { path: routes.constellations(), priority: 0.8, changeFrequency: 'daily' },26 { path: routes.operators(), priority: 0.8, changeFrequency: 'daily' },27 { path: routes.countries(), priority: 0.8, changeFrequency: 'daily' },28 { path: routes.launches(), priority: 0.8, changeFrequency: 'daily' },29 { path: routes.launchSites(), priority: 0.6, changeFrequency: 'weekly' },30 { path: routes.debris(), priority: 0.7, changeFrequency: 'daily' },31 { path: routes.reentries(), priority: 0.7, changeFrequency: 'daily' },32 { path: routes.events(), priority: 0.7, changeFrequency: 'hourly' },33 { path: routes.stats(), priority: 0.7, changeFrequency: 'daily' },34 { path: routes.rankings(), priority: 0.6, changeFrequency: 'daily' },35 { path: routes.sources(), priority: 0.5, changeFrequency: 'weekly' },36 { path: routes.methodology(), priority: 0.5, changeFrequency: 'monthly' },37 { path: routes.status(), priority: 0.3, changeFrequency: 'hourly' },38 { path: routes.developers(), priority: 0.5, changeFrequency: 'monthly' },39 { path: routes.about(), priority: 0.4, changeFrequency: 'monthly' },40 { path: routes.privacy(), priority: 0.2, changeFrequency: 'yearly' },41 { path: routes.terms(), priority: 0.2, changeFrequency: 'yearly' },42];4344const abs = (path: string) => `${SITE_URL}${path}`;45const date = (v: string | null | undefined) => (v && !Number.isNaN(new Date(v).getTime()) ? new Date(v) : undefined);4647/** Number of satellite shards (≥ 1 even when the API is down, so /sitemap/1.xml always exists). */48export async function satelliteShardCount(): Promise<number> {49 const first = await safe(api.sitemapSatellites(1, SATELLITES_PER_SHARD));50 const total = first?.data.total ?? 0;51 return Math.max(1, Math.ceil(total / SATELLITES_PER_SHARD));52}5354export async function shardEntries(shard: number): Promise<SitemapEntry[]> {55 if (shard === 0) {56 const out: SitemapEntry[] = STATIC.map((s) => ({ url: abs(s.path), changeFrequency: s.changeFrequency, priority: s.priority }));57 const ents = await safe(api.sitemapEntities());58 if (ents) {59 const d = ents.data;60 out.push(...d.constellations.map((c) => ({ url: abs(routes.constellation(c.slug)), lastModified: date(c.updated_at), changeFrequency: 'daily' as const, priority: 0.8 })));61 out.push(...d.operators.map((o) => ({ url: abs(routes.operator(o.slug)), lastModified: date(o.updated_at), changeFrequency: 'weekly' as const, priority: 0.7 })));62 out.push(...d.countries.map((c) => ({ url: abs(routes.country(c.slug)), changeFrequency: 'weekly' as const, priority: 0.7 })));63 out.push(...d.launches.map((l) => ({ url: abs(routes.launch(l.slug)), lastModified: date(l.updated_at), changeFrequency: 'weekly' as const, priority: 0.5 })));64 out.push(...d.launch_sites.map((s) => ({ url: abs(routes.launchSite(s.slug)), changeFrequency: 'monthly' as const, priority: 0.5 })));65 }66 return out;67 }68 const page = await safe(api.sitemapSatellites(shard, SATELLITES_PER_SHARD));69 if (!page) return [];70 return page.data.items.map((s) => ({71 url: abs(routes.satellite(s.slug)),72 lastModified: date(s.updated_at),73 changeFrequency: s.status === 'ACTIVE' ? 'daily' : 'weekly',74 priority: s.status === 'ACTIVE' ? 0.6 : 0.4,75 }));76}7778const esc = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');7980export function toUrlset(entries: SitemapEntry[]): string {81 const body = entries82 .map((e) => {83 const parts = [`<loc>${esc(e.url)}</loc>`];84 if (e.lastModified) parts.push(`<lastmod>${e.lastModified.toISOString()}</lastmod>`);85 if (e.changeFrequency) parts.push(`<changefreq>${e.changeFrequency}</changefreq>`);86 if (e.priority !== undefined) parts.push(`<priority>${e.priority}</priority>`);87 return `<url>${parts.join('')}</url>`;88 })89 .join('\n');90 return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>\n`;91}9293export function toIndex(shardIds: number[], lastmod: Date = new Date()): string {94 const body = shardIds.map((id) => `<sitemap><loc>${esc(`${SITE_URL}/sitemap/${id}.xml`)}</loc><lastmod>${lastmod.toISOString()}</lastmod></sitemap>`).join('\n');95 return `<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</sitemapindex>\n`;96}9798export const SITEMAP_HEADERS = { 'content-type': 'application/xml; charset=utf-8', 'cache-control': 'public, max-age=0, s-maxage=3600, stale-while-revalidate=86400' };99