spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1/** Site-wide constants (URL, name) used by metadata, sitemap and OG images. */2export const SITE_URL = (process.env.NEXT_PUBLIC_SITE_URL ?? 'https://www.countryatlas.co').replace(/\/$/, '');3export const SITE_NAME = 'CountryAtlas';4export const SITE_DOMAIN = 'countryatlas.co';5export const TAGLINE = 'Explore the world through data.';6export const OG_SIZE = { width: 1200, height: 630 } as const;7/** Public API version shown in docs and the footer (path stays /api/v1; the minor tracks additive endpoints). */8export const API_VERSION = '1.1';910type Q = Record<string, string | number | boolean | null | undefined>;11function query(q?: Q): string {12 if (!q) return '';13 const p = new URLSearchParams();14 for (const [k, v] of Object.entries(q)) if (v !== undefined && v !== null && v !== '' && v !== false) p.set(k, String(v));15 const s = p.toString();16 return s ? `?${s}` : '';17}1819/** Route helpers — keep every internal href in one place so the next agent can add pages consistently. */20export const routes = {21 home: () => '/',22 countries: () => '/countries',23 country: (slug: string) => `/countries/${slug}`,24 countryTopic: (slug: string, topic: string) => `/countries/${slug}/${topic}`,25 countryIndicator: (slug: string, topic: string, indicator: string) => `/countries/${slug}/${topic}#${indicator}`,26 countryDownload: (id: string, fmt: 'csv' | 'json' = 'csv') => `/api/v1/countries/${id}/download.${fmt}`,27 compare: (...slugs: string[]) => (slugs.length ? `/compare/${slugs.join('/')}` : '/compare'),28 compareOg: (slugs: string[]) => `/compare/og?slugs=${slugs.join(',')}`,29 compareDownload: (ids: string[], indicators: string[], q: { from?: number | null; to?: number | null } = {}, fmt: 'csv' | 'json' = 'csv') =>30 `/api/v1/compare/download.${fmt}?countries=${ids.join(',')}&indicators=${indicators.join(',')}${q.from != null ? `&from=${q.from}` : ''}${q.to != null ? `&to=${q.to}` : ''}`,31 rankings: () => '/rankings',32 ranking: (indicator: string, q?: { year?: number | null; group?: string | null }) => {33 const p = new URLSearchParams();34 if (q?.year != null) p.set('year', String(q.year));35 if (q?.group && q.group !== 'world') p.set('group', q.group);36 const s = p.toString();37 return `/rankings/${indicator}${s ? `?${s}` : ''}`;38 },39 indicators: (topic?: string) => (topic ? `/indicators?topic=${encodeURIComponent(topic)}` : '/indicators'),40 indicator: (slug: string) => `/indicators/${slug}`,41 indicatorDownload: (slug: string, fmt: 'csv' | 'json' = 'csv') => `/api/v1/indicators/${slug}/download.${fmt}`,42 regions: () => '/regions',43 region: (slug: string) => `/regions/${slug}`,44 regionCompare: (a: string, b: string) => `/regions/compare?a=${a}&b=${b}`,45 sources: () => '/sources',46 source: (id: string) => `/sources/${id}`,47 methodology: () => '/methodology',48 data: () => '/data',49 download: (q?: Q) => `/download${query(q)}`,50 api: () => '/api',51 changes: () => '/changes',52 /** World Explorer — full-viewport map + time slider. */53 explore: (q?: { indicator?: string; year?: number | null; view?: 'map' | 'rank' | 'trend' | 'distribution'; country?: string | null }) => `/explore${query(q)}`,54 trajectories: (q?: { x?: string; y?: string; size?: string; year?: number | null; group?: string | null }) => `/trajectories${query(q)}`,55 scatter: (q?: { x?: string; y?: string; size?: string; year?: number | null; group?: string | null; log?: string }) => `/scatter${query(q)}`,56 finder: (q?: Q) => `/finder${query(q)}`,57 extremes: (q?: { window?: string; topic?: string }) => `/extremes${query(q)}`,58 stories: () => '/stories',59 story: (slug: string) => `/stories/${slug}`,60 updates: () => '/updates',61 peers: (q?: { y?: string; x?: string; year?: number | null }) => `/peers${query(q)}`,62 search: (q: string) => `/search?q=${encodeURIComponent(q)}`,63 apiSwagger: () => '/api/v1/docs',64 apiOpenapi: () => '/api/v1/openapi.json',65 admin: (section?: 'coverage' | 'runs' | 'issues' | 'raw') => (section ? `/admin/${section}` : '/admin'),66 adminLogin: () => '/admin/login',67} as const;68