- company: Acme Corp role: Senior Engineer applied: '2026-05-01' status: AppliedThose four lines are the whole job tracker. No database, no CMS. I edit the YAML, Astro rebuilds the static site, and Cloudflare Pages publishes it. A Zod schema validates every entry at build time, so a malformed date or a status outside the allowed set fails the build instead of shipping a broken card.
The site at tim.sillysamoyed.com began as a one-page resume and grew into something I run like a small product. I did not hand-write it. I paired with Claude Code, Anthropic’s coding agent: I set the direction and reviewed every diff, and Claude wrote the code and the tests. Every commit in the history carries a Co-Authored-By line for it. Each page reads from data, every behaviour carries a test, and six GitHub Actions workflows keep the whole thing honest.
Every page reads from data
Three YAML files feed the site: resume.yml, projects.yml, and jobs.yml. The pages hold layout and styling; the content lives in the data. Change a job status in one place and the board, the counts, and the feeds all move with it.
The job board borrows Jira’s shape: a column per status, a card per application. One rule does work I would otherwise forget to do by hand. An Applied entry flips to Ghosted once it has sat 28 days without a reply.
export function effectiveJobStatus(job: Job, now: Date): JobStatus { if (job.status === 'Applied' && daysSince(job.applied, now) >= 28) { return 'Ghosted'; } return job.status;}The YAML never mutates. getJobs() derives the display status at render time, so the source file stays a clean record of what I submitted and the board shows the truth about who went quiet.
The pages, and why each one exists
The home page carries the bio and a stat strip that counts years of experience and the active pipeline straight from the data. The resume renders the full CV and prints clean when a recruiter hits Cmd+P.
The projects grid pulls stars, forks, and a “2 days ago” recency label from a generated project-stats.json, refreshed every night from the GitHub API. I never hand-edit those numbers. The job-hunt board is the tracker above. The blog is an Astro content collection, and this post lives in it.
The testing page is the odd one. It documents the test strategy and shows live counts pulled from the suite at build time, plus real per-step durations from the last green CI run. A page about the tests, fed by the tests.
Three feed endpoints round it out: RSS, Atom, and JSON Feed, each built from the same posts.
Tests come in two layers
The rule I hold to: unit first, browser second. Decision logic moves into src/lib/ as a pure function with a colocated *.test.ts, so a Vitest case can exercise it with no DOM. The 28-day ghosting rule, the nav focus-trap math, the blog paging, the theme-picker keyboard navigation all live as pure functions and carry their own tests. That comes to 247 unit tests across 20 files.
Browser tests cover the slice that needs a real page: navigation, focus, viewport layout, the drawer that opens below 425px. Playwright handles those, 213 of them across 11 specs.
A bug fix ships with a test that fails on the old code and passes on the new. If the existing tests could not have caught the regression, the gap is the point.
One spec, seven projects, three engines
Run all 11 specs against all 7 Playwright projects and you get 784 test runs. Most of that is waste. Content renders the same in Chrome, Firefox, and Safari, so the content specs run once. Keyboard and focus behaviour drifts between engines, so the a11y specs run on all three. Layout depends on viewport, so the responsive specs run on mobile and tablet.
{ name: 'a11y-firefox', use: devices['Desktop Firefox'], testMatch: /(nav|theme-picker)\.spec\.ts/,},Routing each spec to where it earns its run cuts 784 down to 213. The coverage I care about stays; the redundant Chrome-versus-Firefox reruns of identical HTML go.
The pipeline gates every merge
A pull request into main runs ci.yml: a doc-sync check, then lint with zero tolerance for warnings, then the unit suite under a V8 coverage gate, then a full TypeScript typecheck, then the build. Any step fails and the merge stops.
The browser suite runs in its own workflow. playwright.yml waits for the Cloudflare Pages preview deploy, then points Playwright at the live preview URL, so the tests hit the same static output a visitor would.
- run: ./scripts/ci/check-claude-md.sh- run: pnpm lint- run: pnpm test:coverage- run: pnpm typecheck- run: pnpm buildThe doc-sync check is the one I reach for most. It fails the build when a dependency, source file, workflow, or script drifts out of step with the README and the contributor guide, so the docs cannot rot while the code moves.
The site keeps itself current
Three nightly workflows commit generated data straight to main. One refreshes the GitHub stars and forks on the projects grid. One refreshes the CI snapshot that feeds the footer and the testing page. One regenerates the test counts whenever a spec changes, so the numbers on the testing page match the suite that produced them.
A fourth workflow reads the merged commits and bumps the version from the Conventional Commit type: feat earns a minor, fix a patch, a breaking change a major. The footer version moves on its own as I merge.
The tests are how I trust code I didn’t type
Most of this code, I never typed. The loop runs like this: I describe a feature or a bug in plain language, Claude Code writes the code and a failing-first test, runs the suite and the typecheck, and opens the PR. I read the diff, I run it, and I merge. My name and Claude’s sit on every commit.
That loop puts more weight on the tests, not less. The 460 of them are how I check a change I did not write line by line. A green suite, a passing typecheck, and a coverage gate turn “the agent says it works” into something I verify before it reaches the branch. The data stays mine: my real applications, my CV, my project list. The code that renders them, and the tests that guard it, came from the pair.
I set the direction and reviewed the diffs. Claude Code wrote the code and the tests. The 460 tests are the contract that lets me trust the result and merge it.
That is the whole site: data in YAML, logic in tested functions, a pipeline that blocks a bad merge, and a set of workflows that keep the generated parts fresh. A human and an agent built it, one reviewed commit at a time. The source is open at github.com/timjstacey/resume-static-site.