21 September is one date. The history behind it is thousands of years old.
The Peace Atlas is a permanent, explorable reference on peace and humanity. It uses the International Day of Peace (established by GA Resolution 36/67 in 1981, fixed to 21 September as a day of non-violence and cease-fire by Resolution 55/282 in 2001) as an entry point into the longer record: people, events, ideas, movements, cultures, treaties, documents, and a chronological timeline. It also includes a teaching quiz, two small games built on the same data, full-text search, and an optional AI research companion.
There are no accounts, no paywalls, no analytics, and no tracking. Quiz progress lives in localStorage only. The site works fully without AI configured.
- Next.js 15.5.4 (App Router, static generation by default), React 19, TypeScript
- No UI framework, no database, no client-side data fetching for content. All atlas content is typed modules shipped with the app.
- Runtime dependencies are
next,react,react-dom. Dev dependencies aretypescript,tsx, and React/Node types. Seepackage.jsonfor exact versions. reactStrictMode: true,poweredByHeader: false(next.config.ts).
Content is a knowledge graph, not a set of disconnected articles. Every entity has a stable string ID and explicit relationship arrays pointing at other IDs. Relations are checked at build time by scripts/validate.ts.
src/
app/ # routes (server components by default)
page.tsx # home, daily discovery
layout.tsx # metadata, theme init, JSON-LD, header/footer
timeline/ events/ people/ ideas/ movements/ cultures/
treaties/ documents/ peace-day/ sources/ glossary/
quiz/ games/ search/ # client components, local state
ai/ # client chat UI, calls /api/ai
about/ privacy/ terms/
api/ai/route.ts # server-only Gemini call
robots.ts sitemap.ts # generated from src/lib/site.ts
components/
Header.tsx # editorial nav order, mobile slide-in menu
ThemeSwitch.tsx # accessible switch, localStorage persistence
Entity.tsx # Evidence, Relations, SourceList renderers
Markdown.tsx # minimal renderer for AI answers (no raw HTML)
lib/
types.ts # Person, HistoricalEvent, Idea, Treaty, Movement,
# CultureTradition, AtlasDocument, TimelineEntry,
# QuizQuestion, GlossaryTerm, Source, EvidenceBlock
data/ # one module per domain plus sources, quiz, peaceDay
index.ts # re-exports and sourceById map
graph.ts # resolveEntity / relatedLinks (id to href + label)
search.ts # client-side weighted search over titles + summaries
daily.ts # deterministic day-of-year rotation for home page
site.ts # canonical URL, site name, route list for SEO
ai/provider.ts # server-only Gemini provider (never imported client-side)
scripts/validate.ts # duplicate IDs, broken relations, missing sources
Entity pages are collection pages with anchor links (/people#immanuel-kant), resolved through graph.ts, rather than one route per entity. This keeps the build fully static and keeps every entry one click from its relations.
Catalog and reference: /timeline, /events, /people, /ideas, /movements, /cultures, /treaties, /documents, /peace-day, /sources, /glossary, /about. Interactive: /quiz, /games, /search, /ai. Legal: /privacy, /terms. API: GET /api/ai (capability probe), POST /api/ai (prompt in, synthesis out). SEO: /robots.txt, /sitemap.xml.
The header nav order is editorial and fixed (Explore through About). It is not alphabetical and should not be reshuffled.
Every entry distinguishes five levels, typed as EvidenceKind in src/lib/types.ts:
documented-factinterpretationcompeting-interpretationuncertaintyai-synthesis
The rule is simple: factual claims trace to listed institutional sources. Contested subjects present multiple readings. Uncertain dates and numbers say so. AI output is always labeled synthesis, never authority. There are no fabricated quotes, statistics, events, or citations. The 20 sources in src/lib/data/sources.ts are real institutional references (UN bodies, ICRC, Nobel, Stanford Encyclopedia of Philosophy, Britannica, PCA, Library of Congress) with access dates.
AI is optional and server-side only. The browser never sees the key.
src/lib/ai/provider.tsreadsGEMINI_API_KEY(and optionalGEMINI_MODEL, defaultgemini-3.5-flash-lite) from the server environment. It posts to the Generative LanguagegenerateContentendpoint with a low temperature (0.4), a 900-token cap, a 30-second abort timeout, and explicit handling for 429/503 (including Google'sretryDelayhint), 400, and malformed responses.src/app/api/ai/route.tsvalidates the prompt (required, 4000 chars max, max 12 context IDs), returns 503 with a plain message when unconfigured, and maps timeouts and busy states to 504/429/503 so the UI can explain what happened.src/app/ai/page.tsxis a client component with suggestion prompts, a thinking indicator, graceful unavailable/busy/error states, answers rendered byMarkdown.tsx(element trees only, external links only, no injected markup), and follow-up links back into the atlas viasearchAtlas.- The system prompt (
SYSTEMinprovider.ts) instructs the model to prefer atlas entities by exact title and stable ID, to separate source-backed claims from synthesis, to present multiple perspectives on contested topics, and never to invent citations, quotes, or links.
- Search (
lib/search.ts): in-memory scoring over titles and summaries. Title matches weigh 3x, body matches 1x, top 30 returned. No server round trip. - Daily discovery (
lib/daily.ts): deterministic day-of-year index, so the home page rotates through person, event, idea, treaty, document, question, and timeline entry without cookies or tracking. - Theme: a pre-paint inline script in
layout.tsxreadspeace-atlas-themefromlocalStorage(falling back toprefers-color-scheme) and setsdata-themeon<html>before first paint to avoid a flash.ThemeSwitch.tsxis arole="switch"button that syncs from the document attribute on mount and persists on toggle. The<html>element carries a scopedsuppressHydrationWarningfor its own attributes only, with the justification in a comment: the server cannot know the visitor's stored theme, so the pre-paint correction differs from SSR output by design. Children hydrate with full checking, and no other component touches<html>. - Mobile nav (
Header.tsx,globals.css): the hamburger toggles an off-canvas panel that slides from the right. Closed links usevisibility: hiddenso they leave the tab order. Escape closes the menu, links close it on navigation, and the panel usesoverflow-x: clipscoping so it cannot cause horizontal scroll. Toggle and switch carryaria-expanded,aria-controls, and labeled states with visible focus rings. - SEO (
layout.tsx,site.ts,robots.ts,sitemap.ts): per-page unique titles and descriptions with a%s · The Peace Atlastemplate,metadataBasefromNEXT_PUBLIC_SITE_URL, keywords, Open Graph and Twitter cards, robots directives, andWebSiteJSON-LD. Sitemap priorities favor/,/timeline, and/peace-day. - Styling (
globals.css): single archival stylesheet with CSS variables, Fraunces/Source Serif 4/IBM Plex Mono type stack, light and dark themes via[data-theme], card/grid/evidence/tag primitives, scroll-contained tables, wrapping form rows, and reduced-motion support. No CSS framework. - Accessibility: skip link, semantic landmarks, one H1 per page, keyboard-operable menu/quiz/games/switch, live regions for AI answers.
Requirements: Node 18 or later, npm.
npm install
npm run validate # content integrity: IDs, relations, sources, evidence
npm run dev # http://localhost:3000AI setup (optional):
cp .env.example .env.local
# edit .env.local: GEMINI_API_KEY=<key>
# optional: GEMINI_MODEL=gemini-3.5-flash-lite
npm run devFor production hosting, set GEMINI_API_KEY in the host's environment dashboard, never in code. Without it, /api/ai returns 503 and the rest of the site is unaffected. Also set NEXT_PUBLIC_SITE_URL to the production origin (no trailing slash) so sitemap, robots, canonical metadata, and JSON-LD use real URLs.
npm run validate # must pass before committing content changes
npm run build # production build, 26 routes including robots/sitemap
npm run start # serve the production buildvalidate fails on duplicate IDs, relations pointing at unknown IDs, references to unknown sources, and entities with no sources or no evidence blocks. Entities with zero inbound links print as warnings (orphan detection), since some valid entries are reachable through search and the timeline rather than relations.
- Edit the relevant module in
src/lib/data/(for examplepeople.ts,treaties.ts,timeline.ts). - Reuse an existing stable ID or add a new lowercase slug-style ID. Never rename an ID that other entries link to without updating those relations.
- Fill the required fields the validator checks (for example, people need name, summary, lifespan, region; treaties need date, signatories, provisions; documents need author and an
httporiginal URL). - Add at least one
evidenceblock with the correctkind, at least one real source ID fromsources.ts, and relation IDs that exist. - Run
npm run validate, thennpm run build.
When adding a new source, use a real institutional URL with an access date. Do not invent citations to satisfy the validator.