Colophon

How this site is built.

Most portfolios tell you what their clients got. This one also tells you how the portfolio itself is made. If something here is useful for your own project, help yourself.

Stack

Stack.

Framework
Next.js 16 (App Router) on React 19
Language
TypeScript everywhere
Styles
Tailwind CSS v4 with @theme tokens; light by default, dark via a data-theme attribute set by an inline bootstrap script (no flash)
Motion
framer-motion, throttled carefully on mobile
Hosting
Vercel Pro, SSR mode
Data
Vercel KV (Upstash Redis) for the portfolio config, rate limits + any toggle-without-deploy flags
Chat
Claude Haiku answering from a written knowledge base, with Voyage embeddings picking the passages each question gets
Forms
Own server endpoint, delivered via Resend
Analytics
Self-hosted Umami (no cookies, no stored IP addresses) + Vercel Speed Insights. A sample of visits is recorded as click maps and replays - the privacy page says exactly what
Auth (admin)
NextAuth v5 with GitHub OAuth
Typography

Typography.

Four typefaces, all self-hosted via next/font and cut down to the characters, weights and widths the site uses. Together they come to 239 KiB, down from 578:

Geist Sans
Body text, form fields, the chat. Variable weight, upright only. The workhorse.
Bricolage Grotesque
Headings. Variable wdth + wght. Headlines stack a wdth-100 line over a wdth-75 line for the editorial contrast that runs the brand.
Fraunces (italic only)
Editorial accents - phrase-level emphasis, sub-titles, FAQ questions. opsz axis for optical sizing. Italic only, weights 400 to 500, which is all the site sets it in.
Geist Mono
Marginalia, page-numbers, hung labels, domains, project indices. Treats numerals as a typographic citizen, not a fallback.
Performance

Performance.

A live PageSpeed Insights run is one click away. These are from Lighthouse run against all 21 public routes on 13 October 2026:

Desktop perf
98 / 100 average, 96 to 99 across the routes
Mobile perf
81 average on Lighthouse's throttled phone profile, from 75 on the home page to 90
Accessibility
100 on all 22 routes, WCAG AA contrast
Best practices
100 across the site
SEO
100 across the site
Homepage HTML
19 KB transferred on first load
Portfolio images
WebP q88 @ 1600w max, 80-150 KB each after a hard audit (PNG originals were 2-3MB)
Regression guard
Lighthouse CI runs on every PR against the Vercel preview, asserts on thresholds
Late fonts
Lighthouse always gets the fonts before first paint, so it cannot see a page jump when they arrive late. A script holds them back 3 seconds and measures the shift on every route; all are under 0.1
Screenshots

How the portfolio screenshots work.

A Puppeteer script at scripts/screenshots.mjs hits every live client site, waits for animations to settle, dismisses common cookie banners, and writes a PNG at 1280x720 @ 2x. The PNGs get piped through sharp to produce the WebP versions used in production. The same script pipes frames of a soft scroll into ffmpeg for the silent VP9 loops on the home page, one per project at desktop and phone size. A loop only plays on the front card once the wheel has come to rest, and not at all on data saver.

Theming

Theme bootstrapping.

Light is the default; the toggle is backed by CSS custom properties under a [data-theme] attribute on <html>. An inline <script> in the head reads localStorage.theme and sets the attribute before first paint, so anyone who picked dark never sees a light flash.

A11y

Accessibility.

Semantic HTML first, ARIA where it adds value. Every interactive element is keyboard-navigable with a visible focus ring. Motion-heavy sections respect prefers-reduced-motion. Text contrast is WCAG AA in both themes, verified via tokens rather than eyeballing hex values.

Retros

Bits I'd do differently.

No site is a straight line. A few retros from the build:

  • I shipped a WebGL domain-warped gradient shader as the hero background because it looked premium on my desktop. Mobile Lighthouse promptly returned a 41 and 167 seconds of blocking time. I spent a session progressively throttling it before admitting two different heroes for two different viewports was a worse outcome than one editorial hero everywhere. That hero has since made way for the project wheel.
  • The "Lighthouse 95+" badge in the footer is true on desktop (avg 98) and aspirational on mobile (avg 81). The PSI link is one tap away so anyone can check.
  • The first portfolio data pass had V Clarke Books' headline metric reading "Author-managed via Decap CMS" - technically true, but a tool name is not an outcome. The CMS has since changed to Sveltia and the metric still names it. Still on the backlog to replace with a real figure.
  • Body text was meant to be Geist Sans from the start. For months it was the visitor's system font: the font classes sat on <body> where the stylesheet could not see them, and nothing errored. I only found out when I went to cut the font files down and saw nothing was drawn in it.
  • The first Lighthouse sweep surfaced a 3.5MB PNG hero photo and five portfolio screenshots totalling ~10MB. The second sweep surfaced that I had been compressing them with the wrong settings for a week. Lesson: measure the bytes on the wire, don't trust the build report.
Build 1c0b534