This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Tech for Palestine Website - A community platform showcasing projects, events, and resources for tech workers supporting Palestine. Built with Astro, React, Svelte, TypeScript, and Notion API integration.
- Repository: https://github.com/techforpalestine/website.git
- Main Branch:
main - Tech Stack: Astro v5 (SSR,
output: "server"), React 19, Svelte 5, TypeScript, Tailwind CSS, Material-UI v6, Notion API - Deployment: Cloudflare Pages (via
@astrojs/cloudflareadapter,nodejs_compat) - Package Manager:
pnpm(v9+, required bypackageManagerfield — do not useyarnornpx astrodirectly)
pnpm install # Install dependencies
pnpm dev # Start dev server at http://localhost:4321 (alias: pnpm start)
pnpm build # Build for production (outputs to dist/)
pnpm preview # Preview the production build locally
pnpm check # Type-check via `astro check`
pnpm format # Format with Prettier (writes)
pnpm format:check # Format check only (CI)For CI/reproducible installs, npm ci is also supported (both package-lock.json and pnpm-lock.yaml are checked in — prefer pnpm for local dev).
No test framework is currently configured. If adding tests, prefer Vitest + @testing-library, colocated as *.test.ts(x) or under src/__tests__/.
Astro runs in output: "server" mode on the Cloudflare adapter — nearly every page is SSR'd per-request, not statically prebuilt. React/Svelte components are client-side islands, typically mounted with client:only="react" (they render nothing at build/SSR time and hydrate fully in-browser).
src/middleware/index.ts chains two middlewares via sequence(), in this order:
cache-control.ts— setsCache-Controlheaders (API routes getno-store)csp.ts— injects a per-request CSP nonce into inline<script>/<style>tags via CloudflareHTMLRewriter, and may rewrite the response
Because csp can replace the response object, cache-control must run first so its header survives the rewrite. There must only ever be one middleware entry point (src/middleware/index.ts) — a parallel src/middleware.ts would silently shadow it.
Do not build on -new pages, and do not create new ones. All design work targets the live pages.
A redesign wave once duplicated most routes as about-new.astro, events-new.astro and so on. It was shelved in #524 when the homepage A/B test closed in favour of the control. Every -new URL now 301s to its live counterpart in public/_redirects, so the ~26 page files still in src/pages/ are unreachable dead code, as are HomeLayout.astro and the design-system.css typography scale it loads. They are kept only to avoid a large deletion diff; treat them as deleted.
The one exception is ProjectsNew.tsx, which despite its name is imported by the live /projects page.
The live design system is documented in DESIGN.md, derived from /membership as the canonical page. Note that design-system.css is imported only by HomeLayout and AdminLayout, never by Layout.astro — so ts-* classes and font-serif on a public page silently render unstyled.
Experimental or orphan pages must still be added to the sitemap filter exclude list in sitemap() in astro.config.mjs so Google does not index unreachable pages.
- Events ICS feed (
src/store/eventsClient.ts) —fetchEvents()fetches and hand-parses a public ICS calendar feed (EVENTS_ICS_URL, Mattermost Events Calendar plugin), server-side only since the URL carries an auth token. Consumed by/api/eventsand grouped into category sections (src/utils/eventSections.ts) byevents.astro/Events.tsx. Seedocs/EVENTS.md. - Notion API (
src/store/notionClient.ts) — FAQ, ideas, agenda/speakers, E4P signatories, endorsements, and community calls (events no longer come from Notion). Images are direct Notion-hosted URLs (no proxy/cache — that worker was removed), expire after ~1hr, client falls back to/images/default.jpgon load error. Seedocs/NOTION.md. - ProjectHub (external service) —
src/pages/api/projects.tscallsprojecthub.techforpalestine.org/api/public/projectsdirectly and is fetched client-side via/api/projectsbyProjectsNew.tsx(the live/projectsdirectory, despite the name).src/pages/api/project-proxy.tsis unrelated — it's a generic authenticated proxy used only by the volunteer/incubator application forms. Seedocs/PROJECTS.md. - Cloudflare KV —
DROPPED_CONVERSIONSnamespace (bound inwrangler.toml) is used for conversion tracking, surfaced atsrc/pages/admin/conversions.astroandsrc/pages/api/admin/conversion-stats.ts. - Content collections (
src/content/config.ts) — currently empty (collections = {}); older docs referencingcontent/ideasandcontent/projectsmarkdown collections are stale — checksrc/content/before relying on this.
Always resolve env vars through getEnv(name, locals) (src/utils/getEnv.ts), which falls back across Cloudflare runtime env → import.meta.env → process.env in that order. Never read process.env directly in code that runs on the Cloudflare Pages runtime. For local dev, .dev.vars populates the first tier (matches production), .env only reaches the second/third — see docs/ENVIRONMENT.md for the full mechanism and a variable-by-variable audit.
This project has undergone multiple rounds of security auditing (see security_audit/). These rules come directly from findings that were fixed — do not regress them.
- Secrets live only in the Cloudflare Pages dashboard env vars, never hardcoded in
wrangler.toml,astro.config.mjs, or source. Resolve viagetEnv(). Never import/reference secrets in client-executed code (e.g.src/store/api.ts) — add a server-side proxy route undersrc/pages/api/instead. - Webhooks authenticate via header (
X-Webhook-SecretorAuthorization: Bearer), never a URL query param (those leak into access logs). - Secret comparison must use
constantTimeEqual(a, b)fromsrc/utils/crypto.ts, never===. - Proxy routes (
src/pages/api/project-proxy.tsis the existing example) must normalize the upstream path withnew URL(path, "http://localhost").pathnamebefore checking it against an explicit allowed prefix, and must build an explicit header allowlist rather than forwarding all incoming headers (which would leakCookie,X-Forwarded-For, etc.). - CORS: never
Access-Control-Allow-Origin: *on write endpoints (POST/PUT/PATCH/DELETE) — usehttps://techforpalestine.orgexplicitly. Read-only GET endpoints serving public data may use*. - CSP: managed only in
src/middleware/csp.tsvia per-request nonces (HTMLRewriter). Never add'unsafe-inline'toscript-src/style-src, never usestyle=""inline attributes (silently blocked in prod, appears to work in devtools), and don't add new external script origins without review. - Public POST endpoints must validate required fields, email format, URL format (
try { new URL(value) } catch), cap free-text at 2000 chars, and reject ifOrigin !== "https://techforpalestine.org"before parsing the body. - Errors: never return raw error objects/stack traces to clients — generic message +
console.errorserver-side only. Never log full secrets or PII (emails:[redacted]@${domain}only).
- When removing or renaming a page, add a 301 redirect in
public/_redirectspointing to the closest equivalent page. - Custom 404 lives at
src/pages/404.astro, using the standardLayout.astro(keep site nav visible). - Any test/staging/orphan page must be added to the sitemap
filterexclude list inastro.config.mjs.
src/
├── components/ # React/Astro/Svelte components (events/, home/, hook-form/, projects/, ui/, membership/, london-gathering/)
├── content/ # Content collections config (currently empty — see note above)
├── layouts/ # Layout.astro (shared page layout)
├── lib/ # report-error.ts
├── middleware/ # index.ts (sequence entry point), cache-control.ts, csp.ts
├── pages/ # File-based routes; api/ for endpoints, admin/ for internal tools
├── store/ # Notion client and data-fetching utilities
├── structures/ # Reusable Astro structural components (forms, buttons)
├── styles/ # Tailwind entry (base.css)
├── types/ # Shared TypeScript types
└── utils/ # getEnv.ts, crypto.ts, basicAuth.ts, helpers.ts
- Pages:
kebab-case.astro - Components:
PascalCase.tsx/jsx/astro/svelte - Utilities:
camelCase.ts - Content:
kebab-case.md
Full index: docs/README.md. Highlights: docs/ARCHITECTURE.md (middleware chain, data sources, the abandoned -new redesign), docs/API.md (every route's auth/upstream), docs/SECURITY.md (audit-derived rules with the incident behind each), DEPLOYMENT.md (full env var list).
Always verify the working branch hasn't been merged before committing. Use git log --oneline main..HEAD to check for unmerged commits before starting new work on a branch.
- When I say "ship it" I want you to create a new branch (if we're on main or a previously merged feature branch), stage and commit new edits, push it to github and open a PR.