Colophon
Decisions this site is built on, each with the file it is implemented in. The reasoning lives in the source comments; this is the index to them.
An animated cover stays a raw <img>
src/components/shared/ProjectCover.tsx
One of the project covers is an animated WebP with a GIF fallback. It is rendered as an <img> inside a <picture> rather than through next/image, because next/image cannot express a fallback chain — not because a re-encode would freeze the animation, which measurement showed it does not.
Four cover surfaces, one component
src/components/shared/ProjectCover.tsx
The home grid, the featured build, the projects index and the project detail hero all render a cover, and each used to carry its own copy of the animated-or-still branch. One of the four had drifted. They now call one component with the cover, the fallback, the sizes and the zoom class.
The feed is fully static
src/app/blog/rss.xml/route.ts
The RSS route declares `dynamic = "force-static"` and reads the post list at build time. There is no runtime dependency in it, so a subscriber's request never touches the server's data path, and each item carries the whole post body rather than a summary.
The sitemap has no build-time lastModified
src/app/sitemap.ts
The static routes deliberately carry no `lastModified`. These pages have no honest per-URL timestamp, and the build time is not a modification time — a crawler told otherwise would re-crawl the whole site on every deploy. Only the posts, which have a real date, carry one.
One content entry point
src/lib/content.ts
Every content module is re-exported from a single barrel, and nothing outside `src/content/` imports the modules directly. "What content exists" is a question answered by reading one file rather than by grepping the tree.
Motion timing lives in one module
src/lib/motion.ts
Eases and durations are named in one place and imported by every animating component, so a value cannot be changed in one surface and missed in another. The reduced-motion branch comes from the same module, through `gsap.matchMedia()`.
The content security policy is written out directive by directive
next.config.ts
The CSP names what each directive is for and why it is needed, including the one place `unsafe-inline` is required for styles: React renders inline `style` attributes for the ported widgets. `img-src` lists no third-party origin, because no icon is fetched from one any more.
The test suite builds itself and refuses to adopt a running server
playwright.config.ts
`pnpm test:e2e` chains the production build, so the suite works on a fresh clone with no manual step, and the web server is never reused. Before, a stray dev server on the port was adopted silently and the suite reported a pass against the dev build.
Malformed post frontmatter fails the build
src/lib/posts.ts
A post with no frontmatter block, no `title`, or a `date` that is not a real ISO calendar date throws with the file named. Every one of those used to fail silently: the slug published itself as the heading, and a bad date sorted the index wrongly.
The theme wipe's glint removes itself
src/components/shell/ThemeToggle.tsx
The sword-glint element is removed on its own `animationend` rather than after a fixed delay, so its lifetime and the animation's lifetime cannot disagree. Reduced motion gets no glint at all: with the animation disabled the event would never fire and the element would stay in the document.