SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
5.6 KB · 99 lines typescript
Raw Blame History
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');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