Colophon

How this site is built

Every page here argues that a design should be judged by what it measures, so it would be strange not to say what this one is made of. This is the stack, the decisions behind it, and the parts I got wrong.

Written by hand

There is no site builder or template under this. Every component is a file in a repository, and the layout is Tailwind utilities with a set of CSS custom properties on top for colour, glass and depth. The two themes are the same variables with different values, which is why the light mode is a genuinely different palette rather than an inverted dark one.

I use language models heavily while building, and I would rather say that than pretend otherwise. What I have found is that the value is not in generating code quickly. It is in having a specification, a test, and enough understanding to tell whether what came back is right. Several of the fixes listed below were things that looked finished and were not.

Images

The photographs come off the camera at up to 16320 by 12240 pixels. None of that reaches your browser. A script walks the originals folder, which is outside the deployed directory entirely, and writes two derivatives of each image: an 800 pixel tile for grids and a 2560 pixel version for the lightbox. A manifest of intrinsic dimensions is written alongside them so tiles can reserve the right box and the page does not jump while images load.

It runs on System.Drawing through PowerShell rather than an image library from npm, because it is a build-time tool on a Windows machine and adding a dependency tree to crop photographs seemed like the wrong trade. It also applies EXIF orientation and then strips the tag, which I added after a batch of photos appeared rotated in the grid and upright in the lightbox.

Things that were wrong

A global rule capped every paragraph on the site at 72 characters. It was meant for prose and it applied to captions, spec values and button labels too, and because it targeted the text rather than the container, no layout class could see it. It cost three separate debugging sessions where the copy stopped short of its column and looked like a broken grid. It is opt-in now.

Every missing file used to return a page instead of a 404. The single-page routing rule sent anything unmatched to the app shell, which is correct for routes and wrong for images, so a broken photograph returned a valid HTML document with a 200 and nothing appeared in any log. The rule now only matches paths without a file extension.

Social previews were wrong for a year without anyone noticing, because the tags are set after React mounts and the crawlers that read them do not run JavaScript. The same blind spot was hiding something larger: every route served an empty root element, so anything that did not execute JavaScript saw zero characters of text. Each route is now rendered to real HTML at build time — the page itself, not only its title — from the same component tree the browser uses, so the two cannot drift apart.

Weight

The icon font was the single largest asset on the site at 5.2 MB, because the full variable font ships unless you ask for a subset. Requesting only the icons actually used took it to 67 KB, and it is served from this domain rather than Google's. Serving derivatives rather than originals took the deploy from 639 MB to 147 MB: 872 MB of originals become 113 MB of tiles and lightbox copies. That number was 284 MB until recently, because the lightbox tier had been raised to 4000 pixels and the photography alone came to 176 MB; 2560 covers a full-screen lightbox on a 4K panel and gave half of it back. The two videos were another 50 MB of it, one of them encoded at 1440p50 for a player that is about 1280 pixels wide on a laptop. Re-encoded to 720p they are 25 MB, and the frames they are made of are the same ones as before.

The 3D board and the signal-chain diagrams are the two heaviest things here, so neither loads until you visit the page that uses it, and the board waits for a click before it starts a WebGL context at all. A portfolio that takes ten seconds to load has already made its argument about engineering judgement.

Still on the list

The repository is far larger than it should be, because full-resolution originals are tracked in git history. Rate limiting is written and deployed for the classifier endpoint but inert until I set the credentials.

FrameworkReact 18, react-router 7
BuildVite 8 (rolldown)
StylingTailwind 3 + hand-written CSS variables
MotionCSS animations, no motion library
3Dthree.js via react-three-fiber
DiagramsReact Flow
TypeExo 2, Playfair Display, Material Symbols — self-hosted
HostingVercel, with serverless functions in /api
ImagesPowerShell + System.Drawing, no npm dependency
Testsnode:test, on the serverless code and the build output

Source

The whole site is public, commit history and all.

github.com/NiccTM/Website

Elsewhere

The voltage reference is the piece of work I would point at first. About covers the rest.

Built in Kelowna, British Columbia.