# CLAUDE.md — working brief for agent sessions Read this before touching anything. Human setup instructions live in [README.md](README.md); this file is the short version plus the rules that are easy to break. ## What the project is **SHARP-ER** — *Shared Action on Rule of Law Progress – Exit Route*. EU-funded, nationwide Libya, December 2025 – December 2027. Implemented by **Acted** (lead), **United Efforts Consultancy (UEC)** and **Stichting Bureau Ensemble (SBE)**. Supports Libyan civil society, local institutions and communities on justice, accountability and governance. Successor to SHARP I and II (2019–2024). Tone in all copy: professional, neutral, factual, inclusive, non-confrontational. **Not** political, **not** advocacy, **not** campaign-style. ## Architecture in one screen ``` apps/web React Router 8 (framework mode, ssr: true) · React 19 · Vite 8 · Tailwind 4 SSR only reads the CMS from loaders. No client-side fetch on first paint. / 302 (Vary: Accept-Language) -> /en or /ar /:lang locale layout, validates params.lang, provides { locale, dir } /:lang/slug ten top-level sections + detail routes Route ids: root · locale · home · about · whatWeDo · activities · events · applications · news · contact · resources · knowledgeHub (+ *Detail) apps/cms Strapi 5.53 (TypeScript) · PostgreSQL 16 · i18n (en default, ar) · draft & publish src/api/** content types (schema.json is the contract, commit it) src/bootstrap/** runs on every boot: creates locales, grants Public permissions (find/findOne on content; create-only on submission types) config/ server · admin · database · middlewares (CORS) · plugins docs/ Source material: ToR, approved EN/AR draft copy, brand guideline. READ ONLY. ``` Data flow: browser → React Router SSR loader → Strapi REST (`~/lib/strapi`, server only) → PostgreSQL. `STRAPI_TOKEN` never reaches the browser (`~/lib/env.server.ts` is `.server`, and the bundler enforces it). Every page falls back to bundled draft copy when the CMS is unreachable — the site must never 500 because Strapi is down. ## Hard rules 1. **Design tokens are two-tier. Components use tier 2 only.** Tier 1 (`app/styles/primitives.css`) holds raw values; tier 2 (`semantic.css`, `typography.css`) holds meanings. Never a raw hex, never a tier-1 primitive, never a raw `font-size` / `line-height` — use the type-role utilities (`display-sm`, `heading-h2`, `body-md`, `label-md`, …). Check: `grep -rn "#[0-9a-fA-F]\{6\}\|--color-green\|--color-neutral\|--color-red" apps/web/app/components apps/web/app/routes` → must return nothing. 2. **Logical properties only. Never `left` / `right`.** `ps-` `pe-` `ms-` `me-` `start-` `end-`, `margin-inline-start`, `text-align: start`. The site ships RTL Arabic; physical directions silently break it. Check: `grep -rnE "\b(pl|pr|ml|mr|left|right)-[0-9]" apps/web/app` → nothing meaningful. 3. **Surfaces, not a theme.** `:root` *is* the dark surface; `data-surface="light"` flips a subtree and repaints itself. Do not add a global theme toggle or a `dark:` variant. 4. **Figma is READ ONLY.** Only `get_design_context`, `get_screenshot`, `get_metadata`, `get_variable_defs`. Never call anything that writes (`use_figma`, `generate_figma_design`, `create_new_file`, `upload_assets`, shaders, plugins, …). Before the first `get_design_context`, read `skill://figma/figma-design-to-code/SKILL.md` and pass `skillNames: "resource:figma-design-to-code"`. File key `yvKHp3Ha2rQmv9kt9tCC7j`. 5. **Never invent statistics, dates, names or quotes.** If a number is not in `docs/` or in the CMS, it does not go on the page. Leave the slot empty or omit the component. 6. **`docs/` is read-only source material.** Never edit, move or "clean up" those files. 7. **Both locales, always.** New UI text goes into `app/i18n/messages/en.json` *and* `ar.json` with identical keys (the `Messages` type enforces it). A component that hard-codes an English string is a bug — that is why `Card.Content`'s `actionLabel` is required. 8. **EU visibility is contractual.** "Funded by the European Union" badge top-right, project logo top-left, the three partner logos always shown together, no standalone partner branding. Do not restyle or omit them. 9. **No long-running servers or installs without being asked.** Do not run `pnpm install`, `pnpm dev`, `strapi develop` or `docker compose up` speculatively. If you need a new dependency, report it instead of installing it. 10. **No `?lang=` parameter, ever.** The locale is a URL prefix on both languages. ## Where things live | You need | It is in | | --- | --- | | Tokens, surfaces, type roles | `apps/web/app/styles/` (+ its own `README.md`) | | UI components | `apps/web/app/components/ui/{Button,Input,Card,Navigation,Footer,LanguageSwitcher}` | | Page modules | `apps/web/app/routes/*.tsx`; shared kits in `routes/_page-kit/`, `routes/_shared/`, `routes/_events-opportunities/` | | Route config, slugs | `apps/web/app/routes.ts`, `apps/web/app/lib/route-slugs.ts` | | i18n | `apps/web/app/i18n/` (`config.ts`, `paths.ts`, `t.ts`, `format.ts`, `messages/*.json`) | | Strapi client, SEO, env | `apps/web/app/lib/{strapi.ts,seo.ts,env.server.ts,route-handle.ts,site-navigation.ts}` | | HTML shell, canonical, hreflang | `apps/web/app/root.tsx` | | Content types | `apps/cms/src/api//content-types//schema.json` | | Boot-time locale + permission setup | `apps/cms/src/bootstrap/` | | CORS, upload rules, i18n locales | `apps/cms/config/{middlewares.ts,plugins.ts}` | ## Conventions that bite - **`meta` reads `loaderData`, not `data`.** In React Router 8, `matches[0].data` is `undefined`; use `readRootSeo(matches)` from `~/routes/_shared/listing`. - **Do not pass `alternates: true` to `buildMeta`.** `root.tsx` emits the canonical and the `hreflang` alternates for every route; a leaf that repeats them duplicates the tags. Use `handle.canonicalPath` to override the canonical. - **`app/routes.ts` existence-checks section modules** and Vite does not re-evaluate it when a file appears — restart the dev server after adding a route module. - **Strapi 5 addresses entries by `documentId`**, not by a numeric `id`, and localisation is a `locale` query parameter on the same entry. - **Submission content types are write-only for the public** (`create`, never `find`). They hold personal data. The field is `language`, not `locale` — `locale` is reserved by the i18n plugin. - **`.server.ts` files never reach the browser.** Anything touching `STRAPI_TOKEN`, the database or `process.env` belongs in one. - **Licensed fonts are not in the repository** (`.gitignore` excludes `public/fonts/*.woff2`). Expect fallback typefaces locally; never commit a font file. ## Verifying a change ```bash pnpm typecheck # react-router typegen + tsc (web; the CMS is checked by its build) pnpm --filter web build # the SSR build must succeed bash -n scripts/.sh # any shell script you touched docker compose config # any docker-compose.yml change (needs the docker CLI) ``` Then, in a browser or with `curl`, on **both** locales: ```bash curl -sI http://localhost:5173/ # 302 + Vary: Accept-Language curl -s http://localhost:5173/en | head -c 200 curl -s http://localhost:5173/ar | head -c 200 # lang="ar" dir="rtl" in the first bytes ``` Checklist before calling a UI change done: renders in **both** locales; Arabic mirrors correctly; no raw hex or tier-1 token; no physical-direction utility; keyboard focus is visible; text comes from the message catalogue or the CMS, not from a literal in the component.