How this site was built

I built this site collaboratively with Claude Code, working from a structured roadmap rather than ad hoc prompting: phases, done when criteria, and a running log of what actually happened, not just what was planned. Below: the real performance and accessibility numbers, what the stack actually is, and a few moments from that process.

Lighthouse, checked on every route

Ran Lighthouse’s command line tool directly against the built production site, not the dev server, across seven representative routes: the homepage, the work index, a case study, About, Art, Contact, and a tech filter page. Accessibility lands at 100 on every single one, and it got there by finding and fixing real issues the tool surfaced (see the links story below), not by re running it until the number looked right.

Lighthouse scores by page
PagePerformanceAccessibilityBest PracticesSEO
Home98100100100
Work98100100100
Case study (GOARMY)99100100100
About99100100100
Art92100100100
Contact99100100100
Tech filter (React)99100100100

The one honest outlier is Art, at 92. That page renders full resolution photography of my own artwork, and Lighthouse wants heavier compression than I’m willing to apply to work I actually made. Left as is on purpose rather than trading image quality for a higher number.

Built to actually be usable

Every color pairing on the site was checked against real WCAG contrast math before shipping, not eyeballed. Every interactive element (the lightbox, the password protected resume download, the work page’s tech stack filter) is a real button or link with visible focus states and, for the filter, an aria-live region that announces the result count as it changes. The lightbox and resume download both trap keyboard focus while open rather than letting it leak out to the page behind them.

A 100 across every route is the automated third of that claim. Automated tools cannot verify everything: whether a screen reader announcement actually makes sense out loud, or whether every interactive element is reachable with a mouse unplugged. That human pass is part of what "done" means here, not a step skipped because the score already looked good.

Under the hood

Astro 7 for the site itself, with React islands used only where real interactivity is needed: the image lightbox and the password protected resume gate, hydrated on demand rather than shipped on every page load. Styling is Tailwind v4 with self hosted fonts, no third party font requests.

Project content lives in a schema validated content collection: every case study’s cover image and gallery images are checked against real files at build time, so a broken image path fails the build instead of shipping a broken image to production. Those images are then processed automatically into responsive WebP versions at multiple widths, with explicit dimensions on every tag so the browser can reserve layout space before the image loads.

Hosted on Cloudflare Workers using native static asset bindings, with a KV namespace backing the resume password gate rather than storing the file as a public static asset. The social preview image is generated from the site’s actual fonts and color tokens with a small build script, not a generic placeholder. Every page has a real canonical URL and a unique title, a sitemap and robots.txt are generated automatically, and the two pages that shouldn’t be indexed (this site’s 404 page and the contact form’s thank you page) are explicitly marked not to be.

A few moments from the process

  • Google Analytics was live and silently broken

    The tag loaded correctly on every page and looked properly wired up in the markup. It was actually dropping every single hit. The tracking code pushed a plain array into dataLayer, but Google’s gtag.js only recognizes entries that match a real JavaScript arguments object, so every event was discarded before it ever reached Google’s servers. Found by reading gtag.js itself instead of assuming the snippet was correct because it came from Google, then fixed by pushing the actual arguments object instead of an array.

  • Links that only had color to tell them apart

    A Lighthouse accessibility run flagged inline links on the homepage and About page for relying on color alone, with no underline, to be distinguishable from the surrounding sentence, a real WCAG failure for anyone colorblind or on a low contrast display. That single automated run only caught two of the four places it existed. Reading the actual audit output, not just the score, led to the same pattern in the site footer (present on every page) and the process page, both untouched by that run. Fixed everywhere by making the underline the default state, not something that only appeared on hover.

  • Diagnosed a Workers vs. Pages configuration mismatch

    Every image on the live site was broken, despite working perfectly in local builds. Traced it to Cloudflare’s Workers deployment path silently falling back to an on demand image endpoint instead of serving the pre built static files, by comparing the local build output against what actually shipped in production. Fixed by configuring the Workers native asset bindings directly, rather than assuming the Pages oriented defaults would apply.

  • Found a real browser bug, not just a CSS mistake

    Client logos were silently invisible in the "Trusted by" section. Traced it to a genuine Chromium quirk: SVGs loaded via <img> with only a viewBox can collapse to zero size inside CSS Grid with centered items. Reproduced it in isolation before touching the real fix, so the fix was based on evidence, not a guess.

  • Caught PII before it shipped, not after

    A batch of old project screenshots included one of a real internal employee directory: actual coworkers’ names and personal cell numbers. It would have been easy to use as is; instead every image got a visual check before being attributed to a project, and that one was pulled and flagged for redaction rather than published.

  • Every project follows the same real structure

    Problem, role, process, outcome: enforced by a shared content schema, not just a writing convention. Where the real number or the real brief wasn’t available, it says so directly instead of filling the gap with something that sounds plausible.