/** Site-wide constants (URL, name) used by metadata, sitemap and OG images. */ export const SITE_URL = (process.env.NEXT_PUBLIC_SITE_URL ?? 'https://www.countryatlas.co').replace(/\/$/, ''); export const SITE_NAME = 'CountryAtlas'; export const SITE_DOMAIN = 'countryatlas.co'; export const TAGLINE = 'Explore the world through data.'; export const OG_SIZE = { width: 1200, height: 630 } as const; /** Public API version shown in docs and the footer (path stays /api/v1; the minor tracks additive endpoints). */ export const API_VERSION = '1.1'; type Q = Record; function query(q?: Q): string { if (!q) return ''; const p = new URLSearchParams(); for (const [k, v] of Object.entries(q)) if (v !== undefined && v !== null && v !== '' && v !== false) p.set(k, String(v)); const s = p.toString(); return s ? `?${s}` : ''; } /** Route helpers — keep every internal href in one place so the next agent can add pages consistently. */ export const routes = { home: () => '/', countries: () => '/countries', country: (slug: string) => `/countries/${slug}`, countryTopic: (slug: string, topic: string) => `/countries/${slug}/${topic}`, countryIndicator: (slug: string, topic: string, indicator: string) => `/countries/${slug}/${topic}#${indicator}`, countryDownload: (id: string, fmt: 'csv' | 'json' = 'csv') => `/api/v1/countries/${id}/download.${fmt}`, compare: (...slugs: string[]) => (slugs.length ? `/compare/${slugs.join('/')}` : '/compare'), compareOg: (slugs: string[]) => `/compare/og?slugs=${slugs.join(',')}`, compareDownload: (ids: string[], indicators: string[], q: { from?: number | null; to?: number | null } = {}, fmt: 'csv' | 'json' = 'csv') => `/api/v1/compare/download.${fmt}?countries=${ids.join(',')}&indicators=${indicators.join(',')}${q.from != null ? `&from=${q.from}` : ''}${q.to != null ? `&to=${q.to}` : ''}`, rankings: () => '/rankings', ranking: (indicator: string, q?: { year?: number | null; group?: string | null }) => { const p = new URLSearchParams(); if (q?.year != null) p.set('year', String(q.year)); if (q?.group && q.group !== 'world') p.set('group', q.group); const s = p.toString(); return `/rankings/${indicator}${s ? `?${s}` : ''}`; }, indicators: (topic?: string) => (topic ? `/indicators?topic=${encodeURIComponent(topic)}` : '/indicators'), indicator: (slug: string) => `/indicators/${slug}`, indicatorDownload: (slug: string, fmt: 'csv' | 'json' = 'csv') => `/api/v1/indicators/${slug}/download.${fmt}`, regions: () => '/regions', region: (slug: string) => `/regions/${slug}`, regionCompare: (a: string, b: string) => `/regions/compare?a=${a}&b=${b}`, sources: () => '/sources', source: (id: string) => `/sources/${id}`, methodology: () => '/methodology', data: () => '/data', download: (q?: Q) => `/download${query(q)}`, api: () => '/api', changes: () => '/changes', /** World Explorer — full-viewport map + time slider. */ explore: (q?: { indicator?: string; year?: number | null; view?: 'map' | 'rank' | 'trend' | 'distribution'; country?: string | null }) => `/explore${query(q)}`, trajectories: (q?: { x?: string; y?: string; size?: string; year?: number | null; group?: string | null }) => `/trajectories${query(q)}`, scatter: (q?: { x?: string; y?: string; size?: string; year?: number | null; group?: string | null; log?: string }) => `/scatter${query(q)}`, finder: (q?: Q) => `/finder${query(q)}`, extremes: (q?: { window?: string; topic?: string }) => `/extremes${query(q)}`, stories: () => '/stories', story: (slug: string) => `/stories/${slug}`, updates: () => '/updates', peers: (q?: { y?: string; x?: string; year?: number | null }) => `/peers${query(q)}`, search: (q: string) => `/search?q=${encodeURIComponent(q)}`, apiSwagger: () => '/api/v1/docs', apiOpenapi: () => '/api/v1/openapi.json', admin: (section?: 'coverage' | 'runs' | 'issues' | 'raw') => (section ? `/admin/${section}` : '/admin'), adminLogin: () => '/admin/login', } as const;