Colophon
How this site is built
Small site, team-sized setup. The point is the process: anyone on a team — engineer or not — can ship a change without breaking production.
One monorepo
pnpm workspaces and Turborepo, the same shape I use at work. The site and the component library live side by side; Turborepo runs lint, typecheck, test and build across both and caches what didn't change.
jinawong-site/
├─ apps/site/ Astro site (static output)
│ ├─ src/content/ case studies (Markdown + zod schema)
│ ├─ src/pages/ routes — /, /work, /about, /ui …
│ ├─ functions/api/ Cloudflare Pages Function (contact, off)
│ └─ wrangler.toml Cloudflare Pages project config
├─ packages/ui/ React + TypeScript component library
│ ├─ src/tokens/ design tokens (CSS variables, light/dark)
│ ├─ src/components/ Button, Card, Tag, Timeline, …
│ └─ .storybook/ Storybook
├─ .claude/commands/ Claude Code slash commands
├─ .github/workflows/ci.yml lint → typecheck → test → build → Lighthouse → preview
├─ CLAUDE.md conventions for humans and agents
└─ turbo.json task graph + cachingAstro, with islands only where needed
Every page is pre-rendered to static HTML. React components from the library render on the server and ship zero JavaScript by default. Only the component gallery hydrates a React island, and only when it scrolls into view (client:visible). The theme toggle in the header is the library component's static HTML, wired by a few lines of framework-free script — no React on the page.
A component library with design tokens
packages/ui is a small React + TypeScript library: Button, Card, Tag, Timeline, MetricStat, SectionHeading and ThemeToggle. Styles are SCSS Modules that read only from design tokens — CSS variables for color, spacing and type, with light and dark themes. Each component has Storybook stories and Vitest + Testing Library tests that check behavior and accessibility (roles, names, aria-pressed), not snapshots.
Content with a schema
Case studies are Markdown files in an Astro Content Collection. A zod schema defines every field. A missing field, a wrong date format or a typo in a key fails astro check in CI, with a message that says which file and which field.
period: z.string().regex(/^(Mon )?YYYY( – (Mon YYYY|Present))?$/),
stack: z.array(z.string()).min(2).max(10),
problem: z.string().min(40).max(500),
outcome: z.array(z.string()).min(1).max(5),
// z.strictObject: an unknown or misspelled key fails the buildCI with budgets
GitHub Actions runs the whole pipeline on every pull request. Lighthouse CI enforces a performance and accessibility budget of 95 — a slow or inaccessible change fails like a failing test.
pull request
│
├─▶ lint ESLint (TS, Astro, React hooks, jsx-a11y)
├─▶ typecheck tsc + astro check ◀── content schema runs here
├─▶ test Vitest + Testing Library (packages/ui)
├─▶ build astro build + storybook build
├─▶ lighthouse perf ≥ 95 · a11y ≥ 95, or the PR fails
└─▶ preview Cloudflare Pages preview URL on the PRCloudflare Pages
Static output deploys to Cloudflare Pages, with a preview URL for every pull request. A tiny Pages Function for a contact form is included behind Cloudflare Turnstile and stays switched off until it's configured.
Non-engineers ship safely, with Claude Code
The repo is set up so that someone who doesn't write code can still add a case study and get it live, with guardrails at every step:
- Open the repo in Claude Code and run
/new-case-study. It asks a few plain-language questions and creates the file from a template. - Claude follows
CLAUDE.md— the house rules: where content goes, the schema, the tone, and what never to publish (private screenshots, internal code, secrets). - It runs the same checks as CI locally (
pnpm check) and fixes what fails before opening a pull request. - CI repeats every check, and Cloudflare posts a preview link to review the real page.
- An engineer approves and merges. Production updates on merge.
The written guide is CONTRIBUTING-for-non-engineers.md in the repo. Built with Claude Code, directed and reviewed by me — the same way I work day to day.