Building this site — the bridge
Architecture and design choices, warts and all. With a stowaway in the vents.
Quick tour of how this thing is glued together, in case any of it is useful to you, or in case you're just curious why the homepage looks like a ship's bridge.
Stack
Nothing exotic here:
- Next.js 15 App Router, React 19, RSC wherever it made sense
- Tailwind for layout, plain CSS for anything with personality (scanlines, the glitch clone trick, the canvas hero)
- MDX posts via
next-mdx-remote/rscandgray-matter - JetBrains Mono through
next/font/google, self-hosted so there's no FOUT flash on load
The one real decision was MDX vs a CMS. MDX wins because I would rather write posts in my editor than in someone else's web form, and because my posts are short enough that I don't need a preview environment.
The hero
The hero pane is a real <canvas>, not CSS trickery. It runs at 18fps on purpose. 60 felt frantic, 30 still felt busy, 18 is the sweet spot where your eye stops paying attention to it — a slow drift you only notice if you go looking. The columns reset on random intervals instead of synced frames, which is what keeps it from looking like a cheap screensaver. I kept calling the whole thing "warp drive" while building it and eventually the name stuck.
Two small things hook into it directly:
- the pulse dot in the status bar cycles forward, paused, reversed (I spent an embarrassing amount of time tuning the reversed motion so it didn't look broken),
- typing
stormanywhere on the page runs five seconds of chaos at 2× speed. The toast that pops up says "Boom. Weapons free." — there are a few of these nameless nods sprinkled around, mostly for my own amusement.
The easter eggs
I wanted these to live in a single directory so I could rip the whole lot out if it ever got embarrassing. components/easter-eggs/ has:
- a provider that owns the "what has the user discovered" state and a toast host,
- a
config.tslisting every egg and every command palette entry, - one component per overlay: help, palette, quake terminal, screensaver, the red-alert thing,
- a
hooks.tswithuseKonami,useTypedWords,useIdle,useVimEscape,useDevTools,useGamepad.
One global keydown listener dispatches to whichever egg cares. The command palette (⌘K) is the canonical way to run any feature by name, which conveniently saves me from having to remember my own keyboard shortcuts.
The second wave
The more interesting ones are the invisible ones — stuff you'd only stumble on if you were already poking at the internals:
- A
<!-- -->banner at the top of the rendered HTML that tells you which word to type next, - A DevTools handshake: open the console and a cyan ASCII bridge prints, plus
window.brunois wired withhelp(),eggs(),storm(),warp(dir), and friends, - An "adaptive warp" path that reads
navigator.connection(andprefers-reduced-motion) and drops the rain to 2–6fps if the user has signaled they'd rather not, - A scroll-driven CSS progress bar on blog posts, driven entirely by
animation-timeline: scroll()— no scroll listeners, - Cross-tab sync via
BroadcastChannel: open the site in two tabs and your discovered eggs + warp direction stay in lockstep, - Gamepad support: D-pad cycles panes, A opens the palette, Y triggers storm,
- A draggable gutter between the two columns — your layout gets serialized into
#col=<ratio>, so sharing the URL shares the view.
The rain canvas also pauses itself via IntersectionObserver + Page Visibility, so scrolled-out-of-view and background tabs don't burn a single rAF.
Blog infrastructure
Deliberately boring. lib/posts.ts reads content/posts/*.mdx at request time (or at build, via generateStaticParams). Posts render on the server, so the reading experience ships zero client JS. If you've got JS disabled the posts still work; only the homepage loses the fun parts, which feels like the right trade.
That's most of it. Code's on GitHub. If you spot something broken or have a better idea for any of the above, my email is in the contact pane.