SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
4.7 KB · 83 lines typescript
Raw Blame History
1import 'server-only';2import { SITE_URL, NAV, MORE_NAV, FOOTER_NAV } from '@/lib/site';3import { cancerSlugsForSitemap, countActiveCancers } from '@/lib/queries/cancers';4import { geneSymbolsForSitemap, countGenes } from '@/lib/queries/genomics';5import { drugSlugsForSitemap, countDrugs } from '@/lib/queries/drugs';6import { trialNctForSitemap, countTrials } from '@/lib/queries/trials';7import { listSources } from '@/lib/queries/sources';8import { listMetrics } from '@/lib/queries/rankings';9import { biomarkerSlugsForSitemap } from '@/lib/queries/biomarkers';10import { toDate } from '@/lib/format';1112/**13 * Chunked sitemaps: chunk 0 = static pages + sources + rankings; then one chunk per 20k entities of14 * each kind (cancers, genes, drugs, trials) so the index stays under the 50k-URL limit per file.15 */16export const PAGE = 20_000;17type Kind = 'cancer' | 'gene' | 'drug' | 'trial';18export interface Chunk {19  id: number;20  kind: Kind;21  offset: number;22}23export interface Entry {24  url: string;25  lastModified: Date;26  changeFrequency: 'daily' | 'weekly';27  priority: number;28}2930export async function sitemapChunks(): Promise<Chunk[]> {31  const [c, g, d, t] = await Promise.all([countActiveCancers(), countGenes(), countDrugs(), countTrials()]);32  const out: Chunk[] = [];33  let id = 1;34  for (const [kind, n] of [35    ['cancer', c],36    ['gene', g],37    ['drug', d],38    ['trial', t],39  ] as Array<[Kind, number]>) {40    for (let off = 0; off < Math.max(n, 1); off += PAGE) out.push({ id: id++, kind, offset: off });41  }42  return out;43}4445export async function sitemapEntries(id: number): Promise<Entry[]> {46  const now = new Date();47  if (id === 0) {48    const [sources, metrics, biomarkers] = await Promise.all([listSources(), listMetrics(), biomarkerSlugsForSitemap()]);49    return [50      { url: SITE_URL, lastModified: now, changeFrequency: 'daily', priority: 1 },51      ...NAV.map((n) => ({ url: `${SITE_URL}${n.href}`, lastModified: now, changeFrequency: 'daily' as const, priority: 0.8 })),52      ...MORE_NAV.map((n) => ({ url: `${SITE_URL}${n.href}`, lastModified: now, changeFrequency: 'daily' as const, priority: 0.7 })),53      ...FOOTER_NAV.filter((f) => !(NAV as readonly { href: string }[]).some((n) => n.href === f.href) && !(MORE_NAV as readonly { href: string }[]).some((m) => m.href === f.href)).map((n) => ({ url: `${SITE_URL}${n.href}`, lastModified: now, changeFrequency: 'weekly' as const, priority: 0.5 })),54      ...sources.map((s) => ({ url: `${SITE_URL}/source/${s.slug}`, lastModified: toDate(s.updated_at) ?? now, changeFrequency: 'weekly' as const, priority: 0.6 })),55      ...metrics.filter((m) => m.snapshot_count > 0).map((m) => ({ url: `${SITE_URL}/rankings/${m.slug}`, lastModified: now, changeFrequency: 'daily' as const, priority: 0.7 })),56      ...biomarkers.map((b) => ({ url: `${SITE_URL}/biomarker/${b.slug}`, lastModified: toDate(b.updated_at) ?? now, changeFrequency: 'weekly' as const, priority: 0.6 })),57    ];58  }59  const chunk = (await sitemapChunks()).find((c) => c.id === id);60  if (!chunk) return [];61  switch (chunk.kind) {62    case 'cancer':63      return (await cancerSlugsForSitemap(chunk.offset, PAGE)).map((r) => ({ url: `${SITE_URL}/cancer/${r.slug}`, lastModified: toDate(r.updated_at) ?? now, changeFrequency: 'weekly' as const, priority: 0.7 }));64    case 'gene':65      return (await geneSymbolsForSitemap(chunk.offset, PAGE)).map((r) => ({ url: `${SITE_URL}/gene/${r.symbol}`, lastModified: toDate(r.updated_at) ?? now, changeFrequency: 'weekly' as const, priority: 0.5 }));66    case 'drug':67      return (await drugSlugsForSitemap(chunk.offset, PAGE)).map((r) => ({ url: `${SITE_URL}/drug/${r.slug}`, lastModified: toDate(r.updated_at) ?? now, changeFrequency: 'weekly' as const, priority: 0.5 }));68    case 'trial':69      return (await trialNctForSitemap(chunk.offset, PAGE)).map((r) => ({ url: `${SITE_URL}/trial/${r.nct_id}`, lastModified: toDate(r.updated_at) ?? now, changeFrequency: 'weekly' as const, priority: 0.4 }));70  }71}7273const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');7475export function urlsetXml(entries: Entry[]): string {76  return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries.map((e) => `  <url><loc>${esc(e.url)}</loc><lastmod>${e.lastModified.toISOString()}</lastmod><changefreq>${e.changeFrequency}</changefreq><priority>${e.priority}</priority></url>`).join('\n')}\n</urlset>\n`;77}7879export function indexXml(ids: number[]): string {80  const now = new Date().toISOString();81  return `<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${ids.map((id) => `  <sitemap><loc>${SITE_URL}/sitemap/${id}</loc><lastmod>${now}</lastmod></sitemap>`).join('\n')}\n</sitemapindex>\n`;82}83