How this site is built

This portfolio is a small codebase, and I run it like a production one: typed code, tests on every change, and no step I can't repeat. Here is what is under the hood and why. The source is on GitHub.

Stack

  • Next.js 16 with static export. I work with Next.js every day. Here it builds every page into a plain HTML file, which my shared host serves as is. That gives each page its own URL and link preview, with no server to run.
  • Server Components for the content. The sections are rendered to HTML at build time, so the page is readable before any JavaScript runs, or without it. Only a few small pieces run in the browser: the header and the command palette, the clickable architecture diagram, the reveal of the testing bars, and the case studies' "On this page" list.
  • TypeScript in strict mode, React 19.
  • Plain CSS with design tokens. Colours, spacing and fonts are custom properties on :root, redefined for dark mode. No UI framework.
  • One design family with my CV. The standard style keeps the CV's palette and type, warm paper with one teal accent, and pill buttons, but has its own layout. The header, the hero and the approach section are dark bands in both themes, the work sits on paper that follows the theme, and contact is a teal band. Inside a dark band the colour tokens take the band's values, so components need no extra rules. The CV is the document; this site is the showcase.
  • Self-hosted fonts through next/font: Fraunces for headings, Geist for text and Geist Mono for labels and dates. The page makes no requests to Google Fonts.

Two skins, one codebase

The >_ button in the header switches between the standard style and a Terminal style. Both render the same markup and the same content.

  • All Terminal rules live in one stylesheet, src/design/skin-terminal.css, scoped under html.skin-terminal. The scope is wrapped in :where(), which adds no specificity. The skin's rules therefore compete with the component styles on equal terms and win only because they load last. All stylesheets are imported in one place, in a fixed order.
  • A small inline script in the page <head> applies the saved theme and skin before the first paint, so a returning visitor never sees the other look flash. It also reads ?skin= and ?theme= from the URL, so a link like ?skin=terminal&theme=dark opens in exactly that look, for that visit only.
  • The Terminal font, JetBrains Mono, is not preloaded. The browser downloads it only when the Terminal style is on.
  • The ⌘K command palette is one component in both skins. Its commands are plain data with a title for the standard style and a shell alias for the Terminal style, where it becomes a prompt: cd work, cat cv, theme dark, help.

Accessibility

  • Section links move focus to the section's heading, keep a shareable fragment URL, and work with Back. With reduced motion turned on, they jump instead of scrolling.
  • The mobile menu closes with Escape and returns focus to the Menu button. While the menu is shut, it is out of the tab order.
  • The architecture diagram's parts are native toggle buttons with aria-pressed, and its caption is announced politely when it changes. Without JavaScript the first part and its caption still show. The palette uses a native modal <dialog>, which keeps focus inside and makes the page behind it inert, with an ARIA combobox for the list.
  • Motion is small: a dot that runs down the diagram's connector, the testing bars growing once when they scroll into view, and hover states. With reduced motion turned on, all of it stops and the bars show at full length.
  • Every build runs an automated accessibility scan (axe, WCAG 2.1 AA) on each page in both skins and both themes, and on the open palette. A violation fails the checks and stops the publish.

Tests and delivery

  • Unit tests with Vitest and Testing Library cover navigation and focus, the theme and skin controls, the command palette and the command search, the architecture diagram, and the case studies' contents list.
  • End-to-end tests with Playwright run against the exported site, the same files the host serves. They check each page's title, description and Open Graph tags, reading without JavaScript, the saved style applying before paint, and zero console errors. They also check that nothing scrolls sideways at 320px and 801px, and that header items never overlap or clip from 320px to 1280px.
  • GitHub Actions runs lint, type checking, formatting, the unit tests, the build and the end-to-end tests on every pull request. Merging to main runs the same checks, then publishes the exported site to the branch the host serves.
  • Changes land as small, reviewed pull requests, each with its checks and a note on what was verified.

Performance

Lighthouse, mobile profile, median of three runs on the home page, measured on 30 September 2026:

MetricResult
Performance96
Accessibility100
Best practices100
SEO100
First contentful paint1.4 s
Largest contentful paint2.8 s
Total blocking time39 ms
Cumulative layout shift0
Total transfer311 kB
JavaScript (gzipped)144 kB
CSS (gzipped)6.3 kB

Most of the JavaScript is the Next.js and React runtime, which loads after the page has already painted. The hero portrait is a 16 kB WebP.