colophon
Colophon
This site is one of the projects. Here is how it is built and what I decided along the way.
- performance
- ≥ 90
- accessibility
- 100
- best practices
- ≥ 95
- seo
- 100
Lighthouse, asserted on every build across every public route — not a one-off screenshot of the fastest page. These are the floors the build fails below, deliberately quoted instead of a measurement: performance moves a couple of points between identical builds, so any number pinned here would be wrong within a week. It held at 96 before the photography, and the floor moved to 90 on purpose, spent on images.
Stack
Next.js 15 (App Router) with React 19 and TypeScript in strict mode, styled with Tailwind CSS 4. Case studies are MDX validated with Zod. Tests are Playwright, including accessibility scans with axe-core. Hosted on Vercel, deployed on every push to main.
There is no database, no CMS, and no analytics. The whole site is a static export — output: 'export' — so it could move to GitHub Pages tomorrow without a code change.
Decisions worth explaining
Static export as a constraint from commit one. Turning it on at the start means nothing can quietly grow a server dependency. It forced one interesting call: the projects filter avoids Next's useSearchParams, because that would replace the prerendered card list with an empty loading fallback in the exported HTML. Filter state is ordinary React state plus history.replaceState, so deep links work and every card is still in the source.
Theming with no dark-mode variants. Colors are semantic tokens declared with the CSS light-dark() function, so components write text-muted rather than text-neutral-600 dark:text-neutral-300. The toggle sets one attribute; an inline script applies a saved preference before first paint so there is no flash.
Content validated at the boundary. Every case study's frontmatter is parsed with Zod during the build. A category typo or a slug that disagrees with its filename fails the build rather than rendering something broken.
One builder for every page's metadata. Titles and descriptions were per-page from the start, but the Open Graph and Twitter tags were not — so every case study shared as the homepage, with og:url pointing at /. The fix routes all metadata through a single function, because Next merges metadata between layouts and pages shallowly: a page that declares its own openGraph replaces the layout's object outright, silently taking og:site_name, twitter:card and the inherited image with it. One place to get that right is one place to test it.
The interactive demo is deterministic on purpose. The PID lab on the Fast Robots case study runs no random numbers and reads no clock, so the server render and the browser compute the same curve. That is what lets the default response — both traces and the measured overshoot, settling time, and steady-state error — ship inside the exported HTML: without JavaScript you still get a real chart with real numbers, just no sliders. It costs 2.4 kB of JavaScript and no dependency, and it did not move that page off the top of the performance range.
Accessibility measured, not claimed. axe-core runs over every route in both themes and both viewports on each test run. It has already earned its place: it caught a reduced-motion rule that zeroed animation durations but not delays, which left content invisible for 240 ms for exactly the users who had asked for less motion.
What I would do next
- Migrate the Fast Robots lab reports into native MDX so everything lives under one domain and one design system.
- Extend the interactive work past the PID lab on the Fast Robots case study — a Kalman visualizer is the obvious next one, though it needs noise, which is harder to keep deterministic.
- Push the remaining case studies past the write-up stage, and win back the performance the photography cost — the heading is waiting on a font, so subsetting is the next thing to try.
The source, including the build plan and a per-story log of decisions and mistakes, is on GitHub.