← Back to all projects

Case study

leonardonunez.dev

  • Astro
  • TypeScript
  • CSS
  • Playwright
  • Vitest
  • Cloudflare Workers

The problem

Interested viewers often open a portfolio on a phone, in a spare minute. Much of this site’s code is written by Claude Code sessions, and none of them has ever looked at a phone. So nobody can be relied on to notice a broken layout: every change has to prove itself at every width before it reaches the live site.

What every change must pass

Every pull request runs the same gate (ci.yml). The build checks, Playwright and Lighthouse are required, so nothing merges until they pass; the rest report on every change:

  • lint, typecheck, unit tests, and tests on the built pages: meta tags, links, picture sizes and the JavaScript budget;
  • Playwright at 360, 390, 768 and 1280 px: every page loads, nothing scrolls sideways (layout), nothing overlaps while pictures and animations are still loading (loading), axe finds no serious accessibility problems in light or dark mode (a11y), and screenshots match their baselines (visual);
  • Lighthouse on a simulated phone, at 90 or above for performance and accessibility (lighthouserc.json);
  • a link checker, proved by a deliberately broken link before it went live (#35); CodeQL; and a check that no personal details or local paths are committed.

Where it breaks

The page below is laid out at four real screen widths; the tabs switch between them. Pick a bug to inject: each width then runs the check CI runs, from the same code (#46), the tabs show where it fails, and the figure opens the first one. Then fix it.

Pick a bug to inject. Each frame then runs the check CI runs on every page.

What it caught, and what it missed

The gate earns its keep on bugs nobody sees on their own screen. In #40 the satis-manager figures looked right on my Windows machine, but CI’s Linux fonts set a label wider, and on tablets a route crossed “CLOUDFLARE”. The check failed, the route was redrawn to climb clear, and a unit test now keeps the clearance. The same tests stopped this page’s own title before it went live: “leonardonunez.dev” is one long word, and on a 360 px phone it ran 65 px off the screen until the title was allowed to break after the dot (#46).

It misses things too. A grid meant for the home page’s portrait applied to every page header, so on tablets and desktops the titles of two pages split into two columns. It passed every check, because no check asked where a title starts. I found it on the live site, and #38 fixed it and added that check, which failed before the fix.

Pictures without reserved space were a miss too: the case study’s video and diagrams grew from nothing and pushed the text down as they loaded, on the live site, until #35 reserved their space and added the loading tests that now guard every page.

Fast by default

A page runs no JavaScript unless it has an interactive figure, and a page with one gets 15 KB, gzipped (budget.ts). The build tests fail any page over its budget, and a figure’s script loads only when it scrolls into view.

Page JavaScript, gzipped
/ 0 KB
/about 0 KB
/projects/satisfactory-dash 13.6 KB
/projects/lanes 5.8 KB
/projects/leonardonunez-dev 3.4 KB

Built files with hashed names are cached for a year and pages not at all, so a deploy shows on the next visit (_headers). In Chrome, a page starts loading when you hover its link (speculation rules).

Limits and trade-offs

  • Screenshots are valid from one machine only. Fonts render differently on Windows, macOS and Linux, so baselines come only from Playwright’s container on CI. Every intended visual change costs a round trip: run the workflow, check its images, commit them.
  • A tolerance is a blind spot. Full-page screenshots allow 1,500 scattered pixels so that anti-aliasing noise doesn’t fail them, which means a change that small passes too.
  • Local isn’t CI. A figure can pass on my machine and fail in CI on font widths alone (#40). Figures now leave generous clearance.
  • Tests can be wrong too. The site’s smooth scrolling made a test measure a figure mid-scroll, and it failed until it scrolled instantly (#43). Writing the table above showed the budget check had never counted a figure’s lazily loaded script: the minifier quotes that import with backticks, which the check didn’t read. Every page still fit once it did (#46).
  • It only checks what someone thought of. The split titles passed everything (#38). Checks grow from misses, and my own review of each preview is still part of the gate.
More detail: how the figure checks a page it scaled down

How it was built

Built with Claude Code sessions, each change reviewed by me on its preview before it merged. All three case studies share one small figure kit, and the site is open source on GitHub.