Skip to content
Back to Home

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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()`.

  7. 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.

  8. 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.

  9. 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.

  10. 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.