Skip to content
All posts

How this site is built

The architecture behind this portfolio — one shared WebGL canvas, MDX content validated at build time, and a deliberate rule that adding anything is one file.

· updated August 27, 2026· 2 min read
  • next.js
  • architecture
  • three.js

The goal for this site was not a design. It was a property: adding something should be one file and one commit. Everything else follows from that.

Content is data, not code

Projects, posts, and lab notes are .mdx files under content/. Frontmatter is parsed by gray-matter and validated with Zod before anything renders:

src/lib/content/schema.ts
export const postSchema = base.extend({
  featured: z.boolean().default(false),
});

A missing summary or a mistyped tags key fails the build with the offending file name. That's the point — a broken page in production is much worse than a red build.

One canvas, many scenes

The naive approach puts a <Canvas> inside each 3D component. Browsers cap you at roughly sixteen WebGL contexts, and every route change tears down and rebuilds the renderer.

Instead there's a single canvas fixed behind the whole app. Components claim a rectangle of it with <SceneView>:

<SceneView name="hero" />

The canvas doesn't mount until at least one SceneView registers, so pages with no 3D create no WebGL context at all.

3D never breaks the site

Three gates sit in front of every scene:

CheckBehaviour when it fails
prefers-reduced-motionPoster image, no WebGL
No WebGL2 / low memory / save-dataPoster image, no WebGL
Scene throws at runtimeError boundary swaps in the poster

The poster is an inline SVG component reading the same design tokens the live scene does, so it stays in sync with light/dark mode with no JavaScript at all. Worst case, the visitor sees a still image and the page is otherwise untouched.

What this buys

Six months from now, a new experiment is: add content/lab/thing.mdx, add a scene folder, add one line to the registry. No restructuring.