尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗An open, sourced atlas of African peoples, languages, linguistic families and countries — published in French, organised by the AFRIK methodology, written from a decolonial editorial posture.
Every claim carries its source. Colonial-era names are kept and explained rather than quietly dropped, and the autonym — what a people calls itself — is always surfaced alongside the exonym.
/docs/api (Swagger UI) · /api/docs (OpenAPI JSON)Node 22.x (package.json engines, and the production Dockerfile). The AFRIK data loaders
need it too: @supabase/supabase-js requires a native WebSocket — see
docs/runbooks/afrik-data-sync.md.
git clone https://github.com/big-emotion/ethniafrica.git
cd ethniafrica
npm install
cp .env.example .env.local # then fill in the three required values below
npm run dev # http://localhost:3000
No credentials for a shared Supabase project? You do not need any. supabase start builds the
whole schema on your machine and --target=local loads the corpus into it — see
Local bootstrap. That is the contributor-safe
path; recette and production are self-hosted stacks you cannot, and should not, write to.
The site publishes in French only. Without Supabase credentials the pages render but data-backed routes fail — the modules validate their configuration at import time and throw when it is missing.
Three variables are required to run:
| Variable | |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | your Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | browser-safe anon key |
SUPABASE_SERVICE_ROLE_KEY | server-only — never let this reach the client bundle |
ANTIBOT_HMAC_SECRET is required for reporting to work at all: unset, the anti-robot challenge
answers 503 and every report dialog fails while the build stays green. NEXT_PUBLIC_SITE_URL is
required in production: src/lib/siteUrl.ts throws on a production server without it, so every
page answers 500 while next build stays green. Sentry and Plausible are optional. Upstash (rate limiting) is optional locally but mandatory in
production, where rate limiting fails closed. .env.example is annotated and authoritative;
npm run check:env-example keeps it honest against what the code actually reads.
The moderation console opens to a magic link sent to an address on admin_allowlist; there are no
public accounts and no OAuth providers. Put the first address on the list with the target
project's service-role key:
npx tsx scripts/seedAdminAllowlist.ts [email protected] "Editorial moderation lead"
The legacy user_roles table opens no door in the console — see docs/DEPLOYMENT.md.
The corpus lives as JSON fiches in git, under dataset/source/afrik/ — not only in the
database (git ls-files 'dataset/source/afrik/**/*.json' | wc -l counts them). The files are the editorial source of truth; Supabase is a projection of them.
linguistic family → language → people → country
FLG_* ISO 639-3 PPL_* ISO 3166-1 alpha-3
dataset/source/afrik/
famille_linguistique/FLG_*.json
peuples/FLG_*/PPL_*.json
pays/*.json
{relations,noms,migrations}/
↓ src/lib/afrik/loaders/*JsonLoader.ts
afrik_language_families · afrik_languages · afrik_peoples
afrik_countries · afrik_people_countries
Each fiche's shape is fixed by one of the strict models in public/modele-*.json — the
directory is the list: dossier, peuple, pays, linguistique, langue, lieu, relation, source,
migration, and the two name models (nom, nom-patronyme). Never skip, rename or invent a
section.
Every sources entry carries a tier, and scripts/validateAfrikData.ts enforces it. The rules
the validator applies live in that script.
/api/v2 only. V1 (/api/regions, /api/ethnicities) was removed with the schema behind
it; anything that mentions regions or ethnicities as entities is stale.
Resources under /api/v2/: countries, peoples, language-families, languages,
patronymes, places, relations, migrations, names, oral-narratives, dossiers, media,
sources, reference-library, search, compare, confidence, doctrine, flags,
antibot, quiz, feed, keys, words, and admin (moderator-only). The folders under
src/app/api/v2/ are the list.
curl http://localhost:3000/api/v2/countries
curl http://localhost:3000/api/v2/countries/NGA
curl "http://localhost:3000/api/v2/peoples?limit=5"
curl http://localhost:3000/api/v2/language-families/FLG_BANTU
Every endpoint splits across three layers — route (HTTP, CORS, cache headers) → handler
(business logic, serialization) → service (the only layer that talks to Supabase) — plus the
OpenAPI spec in src/lib/api/openapiV2.ts, which npm run openapi:diff gates against
breaking changes.
No API key is required. A keyless request is served on the anonymous tier, 60 requests a minute
per IP; an Authorization: Bearer <key> header selects that key's larger quota, and a key that is
unknown, revoked or expired is refused with 401 rather than served anonymously. Where a request
says it came from (Origin, Referer) authorises nothing, so the frontend embeds no key and is
metered like any other reader. Bulk exports: /api/download?format=csv or format=excel.
The site is French only; no English launch is planned. Every page lives under /fr/....
The English addresses published during the bilingual period answer with a permanent redirect to
their French page (src/lib/legacyEnglishPaths.ts, applied by src/proxy.ts), and any other
two-letter segment (/es, /pt) is redirected to /fr.
make check # the gate: lint + typecheck + format:check + all tests (stays under 5 min)
npm run dev
npm run test:watch
npm run storybook # :6006
npm run e2e # Playwright — deliberately outside `make check`
Beyond make check, CI runs gates specific to this repository. Run the ones your change
touches before pushing:
| Command | What it protects |
|---|---|
npm run lint:req | @req traceability — every test needs a // @req REQ-NNN, validated against docs/confluence-spec/req-catalog.json |
npm run check:env-example | .env.example matches the variables the code reads |
npm run check:action-pins | every third-party GitHub Action is SHA-pinned |
npm run check:jira-template | the ticket template still exists and matches |
npm run test:charter-contracts | the design-charter contract suite |
npm run check:local-paths | no workstation path in this public repository |
npm run check:dead | unreferenced files, exports and dependencies, against ratcheted ceilings |
npm run check:migration-files | no duplicate or missing number in supabase/migrations/ |
npx tsx scripts/validateAfrikData.ts | AFRIK corpus integrity |
Two things about that list are easy to get wrong:
docs/confluence-spec/*.json or docs/templates/jira-ticket-template.md.
With the catalog missing, lintReqAnnotations.ts returns early and reports OK while checking
nothing — a silently disarmed gate, which is worse than a red one.eslint/rules/ under the afh plugin, and their own tests are
.js files listed explicitly in vitest.config.ts. They once fell outside the glob and never
ran, which is how a broken rule shipped.TypeScript runs with strict: false and strictNullChecks: false. The compiler will not catch
nullability here — the tests are the real gate. Write the failing test first.
recette is the integration branch, main is the base. Both are protected: branch and open a
pull request, never push directly. Conventional commits (commitlint on commit-msg).
recette ↔ main sync PRs need a merge commit, not a squash.
Publishing a GitHub Release is the only thing that deploys production, to a self-hosted VPS. A push
deploys nothing and neither does a tag; Vercel builds only the recette preview, and only when
someone runs that workflow by hand. See docs/DEPLOYMENT.md.
Requirements, decisions and architecture live on Confluence, not in this repository — see
docs/adr/README.md for where and why. Tickets are in the Jira project
ETNI.
Data corrections are welcome as pull requests against the fiches in dataset/source/afrik/, or
through the published site: /fr/contribute to propose a change and /fr/report-error to flag
one.
Next.js 16 (App Router) · TypeScript · Tailwind + shadcn/ui · TanStack Query · Supabase
(PostgreSQL, Auth, RLS) · Vitest + Playwright · Storybook (@storybook/react-vite, not
@storybook/nextjs — Next 16 dropped next/config; installs need --legacy-peer-deps).
Design tokens are CSS custom properties in src/styles/tokens/. Colours belong in tokens, not
literals, and the charter contract suite asserts it. Mobile-first is mandatory: mobile 430px ·
tablet md 720px · desktop xl 800px.
docs/DEPLOYMENT.md | how a change reaches users, and what an operator does by hand |
docs/runbooks/migration-state.md | which Supabase migrations are live on which project, and the two-step rollout rule |
docs/runbooks/ | restore, corpus sync, DBA overrides |
docs/adr/README.md | where architecture decisions live now |
CHANGELOG.md | release history |
Operators, read this first: a hosted Supabase project labels its only environment
"production" — the label describes the project, not the application it serves. Since ETNI-1958
both recette (https://supabase-recette.ethniafrica.com) and production
(https://supabase.ethniafrica.com) are self-hosted Supabase stacks on a VPS, which the
Supabase dashboard and MCP cannot see. shmrjtnfbqzceovroqjj is the hosted project that backed
recette before the move; it is kept only as a rollback path until ETNI-1962. jajggbeimfudpzcxytbb
is a retired hosted project that still answers — never point a secret at either. Every migration is a two-step rollout, recette first. Applying
one and calling it done has already left a corpus loaded on one database and missing on the
other.
Open source. See the repository for terms.