The Lab · 03 · Teardown
How this site is built
A designer's portfolio is a strange artifact: it is the one deliverable where the medium is also the sample. So here is the build, measured rather than described — what it costs, what it depends on, and what is still missing.
A site that argues for evidence must be willing to be inspected itself.
Measured
The whole thing, in numbers
| What | Measured | How |
|---|---|---|
| Hand-written HTML pages | 44 | find . -name "*.html", excluding the book and print sources |
| Stylesheet, entire site | 313.2 KB | stat -f "%z" styles.css → 320,685 bytes, unminified |
| Runtime dependencies, classic site | 0 | No root package.json, no node_modules, no bundler config |
| Third-party domains serving code | 2 | Google Analytics and Microsoft Clarity (heatmaps, added 2026-08-15; honours Do-Not-Track). Attention measurement (scroll depth, section dwell) is first-party: attention.js, 159 lines, no network of its own. Both load async and gate nothing on first paint. Google Fonts left on 2026-08-14 when the nine font files moved to this origin. |
| Interactive pattern demos | 9 | All in patterns/demos.js — 234 lines, 12.9 KB |
| Its imports, requires, fetches | 0 | grep -nE "import |require\(|fetch\(" → no matches |
| Open Graph share cards | 33 | All exactly 1200×630, verified with sips -g pixelWidth |
| Pages carrying a skip link | 38 | grep -rl skip-link |
| Build steps between edit and deploy | 0 | Commit to master; GitHub Pages serves the files as written |
Decisions
Four choices, and what each one cost
No build step, and therefore no build to break
There is no bundler, no transpiler, no CI pipeline. A file I edit is the file the browser receives. The benefit is that this site cannot rot: no dependency will publish a breaking major, and nothing in it needs a lockfile to reproduce in three years.
What it costs: no tree-shaking, no automatic minification, and cache-busting done by hand — you can see the manual query strings, styles.css?v=p77, in the source of every page. On a site of this size that is the right trade. On a product it would not be, and I would not defend it there.
Fonts are served from this origin, not a third party
Nine woff2 files, latin subsets only, preloaded for the two faces that gate the first paint. No preconnect, no Google Fonts, no request leaving this domain for type. font-display:swap paints text immediately in the fallback and re-renders when the face arrives.
<link rel="preload" as="font" type="font/woff2" crossorigin
href="/assets/fonts-web/source-sans-3-normal-400.woff2">
<link rel="preload" as="font" type="font/woff2" crossorigin
href="/assets/fonts-web/source-serif-4-normal-300_600.woff2">
<link rel="stylesheet" href="/fonts.css">
One type scale, written down, enforced by refusal
Every size on the classic site comes from twelve custom properties — --fs-eyebrow through --fs-display-xl — declared once in styles.css and documented in a 174-line spec in the repository. The rule in that file is one sentence: if a value isn't in this file, it doesn't ship. Roughly twenty earlier one-off sizes are listed explicitly as retired, so a stray font-size: 17px reads as a bug rather than a preference.
Writing this page is what caught the drift described at the bottom of it.
The documents are the same HTML, printed
The portfolio PDF and the résumé are not designed twice. They are HTML pages with an @page rule and millimetre units, printed to PDF by Chrome — which is why a wording change reaches the PDF in one step. The ATS-readable .docx is generated by a 107-line Node script using the docx library: the only npm dependency anywhere near this project, and it is offline tooling that never ships to a browser.
One honest exception
The book view does use React
"Zero dependencies" is true of the classic site you are reading and false of the book edition, which is a React application — around 120 KB of my own code plus React and React DOM, self-hosted from the repository rather than a CDN, so the page still has no third-party runtime.
Two views of one portfolio, built two ways, was a deliberate exercise: the classic site proves I can work without a framework, and the book proves I can work with one. It also means I have opinions about both that came from shipping, not reading.
What's missing
“A hand-built static site doesn’t prove you can ship a modern stack.” The book edition of this same site runs React — the stack shows up where the job needs it and stays out where it doesn’t. That allocation, not the absence of a framework, is the decision on display here.
The gaps, before you find them
A teardown that only lists strengths is a brochure. These are real, and they are the next things I would fix.
- There were no automated tests at all until this week. The repository contained zero test files and an empty CI directory; verification was a manual checklist — every page at two widths, no overflow, console clean. The 42 assertions behind loop.js are the first tests in this codebase, which is a genuine gap being closed, not a strength.
- No continuous integration. Nothing ran those 42 tests on push. A GitHub Action now runs node lab/loop.test.js on every push and pull request touching lab/ — this repository's first CI job.
- Cache-busting is manual. Version query strings are bumped by hand, which means they can be forgotten — and have been.
- Two third-party domains gated the first paint. Fixed 2026-08-14: all nine font files are now served from this origin, Google Fonts is gone from the critical path, and one third-party domain remains (analytics). The type stack changed with it — Source Serif 4 and Source Sans 3, a superfamily, replacing a mix that had monospace carrying 34 labels a page.
- A token had drifted out of sync with its own spec. The design system documented --fs-display while the stylesheet defined --fs-display-xl — a rename that never reached the document. Found while writing this page, and fixed in the commit that published it.