import { SITE_NAME, SITE_URL, routes } from './site'; /** * SEO helpers shared by every page family: title patterns from the product spec and JSON-LD builders. * Titles are returned WITHOUT the site suffix when used through the layout template (`%s — CountryAtlas`); use * `withSite()` for places that need the full "… | CountryAtlas" string (OG, JSON-LD names). */ export const seoTitle = { country: (name: string) => `${name} Data, Economy, Population & Statistics`, countryTopic: (name: string, topic: string) => `${name} ${topic} — Data & Statistics`, indicator: (name: string) => `${name} by Country — Data & Rankings`, ranking: (name: string, year: number | string | null | undefined) => (year ? `${name} by Country — ${year} Ranking` : `${name} by Country — Ranking`), region: (name: string) => `${name} Data, Countries & Statistics`, compare: (names: string[]) => `${names.join(' vs ')} — Country Comparison`, story: (title: string) => `${title} — Data Story`, } as const; export function withSite(title: string): string { return `${title} | ${SITE_NAME}`; } export function absoluteUrl(path: string): string { return path.startsWith('http') ? path : `${SITE_URL}${path.startsWith('/') ? path : `/${path}`}`; } type JsonLd = Record; export const jsonLd = { website: (): JsonLd => ({ '@context': 'https://schema.org', '@type': 'WebSite', name: SITE_NAME, url: SITE_URL, potentialAction: { '@type': 'SearchAction', target: { '@type': 'EntryPoint', urlTemplate: `${SITE_URL}${routes.search('{search_term_string}')}`.replace('%7Bsearch_term_string%7D', '{search_term_string}') }, 'query-input': 'required name=search_term_string' }, }), organization: (): JsonLd => ({ '@context': 'https://schema.org', '@type': 'Organization', name: SITE_NAME, url: SITE_URL, logo: `${SITE_URL}/icon.png`, email: 'contact@spboucher.ai', founder: { '@type': 'Person', name: 'Simon-Pierre Boucher' }, }), breadcrumbs: (items: Array<{ name: string; path: string }>): JsonLd => ({ '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: items.map((it, i) => ({ '@type': 'ListItem', position: i + 1, name: it.name, item: absoluteUrl(it.path) })), }), /** schema.org Dataset for an indicator page. */ dataset: (d: { slug: string; name: string; description?: string | null; unit?: string | null; firstYear?: number | null; lastYear?: number | null; nCountries?: number | null; sources?: Array<{ name: string | null; url: string | null; licence?: string | null }>; modified?: string | null }): JsonLd => ({ '@context': 'https://schema.org', '@type': 'Dataset', name: `${d.name} by country`, description: d.description ?? `${d.name} for every country, with sources.`, url: absoluteUrl(routes.indicator(d.slug)), identifier: d.slug, isAccessibleForFree: true, license: 'https://creativecommons.org/licenses/by/4.0/', creator: { '@type': 'Organization', name: SITE_NAME, url: SITE_URL }, ...(d.modified ? { dateModified: d.modified } : {}), ...(d.firstYear && d.lastYear ? { temporalCoverage: `${d.firstYear}/${d.lastYear}` } : {}), ...(d.nCountries ? { spatialCoverage: `${d.nCountries} countries and territories` } : {}), variableMeasured: d.unit ? { '@type': 'PropertyValue', name: d.name, unitText: d.unit } : d.name, distribution: [ { '@type': 'DataDownload', encodingFormat: 'text/csv', contentUrl: absoluteUrl(routes.indicatorDownload(d.slug, 'csv')) }, { '@type': 'DataDownload', encodingFormat: 'application/json', contentUrl: absoluteUrl(routes.indicatorDownload(d.slug, 'json')) }, ], ...(d.sources?.length ? { isBasedOn: d.sources.filter((s) => s.url).map((s) => ({ '@type': 'Dataset', name: s.name ?? undefined, url: s.url })) } : {}), }), /** schema.org Article for a data story. */ article: (a: { slug: string; title: string; description: string; published: string; modified?: string | null; image?: string | null }): JsonLd => ({ '@context': 'https://schema.org', '@type': 'Article', headline: a.title, description: a.description, url: absoluteUrl(routes.story(a.slug)), datePublished: a.published, dateModified: a.modified ?? a.published, author: { '@type': 'Person', name: 'Simon-Pierre Boucher' }, publisher: { '@type': 'Organization', name: SITE_NAME, url: SITE_URL, logo: { '@type': 'ImageObject', url: `${SITE_URL}/icon.png` } }, ...(a.image ? { image: absoluteUrl(a.image) } : {}), isAccessibleForFree: true, }), /** schema.org Place/Country for a country page. */ country: (c: { slug: string; name: string; iso3: string; capital?: string | null }): JsonLd => ({ '@context': 'https://schema.org', '@type': 'Country', name: c.name, identifier: c.iso3, url: absoluteUrl(routes.country(c.slug)), ...(c.capital ? { containsPlace: { '@type': 'City', name: c.capital } } : {}), }), }; /** Serialise for a `