Lexiverse
Lexiverse - AI contextual reading engine for English learners. Your next article is your next review.
An interactive explorer of Africa's 54 states: names, flags, national symbols, mountains and gold.
Translation pending
EducationTypeScript · PythonGitHub @emtatudatatechAvailability needs another checkOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Bara La Afrika (Swahili for "the continent of Africa") is a long-running project for uncovering, learning about and understanding Africa from historical and contemporary perspectives. It is a responsive web app with four sections:
| Section | Route | What it shows |
|---|---|---|
| Home | / | Interactive Equal Earth map of Africa, countries coloured by their UN M49 region |
| Countries | /countries | All 54 states: the meaning of their names, flags, national symbols and national animals (from the project's research document) |
| Harim | /harim | Africa's mountains and their heights, country by country, with ranges, peak pages and exports |
| Dhahabu | /dhahabu | Gold in Africa: where it is found, how much is mined and traded, and how much central banks hold |
| Vinara | /vinara | The cardinals of Africa: the leading state and the runner-up on 13 measures, in Africa and in each region, whether the lead is narrowing, and the next elections |
History appears in the navigation as "Coming soon".
Live site: https://baralaafrika.netlify.app. Data is refreshed weekly by a GitHub Actions workflow; every figure on the site carries its own date, and the strip at the top of each page shows the sources behind it.
Browser (React + Vite SPA)
│ /api/* (JSON, zod-validated, CDN-cached)
▼
Netlify Functions (Node, TypeScript) ── read-only role ──▶ Neon Postgres
▲
Python pipelines (uv, GitHub Actions cron) ── write role ────┘
zod; every error uses the shape { error: { code, message } }. Responses send Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400 plus an ETag.uv. Every pipeline is idempotent, logs its runs and records dataset coverage.packages/shared holds the API types, schemas and constants used by both the web app and the functions. Unit constants (e.g. troy ounces per tonne) are defined once in config/constants.json and read from TypeScript, Python and SQL.apps/web/ React + Vite app
index.html Real HTML entry, incl. the no-flash theme bootstrap
build/ build steps: the CSP (_headers), page shells with route preloads,
sitemap.xml and robots.txt, and the prerender of the main pages
public/ favicon (Africa silhouette), boot.js, social images, map topojson, flags
src/
app/ router, layout shell, theme provider, error boundaries
features/ map, countries, harim, dhahabu, vinara, coverage (data ribbon)
components/ Icon, IconButton, GlassCard, PageHeader, Tooltip, Drawer, RegionFilter…
styles/ tokens.css, glass.css, base.css, media.css, fonts.css
lib/ strings (i18n dictionary), api client, units (m/ft), storage, theme
packages/shared/ types, zod schemas and constants shared by web + functions
netlify/functions/ API functions (_shared/ holds HTTP and DB helpers)
netlify/tests/ function tests (kept outside functions/ so they never deploy)
db/migrations/ numbered .sql migrations
db/roles.sql one-time read/write role setup (run in the Neon SQL editor)
db/tests/ schema and seed tests against PGlite
db/seeds/ generated, reviewable JSON seeds (committed)
pipelines/ Python package: `python -m pipelines <command>`
scripts/ build helpers (Material Symbols subsetting, favicon, social images)
config/constants.json single source of truth for unit constants
e2e/ Playwright suites (desktop 1440×900, mobile 390×844)
data/source/ the research PDF (raw downloads go to data/raw/, gitignored)
docs/ statistic definitions (harim-stats.md, dhahabu-stats.md), the Vinara method,
and gold and Vinara sources and licences
data/*.md review reports: research document, region colours, boundaries, mountains, gold
| Tool | Version | Install (macOS) |
|---|---|---|
| Node.js | 24 (see .nvmrc; ≥ 24.0) | brew install node@24 or nvm |
| Python | 3.15 (see .python-version) | brew install [email protected] |
| uv | current | brew install uv |
| cairo | current (native lib for flag rasterisation) | brew install cairo |
On Linux, install libcairo2 instead of cairo.
npm install # all JS workspaces
uv sync # Python venv for pipelines
npx playwright install chromium # browser for end-to-end tests (one-time)
npm run dev:local # site + API with the committed seed data: http://localhost:5173
Three ways to run the site locally:
| Command | Open | Data |
|---|---|---|
npm run dev:local | http://localhost:5173 | The real API handlers on an in-memory Postgres loaded with the committed seeds (e2e/api-server.ts, port 4175), behind Vite's proxy. No database or .env needed. Ctrl+C stops both. |
npm run dev:netlify | http://localhost:8888 | The Netlify Functions emulator on your Neon database (DATABASE_URL_READ from .env). The CLI injects every .env variable locally, but the functions only ever read DATABASE_URL_READ. |
npm run dev | http://localhost:5173 | None: the site alone, for layout work. Where data would load, development builds explain which command to run instead. |
netlify dev and the production CSP: the build writes the Content-Security-Policy to apps/web/dist/_headers, and Netlify merges <publish>/_headers into its config. Under netlify dev that CSP would block Vite's inline development scripts and leave a blank page, so netlify.toml points publish at apps/web/public in the dev context. Deploys still use apps/web/dist and its CSP (a test guards this).
Local netlify dev quirk: after a 404 from a function, the emulator retries the same path with .html/index.html suffixes (to mimic static-file lookup), so a not-found message may name a suffixed path. Production returns the function's response as is.
The pipelines run from the repo root (uv run python -m pipelines --help); see the next section.
db/roles.sql after replacing its two placeholder passwords. It creates bla_writer (pipelines) and bla_reader (read-only, for the Netlify Functions).bla_writer → DATABASE_URL_WRITE in your local .env and the GitHub Actions secrets; bla_reader → DATABASE_URL_READ in the Netlify site settings.| Step | Command | What it does |
|---|---|---|
| 1 | uv run python -m pipelines migrate | Applies db/migrations/*.sql in order (checksummed; an applied file that was edited is an error) and grants SELECT to bla_reader |
| 2 | uv run python -m pipelines extract-pdf | Research PDF → db/seeds/{countries,sources,datasets}.json and data/review_report.md, with loud validation (54 countries; UN M49 region counts 6/16/9/18/5; the PDF's own headings checked against the document's grouping; unique ISO codes; every summary field present; no stray citation digits) |
| 3 | uv run python -m pipelines flags | Downloads the 54 flag SVGs from flagcdn.com (cached in data/raw/flags/, copied to apps/web/public/flags/ as the offline fallback), computes each flag's palette and each region's colour, and writes db/seeds/flags.json and data/region_colors_report.md |
| 4 | uv run python -m pipelines boundaries | Natural Earth 4.1.0 (1:50m, via world-atlas 2.0.2) → apps/web/public/geo/africa.topo.json (the 54 countries, Somaliland tagged as Somalia, Western Sahara as disputed) and africa-context.topo.json (neighbouring land, loaded after the countries); computes each country's area on the equal-area Equal Earth projection, a map anchor and whether it needs a circle marker; writes db/seeds/boundaries.json and data/boundaries_report.md |
| 5 | uv run python -m pipelines mountains | Builds the Harim catalogue (see Mountain data) → db/seeds/{mountains,mountain_ranges,country_low_points}.json and data/mountains_review_report.md |
| 6 | uv run python -m pipelines gold | Builds the Dhahabu data (see Gold data) → db/seeds/gold_*.json, data/gold_coverage_matrix.md and data/gold_review_report.md (needs COMTRADE_API_KEY) |
uv run python -m pipelines status prints every dataset's "as of" date, last refresh, cadence and last run (flagging overdue datasets).
migrate --dry-run lists pending migrations. extract-pdf --check validates without writing and fails if the committed seeds are stale; the pytest suite runs it.
python -m pipelines mountains (pipelines/africa_pipelines/harim*.py) builds a catalogue covering all 54 countries. Every download is cached under data/raw/ (gitignored), so re-runs are reproducible and offline; --refresh fetches again.
harim.html and README.md), which decides which peaks exist;volcanoes section, each with a quoted source (Mount Kenya: an extinct stratovolcano). Status (active, dormant, extinct) is recorded only where a source states it (a Wikidata class or the curated source); nothing is inferred, so most statuses are unknown.elevation_alternatives. Confidence is high when they agree within 10 m, medium within 50 m (or with one reliable source), low otherwise; any disagreement above 10 m, including the prototype's, sets needs_review. Values that bulk-imported Wikidata items copied from GeoNames count as GeoNames.python -m pipelines gold runs seven steps (pipelines/africa_pipelines/gold_*.py), each also a command of its own. Every download is cached under data/raw/gold/; --refresh fetches again. Sources, licences and the decisions behind them are in docs/dhahabu-sources.md: only sources usable without asking permission are used (the British Geological Survey and the World Gold Council are not).
| Step | Source | Output |
|---|---|---|
gold-price | World Bank Pink Sheet, monthly average (IMF PCPS as cross-check and fallback) | gold_price.json: 801 months from 1960 |
gold-production | USGS Mineral Commodity Summaries 2022–2026 and the Minerals Yearbook country chapters (about 200 workbooks) | gold_production.json: every edition's figures, "included in other" rows, in-ground reserves, and the evidence each chapter gives (notes, artisanal splits, operations) |
gold-official-reserves | IMF International Liquidity, one call for the 54 countries | gold_reserves.json: monthly holdings since 2000, valued at each month's price, and who reports late |
gold-trade | UN Comtrade (HS 7108 without 7108.20), reported and mirror, 111 calls | gold_trade.json: flows, top 10 partners, total exports |
gold-mines | Wikidata | gold_mines.json: candidate mines and the quality-bar verdict (the layer is off: it fails all three checks) |
gold-presence | the above, plus db/seeds/gold_presence_overrides.json | gold_presence.json: each country's tier, evidence, confidence and source |
gold-profiles | the above, plus db/seeds/gold_profile_notes.json | gold_profiles.json, then validation and the two reports |
included_in_other with no tonnes (and only where the yearbook shows it producing).v_gold_production prefers a figure over "included in other", a reported figure over an estimate, then the newer edition (gold_production_source_priority), and values it at that year's mean monthly price (empty until all twelve months exist).data/gold_coverage_matrix.md (country × metric, with the years available) and data/gold_review_report.md (tiers with their evidence, source conflicts, lumped countries, late reporters, trade gaps, the mines verdict, figures left out, what needs review).0007_dhahabu.sql (tables, v_gold_price_annual, v_gold_production, and troy_oz_per_tonne(), which a test checks against config/constants.json). Migration 0008_dhahabu_api.sql adds world production (the MCS world totals) and each central bank's reporting status (gold_reserves_reporting), gold_country_metrics(year) with v_gold_country_latest (each metric's latest value in or before a year, with its own year or date), and v_gold_stats(region, year), the statistics band (definitions in docs/dhahabu-stats.md, which a test keeps word for word in step with the SQL). Migration 0009_dhahabu_charts.sql adds gold_price_effect() and , the split of a change in holdings value into a price effect and a volume effect (they sum exactly to the change, by construction), and the 11th card, "Price effect" (the 0008 function is renamed and adds the card to its ten).python -m pipelines indicators (pipelines/africa_pipelines/indicators*.py) reads annual figures for the 54 states from 2000. Every download is cached under data/raw/indicators/; --refresh fetches again. What each series is, and its code at the source, is in pipelines/africa_pipelines/reference/indicators_config.json; sources and licences are in docs/vinara-sources.md and the rules in docs/vinara-method.md.
| Indicators | Source |
|---|---|
| Population and its growth, land and surface area, GDP (US$ and PPP), GDP per capita (PPP), real growth, inflation, GNI | World Bank WDI (CC BY 4.0) |
| Government gross debt (% of GDP) | IMF World Economic Outlook |
| Military spending (US$ and % of GDP) | SIPRI, read through WDI; only the latest ten years are kept (fair use) |
| Human Development Index | UNDP Human Development Report (CC BY 3.0 IGO); when UNDP's site does not answer, Our World in Data's copy of the same series |
| External debt, public external debt, and what is owed to multilateral lenders, governments, bondholders, commercial banks and other private lenders; each creditor's share | World Bank International Debt Statistics (CC BY 4.0) |
data/indicators_coverage_matrix.md (each country's latest year per indicator) and data/indicators_review_report.md (coverage and each indicator's default ten-year window, gaps, the IMF estimates used, where the World Bank and the IMF disagree, SIPRI's years, external debt coverage and the largest creditors, and extreme values to check).0011_indicators.sql: indicators (definition, unit, display precision, source code and dataset), indicator_observations (value, reported or estimate, source and edition) and external_debt_creditors.python -m pipelines politics (pipelines/africa_pipelines/politics*.py) reads two Wikipedia lists through the MediaWiki API: List of current heads of state and government and List of next general elections. Sources and licences are in docs/vinara-sources.md; the rules are in docs/vinara-method.md.
election_status() (migration 0012_politics.sql) gives "Election within 12 months", "No election within 12 months" or "No date set" from today's date whenever it is read.pipelines/africa_pipelines/reference/politics_curation.json holds country names as the lists spell them, the executive override for Sudan's collective council, Eritrea's lack of elections, and short notes (Somalia's disputed presidency, Libya's and Sudan's rival authorities, Cabo Verde's acting president, the monarchies). Every note and override rests on quotes from the lists, which each run re-checks.reference/politics_approved.json. Every run lists the differences from it in data/politics_review_report.md, and load keeps the approved records until politics --approve records new ones. The weekly refresh never approves.country_leaders and country_elections, with election_status(next_date, today).python -m pipelines fx (pipelines/africa_pipelines/fx*.py) reads rates in local currency per US dollar from 1 January 2020 (the floor for exchange rates) to the day before the run. Sources and licences are in docs/vinara-sources.md (§4, 5 and 12); the reference of currencies, pegs, currency changes and daily sources is pipelines/africa_pipelines/reference/currencies.json.
data/fx_review_report.md lists, per state, its daily source and span, its monthly span and status, and why a state is monthly only; how each currency change was applied; the peg and daily checks; and every month a currency moved more than 50% against the dollar.0014_fx.sql: country_currencies, currency_changes, fx_monthly, fx_daily (with each rate's source) and fx_reporting.Every Vinara rule is computed in the database (migration 0015_vinara.sql), from the indicators and exchange rates above; there is no pipeline step. The rules are in docs/vinara-method.md, and each metric's definition there is word for word the one the database returns (a test keeps them identical).
vinara_metrics: the 13 ranked metrics, each with how it is measured over the window, which way leads, its precision (values equal at it tie), how a region's figure is made, and its definition.v_fx_month: each state's monthly rate on one basis for ranking (the mean of its daily rates where it has them, otherwise the IMF's monthly average).vinara_window(metric, from, to): the default window (the last ten complete years) or a range asked for, kept within the data.vinara_rankings(metric, region, from, to): the states ranked, with ties sharing a rank; vinara_signal(...): whether the cardinal's lead is narrowing; vinara_region_values(...): each region's figure, its median over states and the share it covers.vinara_summary(region, from, to, metric): everything the page needs for each metric in one call. It holds intermediate rankings as JSON rather than temporary tables, so it runs for the read-only API role (a test runs it in a read-only transaction).0016_vinara_speed.sql redefines the per-state and regional functions with the same results, so that each metric runs only the steps it needs: the exchange-rate steps for the two currency metrics, and each regional step for the metrics that use it.0017_vinara_currency_window.sql ends the currency metrics' default window at the latest full year of rates any state has, so they show the latest rates; states without rates for its last month are listed as not ranked.Each region's identity colour is the mode colour across its flags: flags are rasterised, clustered into at most 8 colours in CIELAB, merged across flags (CIEDE2000 < 10) and summed per region; white, black and grey are excluded by default, and near-identical picks are resolved by giving the colour to the region where it has the larger share. Anti-aliasing blends at stripe edges are folded back into the real flag colours first. Every parameter is a command-line option (python -m pipelines flags --help), and --include-neutrals switches which result is selected (both are always reported).
data/region_colors_report.md shows the picks with swatches, the top five candidates per region, every uniqueness decision, both results, per-flag palettes and automated checks (distinctness, colour-vision-deficiency simulation, contrast against the light and dark page). Colours are never adjusted by hand in the computation; an adjustment the owner approves is recorded with its reason in the approval file.
Approval. The approved colours (2026-10-22: Northern #c1272d, Western #55d27a, Central #fcd116, Eastern #006600, Southern #6da9d2) are recorded in pipelines/africa_pipelines/reference/region_colors_approval.json, and load writes only those to regions.color_hex. The file keeps the approved computation separately from the colours in use: Western Africa's computed #009e49 is lightened to #55d27a (same hue) so it stands apart from Eastern Africa's green, and the report names that adjustment with its reason. If a later flags run computes different colours, the approved ones stay in use and the report says the computation differs, until a new approval is recorded there.
The research document is not fully verified (see data/review_report.md). To correct it:
db/seeds/countries.json (or sources.json). For example, fix a summary, set etymology_category, change animal_status, or set needs_review to false once a record is checked.uv run python -m pipelines load.extract-pdf refuses to overwrite seeds you have edited (it compares a content hash recorded at generation time) unless you pass --force.
How the extraction works and where its judgement calls live:
pipelines/africa_pipelines/reference/african_states.json: canonical names (with diacritics), ISO codes, UN M49 regions (with the research document's own grouping for the five states it files differently), and every label the PDF uses for each country.pipelines/africa_pipelines/reference/symbology_curation.json: the reviewed mapping of each country's prose sentences to profile fields, the animal-status decisions with their reasons, the reviewed etymology categories, the known inconsistencies (each marked corrected, status changed or open) and the verified corrections. Each correction replaces or appends exact text, gives its reason and cites the reference source it was checked against; that source is added to sources and cited on the corrected field. If a correction's original text stops matching, extraction fails rather than silently skipping it. The review report lists every correction with its before and after text.pipelines/africa_pipelines/reference/source_tiers.json: the reliability tier for each cited website..github/workflows/data-refresh.yml runs every Monday at 03:17 UTC (and on demand): migrate → flags --refresh → mountains --refresh, the gold steps and indicators --refresh (first Monday of the month, or on a manual run; gold production only on a manual run), fx --refresh (weekly), politics --refresh (weekly; changes wait for approval) → load → status, then calls the Netlify build hook if load changed any rows. If upstream data changed the generated seeds, the run warns and uploads them (with the colour, mountain, gold, indicator, exchange-rate and politics reports) as the refreshed-data artifact for you to review and commit; the approved region colours are never replaced automatically. Repository secrets: DATABASE_URL_WRITE (required), COMTRADE_API_KEY (for gold trade; without it the step is skipped with a warning) and NETLIFY_BUILD_HOOK (optional).
Tests never need a database server. Vitest runs the migrations and loads the seeds into PGlite (Postgres compiled to WebAssembly, in-process). pytest starts PGlite's socket server so the Python migration runner is tested over the real Postgres wire protocol.
Netlify Functions in netlify/functions/, one file per route, each declaring its route with config.path (functions are matched before redirects; the /api/* → /.netlify/functions/:splat rewrite in netlify.toml remains the catch-all). All responses are JSON; request parameters are validated with zod against the shared contract in packages/shared/src/schemas.ts.
| Route | Returns |
|---|---|
GET /api/regions | The five UN M49 regions in display order: name, description, approved colorHex, country count |
GET /api/countries?region=<slug>&q=<text> | Country summaries (all 54 without parameters): region, flag URLs (Flagpedia SVG, PNG template, self-hosted fallback), short profile fields, animal status, needsReview, and facts from v_country_facts (migration 0013): latest population and land area with their years, leaders (executive first), and the next election with its status as of the request. q matches names, official names and ISO codes, accent-insensitively ("cote" finds Côte d'Ivoire) |
GET /api/countries/:iso3 | The full profile: long texts, scientific name, review notes and every cited source with its reliability tier and the fields it supports. Unknown or malformed codes are 404 |
GET /api/coverage?datasets=a,b | Each dataset's provider, licence, attribution, coverage, "as of" date and last refresh; requested keys with no data yet are listed in missing |
GET /api/mountains/summary?region=&q= | One row per country (54), highest peak first: the highest peak (with a massif's top summit and the countries it is shared with), number of mountains, mean height, ranges, lowest point and relief |
GET /api/mountains?region=&country=&range=&q=&minHeight=&volcanic=&sort= | The catalogue (counted mountains: standalone and massifs, each once; summits are nested in their massif). country lists a shared peak under each of its countries; volcanic=1 any volcano, or active/dormant/extinct/unknown; sort=height (default), -height, prominence (unknown last) or name. territories=1 lists the territory peaks instead; summits=1 adds summits as rows. Returns count, total and capped (the list is capped at 600) |
GET /api/mountains/:slug | One mountain: height with its source and every other sourced height, prominence, isolation, coordinates, range, type, volcanic status, massif and summits, local names and name origin (each with its source), notes, review notes and sources by field. Unknown or malformed slugs are 404 |
{ "error": { "code", "message" } }: bad_request (400, invalid query parameters), not_found (404), method_not_allowed (405; only GET and HEAD), service_unavailable (503, database not configured) and internal_error (500; details are logged, never returned).Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400 with a weak ETag (304 on If-None-Match). Errors are no-store.bla_reader role (DATABASE_URL_READ); every query is parameterised (netlify/functions/_shared/queries.ts and harimQueries.ts). Search uses the search_fold() SQL function and the v_country_summary view (migration 0002). The Harim routes read v_mountains, v_country_mountain_summary and v_harim_stats() (migration 0004; 0005 adds each height's confidence and other sourced heights to the statistics, and names the region in the lowest point's label). Every statistic's definition and tie rule is in docs/harim-stats.md, word for word as the SQL returns it (a test keeps them identical).vinara_summary(), vinara_rankings() and vinara_country_years() (migration 0015) through netlify/functions/_shared/vinaraQueries.ts, which only shapes their rows./api/mountains/:slug excludes the fixed paths (summary, stats, export, ranges), and a test checks every API path reaches exactly one function.Response (the export), with the same caching and ETag as JSON._shared/contract.ts), so Netlify's bundler inlines it. npm run check:functions builds the functions exactly as a deploy does and checks every bundle loads and answers.apps/web/src/lib/api.ts (typed by the same contract).The home page (apps/web/src/features/map/) draws Africa on the Equal Earth projection (d3-geo), fitted to a box from Cabo Verde to Mauritius (far islands such as South Africa's Prince Edward Islands stay reachable by panning).
--region-<slug>), set from /api/regions, with the same colours in tokens.css as no-flash fallbacks (a test keeps them in sync with the approval).prefersLightLine, lib/colour.ts); the dark theme lifts those fills, so its borders stay dark.darkThemePalette() (lib/colour.ts) keeps every hue. It lifts a colour's OKLCH lightness only to reach 3:1 against the dark map ocean (WCAG 1.4.11), then lifts the lighter of any pair closer than ΔE 30 until they separate. With the approved colours this changes only East (#72b6fe) and Southern (#2b5acd)./countries?region=…./countries (apps/web/src/features/countries/) lists all 54 countries with their flag and region, in two views:
Symbols (the default): name meaning, national symbol and national animal.
Facts (?view=facts): population and land area, each with its year; the leader holding executive power (title above the name), then the other of head of state and head of government; and the next election that decides the executive, with its status ("Election within 12 months", "No election within 12 months" or "No date set", computed from today's date). Transitional and de facto leaders are labelled, and an "i" button explains a country's political context (Somalia's disputed presidency, Libya's and Sudan's rival authorities). A month or year alone reads as a deadline ("by Jul 2031").
Filters live in the URL, so every view can be shared: ?view=facts, ?region=<slug>, ?q=<search>, ?sort=<column> (a leading - sorts descending, e.g. sort=-animal) and ?country=<ISO3> (opens that country's drawer). Invalid values are ignored. Filter changes replace the current history entry rather than adding one.
Region chips in the region colours, plus "All", each with its country count.
Search ignores case and accents ("cote" finds Côte d'Ivoire). It matches names, official names, ISO codes, leaders and the symbol columns, so "eagle" finds the countries whose animal is an eagle and "ruto" finds Kenya. The list loads once (shared with the map) and filters instantly; the API's q parameter matches names and codes only.
Sorting: every column but the flag. Population and land area sort largest first on the first click, and elections soonest first; countries without a figure or a date always sort last. Ties sort by country name; regions sort in their display order (Northern → Southern). Phones get a "Sort by" menu for the view's columns, and cards that expand to the view's fields.
Flags come from Flagpedia (flagcdn.com, 1x/2x, explicit size, lazy-loaded, self-hosted fallback). Hovering or focusing a flag shows the document's flag text (summary, then the full description); a tap toggles it on touch screens. Escape dismisses it, and it stays inside the viewport.
Animal status: anything short of official (de facto, inferred, sports nickname, contested) is badged in the table, so inference never reads as fact.
Drawer: a click on a row (or Enter on the country name) opens the full profile in a modal dialog: a side panel on wide screens, a bottom sheet on phones. It shows the long texts, the etymology category (when assigned), the animal status with its meaning, every source with its reliability tier and the fields it supports, and a "Needs verification" marker that expands to the review notes. Somalia's drawer explains Somaliland. Escape, the close button or a click on the backdrop close it, and focus returns to where it was.
/harim (apps/web/src/features/harim/) answers "what are Africa's mountains, how high are they, and where are they, country by country?"
/countries.features/harim/stats/): nine glass cards under the header, all figures from /api/mountains/stats (computed in the database; definitions in docs/harim-stats.md).
/dhahabu (apps/web/src/features/dhahabu/) covers, for each of the 54 countries, whether gold is found, how much is mined and traded, and how much central banks hold. It is the site's time-series page: the ribbon's year range drives its cards, table, map and charts.
year. A note above the table says which applies.GoldStatsBand.tsx, from /api/gold/stats; definitions and tie rules in docs/dhahabu-stats.md): eleven glass cards, each with a year or month chip, the definition behind "How is this calculated?", count-up (none with reduced motion) and an action.
GoldTable.tsx): flag, country with region chip, presence badge, mine production, gold exports, central bank gold and share of exports. A click on a row (or its name, the keyboard way) opens the country's drawer. The headers sort (production, exports, holdings and share largest first, name A–Z; again to reverse; no data always last).
/vinara (apps/web/src/features/vinara/; Vinara is Swahili for "leaders") names, for each of 13 measures, the cardinal (the leading state) and the silent contender (the runner-up), in Africa or in one UN M49 region, and says whether the cardinal's lead is narrowing. Every ranking, window and signal comes from /api/vinara (computed in the database; rules in docs/vinara-method.md); the page only counts and lays out what it returns.
?region=).?from=&to=) applies to every measure, each keeping to the years it has data for; a range left open at the end runs to each measure's latest complete year. A line above the cards says which applies.VinaraStatsBand.tsx): most cardinal titles, the leading region (Africa only), gaps closing, contenders ahead in the last year, and elections within 12 months, each with its definition behind "How is this calculated?" (the text of the method's "summary cards" section, kept identical by a test). Phones show them as a carousel.VinaraTable.tsx): one row per measure with its years, how many states are ranked, the cardinal and the contender (flag, name, value at the measure's precision, and the year for levels), and the signal: Gap closing (highlighted), Lead holding, Not enough data, or no signal for a tie. Every state in a tie is named. IMF estimates are labelled, and a contender ahead in the window's last year is said in words. Each measure's definition and each signal's meaning show on hover, focus or tap. Below 1024px, one card per measure.RegionComparison.tsx): the leading region and the runner-up for each measure with their figures, and the regions shown as partial (with the share covered), which are not ranked.VinaraExport.tsx): every measure's ranking for the scope and years from /api/vinara/export, with a message confirming the download or reporting a failure.MeasureDetail.tsx, ?metric=; from /api/vinara/metrics/:key): the measure's definition and years, then two charts.
charts/LeadersChart.tsx): the cardinal's and the contender's yearly figures as two lines (the first two categorical colours, a legend and direct labels), with hollow points for IMF estimates, a crosshair tooltip by year and the figures as a table. A tie at the top or for second place has no two lines to follow, and says so.charts/RankingBars.tsx): horizontal bars from zero, so a currency's loss runs left; the cardinal and the contender in colour and the rest neutral. The top ten show first, then all on request. Hover, focus or tap shows the rank and figure with "Open the state", and the figures are also a table.The strip at the top of every page (apps/web/src/features/coverage/) shows the data behind the current view.
handle (ROUTE_DATA in app/router.tsx). The ribbon fetches /api/coverage for exactly those datasets (one cached request, shared with the time range).Gold price: Jan 1990–Sep 2026) and "Refreshed 2 days ago" (the latest pipeline run). Datasets with no row yet show "(not loaded yet)". Phones show one line; the arrow opens the details.14 Oct 2026), so no time zone shifts them. Ranges use the dataset's granularity from its cadence: months (Sep 2026), quarters (Q2 2026) or years (2024)./dhahabu, /vinara) it is a year picker with presets (last 5 years, last 10 years, since 2000, all years) and From/To lists. It is bounded by the data's coverage and never goes beyond it. With no data loaded it shows "No data yet" instead of inventing a range.?from=&to= (years). The full coverage is the default and leaves no params (a route with its own default years, ownDefault in its handle, uses the last ten years of its coverage instead and the control names that default); values outside the data are clamped and a reversed range is put in order, so shared links always resolve to a valid range. Reference pages ignore the params.For time-series views (Dhahabu, Vinara, and any later one):
const { interactive, bounds, range, referenceYear, isDefault, setRange } = useTimeRange();
// range: { start, end } years for charts; referenceYear (= range.end) for tables, maps and cards.
To add a dataset, add its key to DATASET_KEYS (packages/shared/src/datasets.ts), give it a short label in strings.datasets, and list it in the route's handle. Its coverage then appears in the ribbon and, for series, extends the range's bounds.
The target is Lighthouse ≥ 90 on mobile for Performance, Accessibility, Best Practices and SEO on /, /countries, /harim, /dhahabu and /vinara (npm run lighthouse).
Prerendered first views: the build renders each main page's first view (the layout, header, controls and loading skeletons, before any data) into its HTML file: npm run build runs the client build, a server build of src/entry-server.tsx and build/prerender.ts. The page paints as soon as its CSS arrives instead of after the app's script has run.
Hydration, not a second render: on a bare URL the app hydrates that markup (hydrateRoot, after loading the matched route's code-split module, app/hydration.ts), so the painted page is kept. The first browser render must therefore equal the prerendered one: anything the server cannot know (the screen, the theme, <body> portals) uses useSyncExternalStore with a server snapshot or useIsClient, and settles straight after hydration. A URL with a query (filters, an open drawer) renders afresh instead, since its first view differs. An e2e test fails on any hydration error.
The app starts after the first paint: a prerendered page loads /boot.js, which imports the app's entry once the page has painted, so the styles and fonts get the connection first; each page links its route's stylesheets directly. Pages without prerendered markup (peak and range pages, any other path via app.html) load the app's module and preload their route's chunk in the usual way.
No layout shift: the skeletons reserve the loaded cards' heights and choose carousel or grid in CSS; region chips show their names (REGION_NAMES, checked against the seed) with counts pending until the data arrives.
Payload: routes are code-split; the map's context land loads after the countries; Material Symbols is subset to the icons used (≈24 KiB); fonts are self-hosted Latin subsets; API time series are capped and pre-aggregated in SQL; images have explicit dimensions; flagcdn.com is preconnected.
Measured on the live site (LIGHTHOUSE_URL=https://baralaafrika.netlify.app npm run lighthouse, mobile, 2026-10-21):
| Page | Performance | Accessibility | Best Practices | SEO | LCP |
|---|---|---|---|---|---|
/ | 92 | 100 | 100 | 100 | 1.1 s |
/countries | 98 | 100 |
| Command | What it runs |
|---|---|
npm run check | ESLint + Stylelint, Prettier (check), tsc -b, Vitest, production build: the same steps as CI's web job |
npm run lint / npm run typecheck / npm test / npm run build | individually |
npm run format / npm run format:check | Prettier |
npm run check:functions | Builds the Netlify Functions as a deploy does and checks each bundle loads and answers |
npm run e2e | Playwright: desktop (1440×900) and mobile (390×844) projects; includes axe accessibility checks in light and dark mode and no-horizontal-scroll checks at 320px. It runs the real API handlers (e2e/api-server.ts) on an in-memory Postgres loaded with the committed seeds, proxied behind vite preview, so no Neon connection is needed |
npm run screenshots | Captures every page at desktop and mobile, light and dark, into screenshots/ (gitignored) for visual review |
npm run lighthouse | Lighthouse (mobile) on /, /countries, /harim, /dhahabu and /vinara against the production build (run npm run build first) and the local API; fails below 90 in any category; HTML and JSON reports in lighthouse/ (gitignored). LIGHTHOUSE_URL=https://… npm run lighthouse audits a deployed site instead |
uv run ruff check . && uv run ruff format --check . | Python lint and format |
uv run mypy | Python type-check (strict) |
uv run pytest | Python tests (needs npm install for the PGlite-backed database tests) |
Vitest also checks the repository itself: scripts/gitignore.test.ts fails if .gitignore hides any source file, and scripts/netlifyConfig.test.ts keeps the production CSP out of netlify dev.
CI (.github/workflows/ci.yml, Ubuntu 24.04) runs the web checks (npm run check and check:functions), the Playwright suites and the Python checks on every pull request and every push to main. Runs on a pull request cancel that PR's older run; runs on main always complete. npm run screenshots and npm run lighthouse are run by hand.
Copy .env.example to .env for local work. Each variable belongs in exactly one place:
| Variable | Where it lives | Used by |
|---|---|---|
DATABASE_URL_READ | Netlify site environment variables (and local .env for npm run dev:netlify) | Netlify Functions (read-only DB role) |
DATABASE_URL_WRITE | local .env, GitHub Actions secret | Python pipelines only |
COMTRADE_API_KEY | local .env, GitHub Actions secret | gold-trade pipeline only |
ENABLE_WGC_IMPORT | local .env (default false) | reserved for an optional manual WGC import, not built while the WGC is not used |
NETLIFY_BUILD_HOOK | GitHub Actions secret (optional) | data-refresh workflow |
Functions never hold write credentials, and no secret is ever exposed to the client bundle.
netlify.toml configures:
npm run build, publishing apps/web/distnetlify/functions/api/* → /.netlify/functions/:splat redirect and the SPA fallback (/* → /app.html, the plain app; index.html is the prerendered home page, and each page with a shell is served from its own flat file first, e.g. /harim from harim.html, which Netlify serves without a trailing-slash redirect)X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, COOPThe Content-Security-Policy is generated at build time into dist/_headers by apps/web/build/securityHeaders.ts. It allows exactly the inline theme-bootstrap script, by SHA-256 hash, and blocks every other inline script. A unit test keeps the hash in sync with index.html.
The build also writes sitemap.xml (every page with a shell) and a robots.txt that points to it, using the deploy's address (URL, or DEPLOY_PRIME_URL for previews); local builds have no address and skip the sitemap.
The live site is https://baralaafrika.netlify.app, built from main on every push.
db/roles.sql and set the connection strings as in One-time Neon setup.DATABASE_URL_WRITE and COMTRADE_API_KEY in .env, run uv run python -m pipelines migrate, then uv run python -m pipelines load (it loads the committed seeds; regenerate them first with the pipeline commands in First run if you want fresh data). uv run python -m pipelines status shows each dataset's freshness.npx netlify login, then npx netlify init in the repository (or create the site in the Netlify UI from the GitHub repository). The build settings come from netlify.toml, so leave the UI fields empty.DATABASE_URL_READ (the bla_reader connection string), scoped to Functions. Nothing else belongs on Netlify: DATABASE_URL_WRITE and COMTRADE_API_KEY are pipeline secrets (local .env, GitHub Actions).npx netlify deploy --build for a draft URL and npx netlify deploy --build --prod for production./api/regions (it should list five regions), then run LIGHTHOUSE_URL=https://<site> npm run lighthouse.DATABASE_URL_WRITE, COMTRADE_API_KEY and (optional) NETLIFY_BUILD_HOOK. .github/workflows/data-refresh.yml then refreshes the data weekly and monthly, and triggers a rebuild when something changed.Netlify badge: on its free plan Netlify injects a "Powered by Netlify" badge script. The site's Content-Security-Policy blocks its inline script (a console error that costs Best Practices points), so the badge is turned off in the Netlify project settings.
Deploy previews work out of the box: every pull request gets its own URL, and its functions read DATABASE_URL_READ. To keep previews off production data, create a Neon branch for previews and set a different DATABASE_URL_READ for the Deploy previews context in the Netlify UI.
Refreshing data by hand: run the pipeline for a dataset (e.g. uv run python -m pipelines gold-price), review the seed diff, commit it, then uv run python -m pipelines load. The site reads the database at request time (cached for an hour at the CDN), so no rebuild is needed for data. Rebuild only when page shells or the prerender should change (new peaks or ranges).
Adding a dataset or tab: add the dataset's key to DATASET_KEYS and a label in strings.datasets, a migration for its tables, a pipeline command that writes a committed seed, and a TableSpec in the loader. For a new tab, add its route (lazy module) and handle in app/router.tsx, its navigation entry, a function per API route (registered in netlify/tests/functionRoutes.ts), and, to prerender it, an entry in PRERENDERED (build/prerender.ts) and ROUTE_MODULES (build/pageShells.ts).
apps/web/src/styles/tokens.css defines typography, spacing, radii, motion, layers and colours (OKLCH). The light set is on :root and the dark set on [data-theme='dark']..glass / .glass--strong classes in glass.css combine a translucent tint, backdrop blur and saturation, a 1px specular gradient edge, an inner highlight and a depth shadow, layered over an ambient colour mesh. Fallbacks:
backdrop-filter is unsupported or prefers-reduced-transparency is setprefers-contrast: more@custom-media in media.css (compiled by PostCSS) and mirrored in lib/breakpoints.ts. A test keeps them in sync.<Icon name="…" />, which is aria-hidden by default. Icon-only buttons use <IconButton label="…" />. The font is subset to the icons in components/Icon/iconNames.ts (≈20 KiB instead of 4 MB) by uv run python -m scripts.build_icon_font. The script fails if a name is not in the current canonical Material Symbols set. Re-run it after editing the list; a test fails if the subset is stale.prefers-color-scheme until the user toggles it. The choice persists in localStorage (failures are tolerated) and is applied before first paint by the inline script in index.html, so there is no flash.src/lib/strings.ts, ready for translation.SiteFooter.tsx). The year comes from the visitor's clock, so it never goes stale.node scripts/build-favicon.mjs. The SVG switches to a light fill when the browser uses a dark colour scheme, so it stays visible on dark tab strips; (16/32/48 px) is the fallback. The script also stamps the icon links in with , so browsers that cached an older icon for the same address (another project on , or a previous deploy) fetch the new one; a test checks the stamp is current. Material Symbols has no Africa icon. The home page's tab reads just "Bara La Afrika".Decisions taken where the project brief (africa-explorer-claude-code-prompt.md) left room, or where the current ecosystem required it:
overrides entry in package.json records this.packages/shared is a small addition to the specified layout, so the web app and functions share one set of types and schemas.public/boot.js). On a slow connection the page is readable a little before it is interactive, in exchange for an early, stable first paint (and LCP).npm run lighthouse serves the production build with vite preview and the seed-backed local API, so its figures are a floor for the deployed site; LIGHTHOUSE_URL measures a deploy.harim.html, harim/peaks/<slug>.html), because Netlify answers a folder's index.html with a redirect to a trailing-slash URL.ubuntu-24.04 rather than ubuntu-latest, so a new image cannot change system packages unannounced; astral-sh/setup-uv is pinned by commit (it publishes no floating major tag).terrain is now only a legacy alias of landscape, and filter_hdr is not in the current canonical set. They are replaced by landscape_2 and mountain_flag where the specification suggested them.The site covers the four sections above. Each was built against review gates on the data and the design (region colours approved 2026-10-15; the Dhahabu sources decided and its data reviewed 2026-10-18), and the review reports in data/ record what was checked.
Planned, not built:
Credits are shown in the site footer and in data exports, including the attribution wording required by GeoNames (CC BY 4.0) and the IMF. Sources: the project's state-symbology research document, Natural Earth, Flagpedia, Material Symbols (Apache 2.0), Fraunces and Instrument Sans (SIL OFL 1.1), Wikidata (CC0), GeoNames (CC BY 4.0), the CIA World Factbook's final edition (public domain), Wikipedia (CC BY-SA 4.0, cited per item), the Harim prototype, the IMF, USGS, the World Bank and UN Comtrade, and for Vinara SIPRI (military spending, under its terms for non-commercial use of a small part of the database), UNDP (CC BY 3.0 IGO), the ECB, the BIS and the central banks of Kenya, Tanzania, Zambia, Angola, The Gambia, Liberia, Malawi, Tunisia and Morocco (read through Frankfurter). Each is credited with its required wording in the footer and in the Vinara export. The Smithsonian Global Volcanism Program is linked for reference only; none of its data is republished. The British Geological Survey and the World Gold Council are neither used nor linked (decided 2026-10-18).
Boundaries are shown as published by Natural Earth and do not imply endorsement of any border or territorial claim.
| 7 | uv run python -m pipelines indicators | Builds the annual country indicators and public external debt by creditor (see Country indicators) → db/seeds/{indicators,external_debt_creditors}.json, data/indicators_coverage_matrix.md and data/indicators_review_report.md |
| 8 | uv run python -m pipelines politics | Reads each state's leaders and next election (see Leaders and elections) → db/seeds/politics.json and data/politics_review_report.md; --approve records them as approved |
| 9 | uv run python -m pipelines fx | Exchange rates (see Exchange rates) → db/seeds/{fx,fx_daily}.json and data/fx_review_report.md |
| 10 | uv run python -m pipelines load | Upserts the seeds into Neon (regions with the approved colours, countries, profiles, sources, citations, flag palettes, the mountain catalogue, the gold tables, the indicators, the approved leaders and elections, the exchange rates, datasets). Only rows whose values changed are written, so re-runs report 0 changes; each run is logged in ingestion_runs and refuses to start while migrations are pending |
pipelines/africa_pipelines/reference/harim_curation.json holds the pinned commits, thresholds, prototype matches, massifs, sourced elevations, curated volcanoes, ranges (with their descriptions), territories and the decisions where Wikidata and the Factbook disagree on a country's high or low point. db/seeds/mountain_names.json holds local names and name origins. Every text claim has a source URL and the exact quote; the run re-checks each quote against the cached source and fails if it is missing, so nothing is invented.data/mountains_review_report.md lists the per-country high points, every decision and conflict, each prototype entry, elevations to review, the hierarchy, exclusions, territories, low points, ranges and names.0003_harim.sql enforces the same rules: 0–6,000 m, Africa's bounding box, summits under massifs, massif height checked at commit, one high point per country (never a summit), no territory peak in a country, sea-level coasts at 0 m, and sourced name origins.gold_holdings_change()gold_stats_corev_gold_statsGET /api/mountains/ranges?region=&q= | The ranges with catalogued mountains, tallest first: type, length, mountain count, tallest peak, their heights, the countries spanned (most peaks first), regions and the card tint (v_range_summary, migration 0006) |
GET /api/mountains/ranges/:slug | One range: its sourced description and formation note, source, review notes and its mountains. Unknown, malformed or empty ranges are 404 |
GET /api/mountains/export?format=csv|geojson&<filters> | A download of the catalogue, filtered like /api/mountains (never capped). CSV starts with a # block, GeoJSON carries a metadata member: as-of date, filters, each dataset's version, licence and attribution, and the other sources. Rows include the height's source and a link to the peak's page |
GET /api/mountains/stats?region= | The statistics band's figures from v_harim_stats() (for All Africa, or one region), each with its definition; ties are listed, never resolved at random. Each card's details and entities have their own schema in the shared contract. 503 until the mountain data is loaded |
GET /api/gold/countries?region=&q=&presence=&sort=&year= | One row per country (54 without filters): presence tier with its evidence and source; mine production (tonnes or "included in other", basis, year, and the value computed at that year's average price); gold exports and imports as reported and as partners report them (mirror); central bank holdings (tonnes, value, share of reserves, 12-month change, month) and reporting status; gold's share of merchandise exports. Each figure is the latest in or before year (default: latest available) and carries its own year or date. sort=production (default), exports, holdings, share, name, each reversible with - (no data last) |
GET /api/gold/stats?region=&year= | The ten statistics cards from v_gold_stats(), in the Harim stats shape: value, unit, as-of date, details, entities (ties listed) and definition. 503 until the gold data is loaded |
GET /api/gold/export?format=csv&<filters> | The country table as CSV (all 54, or filtered like /api/gold/countries): a # block with the reference year, filters, notes (nominal US dollars, computed values, null vs zero vs "included in other"), each dataset's licence and the required attributions (IMF wording with the transformation statement, World Bank, UN Comtrade, USGS) |
GET /api/gold/price?from=&to=&granularity=month|year | The gold price series (World Bank monthly average, US$ per troy ounce), or annual averages |
GET /api/gold/countries/:iso3?year= | One country's drawer: its table row at the reference year, production by year (with computed values), trade by year (reported and mirror) and its top export partners, central bank holdings (each year's last month), in-ground reserves, curated notes and every source with what it supports. Unknown or malformed codes are 404 |
GET /api/gold/series?metric=production|exports|imports|holdings_tonnes|holdings_usd&countries=®ion=&from=&to=&granularity= | Chart series, one per country, pre-aggregated in SQL; production adds Africa's (or the region's) yearly totals with their completeness. At most 12 countries per request and 6,000 points (larger requests drop the earliest periods and say capped) |
GET /api/gold/holdings-change?countries=&from=&to= | For each country with two or more reports in the years, its first and last holding and the change in value split into price and volume effects (gold_holdings_change) |
GET /api/vinara?region=&from=&to= | For each of the 13 metrics, over its default window or the years asked for: the window, the cardinal and the silent contender (ties all named, each value with its year and whether it is an IMF estimate), the gap-closing signal, whether the contender led in the window's last year, how many states are ranked and why the others are not, and its definition. For Africa (no region), the five regions compared: each region's figure, median, coverage, whether it is partial, and its rank. 503 until the indicators are loaded |
GET /api/vinara/metrics/:key?region=&from=&to= | One metric in full: the same summary, every ranked state with its rank, the regions' figures (Africa only) and the cardinal's and contender's yearly values over the window, for charts. Unknown metrics are 404 |
GET /api/vinara/countries/:iso3?from=&to= | One state on every measure over the page's years: its value, year, whether it is an IMF estimate, and its rank in Africa and in its region (or why it takes no part); context figures (population growth, military spending as % of GDP, external debt); public external debt by creditor in its latest year (the eight largest, the rest summed, each with its share); and its currency (peg, official daily source and span, the IMF monthly series' status). Unknown or malformed codes are 404 |
GET /api/vinara/export?format=csv®ion=&from=&to= | Every measure's ranking for Africa or a region as CSV, ranked states first and then those taking no part (with why): a # block with the as-of date, scope, years, notes, each measure's definition and the datasets' licences and required attributions |
features/map/MapBase.tsx holds what every map has: the fitted Equal Earth projection, context land, graticule, borders and coastlines, d3-zoom with the + / − / reset buttons, and a controller for camera moves (fit a box, centre a point, animated unless the viewer prefers reduced motion). The home map and the Harim map add their own layers on top.Phones (< 720px): a stacked card list (flag, name, region), each expanding to the summaries and a button to the drawer.
Desktop: the column headers stay visible below the ribbon while scrolling. The table is laid out to fit from 720px up, so there is no horizontal scrolling and no sticky first column is needed.
Loading skeletons, an empty state with "Clear filters", and an error state with "Retry".
--band-*, --volcanic-* in tokens.css), tuned for each theme and shared with the map; text always gives the same numbers.features/harim/map/): the shared Africa map with countries in a muted tint of their region colour, and the mountains on top.
features/harim/chart/): a horizontal bar per country in its region colour, highest first or A to Z, following the region filter, with a dashed line at Kilimanjaro. Hover or tap a bar for its peak and height; "Show the data as a table" is the accessible equivalent./harim?country=<ISO3> (that country's row, expanded, and the map fitted to it); each Harim row links back to the country's profile.?view=ranges): a card per range, tinted with the region colour of the country holding most of its peaks (neutral when split), with its type, tallest peak, mountain count, countries and a mini bar of its peaks' heights. Region chips and search apply (search also matches the ranges' mountains); sort by tallest peak (default), most mountains or name. The map shows the listed ranges' mountains./harim/ranges/:slug): the sourced description and formation note with its source ("needs verification" where unsourced; "no sourced description yet" where there is none), type, length, mountain count and tallest peak, the countries it spans (their mountains and profiles), the map fitted to the range, and its peaks (each opening its page)./harim/peaks/:slug, and "Open full page" in the drawer): name, height in both units, flags, range and volcanic status; the height's confidence, source and every other sourced height; prominence, isolation, coordinates (with a copy button), type, massif and summits; local names and name origin with sources ("Unsourced" otherwise); a locator map; nearby peaks (same range or country, nearest first); sources; and a copy-link button./api/mountains/export; a message confirms the download or reports a failure./countries, /harim, /dhahabu, /vinara and every mountain and range page (apps/web/build/pageShells.ts, from the committed seeds) with that page's title, description and Open Graph tags, so shared links show the right text; Netlify serves them before the single-page fallback (as does vite preview). The preview images are public/og/harim.png (Harim and its pages), og/dhahabu.png, og/vinara.png and og/site.png (the site and /countries), all from node scripts/build-og-images.mjs.?view=country|peaks|ranges, region, q, sort, country, minHeight, volcanic, range and peak (the open drawer). Invalid values are ignored. Switching to By country or By range drops the peak-only filters. A country in the URL filters to that country and opens its row (the link from other pages).bla-units) and applied everywhere on the page. Below sea level uses a true minus sign (−155 m).?region=, ?q=, ?presence=, ?sort=, ?metric= (the map) and ?country= (the open drawer) live in the URL with the ribbon's ?from=&to=, so every view is shareable.bla-gold-units). Money is nominal US dollars throughout./api/gold/export, with the attribution block.map/GoldMap.tsx): the shared Equal Earth base with a metric switch (mine production, central bank gold, gold exports by value, share of exports, presence; buttons, or a menu on phones).
--gold-seq-1…5, one hue, light to dark in light mode and dim to bright in dark mode, the faintest step at least 2:1 on the surface, checked with a palette validator). The classes hold about equal numbers of countries, computed from all 54 so a region filter dims rather than recolours. "No data" is a dotted neutral pattern and "included in other countries" a striped one, each in the legend with its count; no data is never drawn as a value.charts/), all following the ribbon's range and the region filter, each with an icon, direct labels and "Show the data as a table":
/api/gold/holdings-change.GoldDrawer.tsx; side panel, bottom sheet on phones): the key figures at the reference year; presence with its evidence, confidence, year and source; production by year with bars and computed values; trade by year (reported and what partners report, with the neutral note) and the top destinations; central bank gold (each year's last month); in-ground reserves in a separate dashed block, with a US dollar value labelled "illustrative at a given price, not a realisable sum"; artisanal mining; curated notes with their source and date; every source with its reliability tier and what it supports. "Country profile" opens the country on /countries, whose drawer links back ("Gold in …").charts/RegionMultiples.tsx, the default; "Table" switches back): one small bar chart per measure, the five regions in their colours, every bar labelled with its figure (and each region's share of Africa for totals). The leading region is outlined; a partial region is lighter and dashed, says how much of it is covered, and is not ranked.VinaraDrawer.tsx, ?country=; a state's name in the table, or "Open the state" in the ranking): its figure and rank on every measure (bold where it leads Africa or its region), its leaders and next election, context figures, who its public external debt is owed to (the eight largest creditors with their shares) and its currency (peg, official daily rates or the IMF's monthly series, and how recent they are). A link opens its country profile.ElectionStrip.tsx): the next election that decides the executive in each state in scope, soonest first, in a strip that scrolls inside itself (elections within 12 months marked), then the states with no date set or no national elections.| 100 |
| 100 |
| 1.1 s |
/harim | 98 | 100 | 100 | 100 | 1.1 s |
/dhahabu | 99 | 100 | 100 | 100 | 1.1 s |
CLS stays under 0.03 and TBT under 300 ms. The local production build (2026-10-24) scores 96–97 for Performance and 100 for Accessibility, Best Practices and SEO on all five pages, /vinara included (LCP about 2 s, without the CDN's compression).
favicon.icoindex.html?v=<hash of both files>localhostconfig.path (Netlify matches functions before redirects). The specified /api/* → /.netlify/functions/:splat rewrite is kept as the catch-all for unknown API paths./api/countries/KE names a country that does not exist, the same as /api/countries/XYZ. Invalid query parameters are 400._shared/contract.ts (relative path, inlined). CI checks the built bundles.load writes the colours recorded in region_colors_approval.json, never a newer computation.boundaries option./api/countries?q= on each keystroke. It is instant, and it also searches the visible columns (meanings, symbols, animals).needs_review set (most cite low-reliability sources), so a marker on every row would carry no information. The drawer shows the marker and the reasons./dhahabu, so ?from=&to= are years. Monthly series can still be drawn month by month inside the chosen years.5,895 m).# lines before the header, since CSV has no metadata field; spreadsheet apps show it as the first rows.river and salt_flat to the specification's examples (many landlocked countries' lowest points are rivers; several North African ones are salt flats).wgc_import adapter is not built, and figures the USGS itself attributes to the WGC (Sudan 2017–2021 in one edition) are left out.hs_code = '7108.1'); the specification's "7108 (non-monetary gold)" includes monetary gold.valuation = computed); gold's share of reserves uses the IMF's own SDR values, so it needs no price. The eight BCEAO members have no national holding (reserves are pooled at the regional central bank)./api/gold/mines and the map's mines toggle are not built while the mines data fails the quality bar (the drawer has no "major mines" section for the same reason). No "Explore on Goldhub" links (owner's decision).npm audit reports advisories in dev-only tooling (netlify-cli's local server dependencies and Stylelint's glob dependencies). None of it ships to the browser or the deployed functions.