# Extending the web app Next.js 15 (App Router, React Server Components) with next-intl (German default, English). Pages fetch from the API on the server; there is almost no client-side JavaScript. How it should look and why: [design/README.md](../design/README.md) (concept) and [design/design-system.md](../design/design-system.md) (tokens, components, content rules). ``` web/ messages/de.json, en.json all UI text (keys must exist in both) src/app/[locale]//page.tsx pages (German route names: /personen, /parteien, /geld …) src/app/[locale]/layout.tsx frame: header, footer, theme, stylesheets src/app/[locale]/loading.tsx, error.tsx, not-found.tsx src/app/icon.svg favicon src/components/ shared components (catalogue: design-system.md §4) src/components/nav.ts site structure for header, menu and footer src/styles/ tokens, base, layout, components, print src/lib/api.ts typed API client (server-side only; reads the filter cookie) src/lib/api-types.ts GENERATED from the backend's response models – do not edit src/lib/format.ts dates, periods, numbers, money, party colours src/lib/prefs.ts the visitor's source/topic filter and theme (cookies) src/lib/roles.ts role labels ``` ## Types come from the backend After any change to `backend/politrack/api/schemas.py`: ```bash cd backend && politrack openapi --ts ../web/src/lib/api-types.ts cd ../web && npm run typecheck ``` `api.ts` re-exports the generated types (`Claim`, `MoneyFlow`, …) and adds one function per endpoint. `api.claims()`, `api.money()`, `api.social()` … automatically apply the visitor's source filter (`withPrefs`), so every list respects "which sources do I trust". `api.claim(id)` deliberately does not: a permalink shows the same to everyone. ## Adding a page 1. Create `src/app/[locale]//page.tsx` – an async server component built from the shared parts: ```tsx export async function generateMetadata({params}: Props): Promise { const {locale} = await params; const t = await getTranslations({locale, namespace: 'MyPage'}); return {title: t('title')}; } export default async function Page({params, searchParams}: Props) { const {locale} = await params; setRequestLocale(locale); const sp = await searchParams; const t = await getTranslations('MyPage'); const page = Math.max(1, Number(sp.page) || 1); const rows = await api.something({kind: sp.kind, limit: PAGE, offset: (page - 1) * PAGE}); return ( <> {rows.length === 0 ? {t('none')} :
    …
} ); } ``` Filters are plain GET forms – shareable URLs, no client state. 2. Add the texts to **both** `messages/de.json` and `messages/en.json`. 3. Add it to the site structure in `src/components/nav.ts` (primary, "Mehr" or a footer group) or link it from the home page's "Entdecken" cards. 4. Page modules may only export Next.js page fields (`default`, `generateMetadata`, …); keep helpers and constants module-local or in `src/components`. 5. Check it at 390 px and in both themes (design-system.md §7). ## Showing a new claim type Claims render through `ClaimList` → `ClaimCard`; type-specific details come from the registry in `src/components/ClaimDetails.tsx`: ```tsx const ambassador: Renderer = (claim, {t}) => (

{t('country')}: {String(claim.data.country ?? '')}

); const DETAILS: Partial> = { …, career: [counterpart, ambassador], }; ``` Also add an icon to `TYPE_ICONS` in `src/components/badges.tsx`, the label under `Claims.type.` in both message files, and (if it should appear) the type to the person page's `TABS`. ## Non-negotiables in the UI * **Every fact shows its sources** (the evidence disclosure under each claim, source chips in tables). Don't render a claim without its evidence. * **Presumption of innocence:** `needsInnocenceNotice()` in `ClaimList.tsx` decides; the notice stays inside the claim card. Never remove it for anything but an undisputed final conviction. * **Connections are not accusations:** sections that show networks or money of connected organisations carry the note (`Network.note`, `Organizations.connectionNote`). * **Neutral colours:** party colours only through `partyStyle()` as dots and thin bars, next to the name. * Unverified social accounts are marked (Wikidata-only handles). * No third-party scripts, fonts, embeds or trackers. ## Styling Global CSS with design tokens, one section per component in `src/styles/components.css`. Use tokens (`var(--ink-2)`, `var(--space-4)`) instead of raw values, reuse the components and layout utilities listed in [design-system.md](../design/design-system.md), and add a new section (and a row in its component table) when you create a component. A new token goes into all three blocks of `tokens.css` (light, dark by preference, dark by choice).