All projects
Portfolio, things I've built and what building them taught me: a blog post, the homepage in light mode and the blog index, stacked in layers.

Completed2026

This Portfolio

This is the second version. The first one worked, but enough of it was propped up by workarounds that rebuilding on Next 16 was easier than unpicking them. The blog was the clearest case: the old site compiled MDX at runtime because the bundler path crashed on it, and that entire layer turned out to be unnecessary once the compile moved to @next/mdx through Turbopack.

Most of what I learned came from things that were quietly wrong rather than obviously broken. A single unlayered CSS rule, `a { color: inherit }`, was overriding every text colour utility on the site, because Tailwind puts its utilities in a layer and an unlayered rule beats a layered one whatever the specificity. The contact button was rendering near-black text on a black capsule at a contrast ratio of 1.18, where 4.5 is the minimum. Nothing errored. It just looked slightly off until it was measured.

The blog runs on a remark plugin that stamps heading anchors, collects the table of contents and counts reading time in one pass over the parsed post, so an anchor and the contents entry pointing at it cannot drift apart. Frontmatter is validated at build time and the build fails naming the file and the field, which is how a date like 2026-02-31 gets caught: it parses happily and silently becomes 3 March.

The parts that took longest were the ones nobody sees. Heading levels that skipped from h1 to h3 on the index pages, a mobile drawer that covered the screen while leaving keyboard focus behind it, links whose target was smaller than the minimum. CI runs lint, typecheck, the test suite and a build on every push, because a static site's build is the only thing that catches a broken MDX compile or a bad frontmatter value.

Every page is generated at build time, link previews included, drawn from the same content that renders the pages. Posts are validated as the site builds, so a broken one fails the build instead of shipping.

Next.js / Tailwind CSS / Motion / Lenis