7.7 KiB
CLAUDE.md — working brief for agent sessions
Read this before touching anything. Human setup instructions live in 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
- 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 rawfont-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. - 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. - Surfaces, not a theme.
:rootis the dark surface;data-surface="light"flips a subtree and repaints itself. Do not add a global theme toggle or adark:variant. - 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 firstget_design_context, readskill://figma/figma-design-to-code/SKILL.mdand passskillNames: "resource:figma-design-to-code". File keyyvKHp3Ha2rQmv9kt9tCC7j. - 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. docs/is read-only source material. Never edit, move or "clean up" those files.- Both locales, always. New UI text goes into
app/i18n/messages/en.jsonandar.jsonwith identical keys (theMessagestype enforces it). A component that hard-codes an English string is a bug — that is whyCard.Content'sactionLabelis required. - 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.
- No long-running servers or installs without being asked. Do not run
pnpm install,pnpm dev,strapi developordocker compose upspeculatively. If you need a new dependency, report it instead of installing it. - 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/<name>/content-types/<name>/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
metareadsloaderData, notdata. In React Router 8,matches[0].dataisundefined; usereadRootSeo(matches)from~/routes/_shared/listing.- Do not pass
alternates: truetobuildMeta.root.tsxemits the canonical and thehreflangalternates for every route; a leaf that repeats them duplicates the tags. Usehandle.canonicalPathto override the canonical. app/routes.tsexistence-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 numericid, and localisation is alocalequery parameter on the same entry. - Submission content types are write-only for the public (
create, neverfind). They hold personal data. The field islanguage, notlocale—localeis reserved by the i18n plugin. .server.tsfiles never reach the browser. Anything touchingSTRAPI_TOKEN, the database orprocess.envbelongs in one.- Licensed fonts are not in the repository (
.gitignoreexcludespublic/fonts/*.woff2). Expect fallback typefaces locally; never commit a font file.
Verifying a change
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/<name>.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:
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.