Files
sharper.ly/CLAUDE.md
T
2026-09-23 21:57:24 +02:00

8.5 KiB
Raw Blame History

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

  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. Never add a second theme system or a dark: variant. The one exception, accepted deliberately: the nav's theme toggle flips which surface <html> stands on (dark default ↔ light), persisted via the sharper-theme cookie read in the root loader and set by the root action — SSR-rendered so the first byte and first paint always match. The mechanism stays surfaces-only: the toggle just re-roots them. --surface-nav must keep a value in BOTH surface blocks.
  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. The same rule governs the seed: anything invented belongs in src/seed/demo.ts, never in approved.ts, and pnpm seed without --demo must stay publishable.
  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/<name>/content-types/<name>/schema.json
Boot-time locale + permission setup apps/cms/src/bootstrap/
CMS seed content (pnpm seed) apps/cms/src/seed/ — approved.ts is from docs/; demo.ts is invented, opt-in behind --demo, every slug prefixed demo-
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

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.