About.
Full-stack web developer — also games, LoRaWAN fleets, and data at scale. Fast because I read the platform; readable because someone has to maintain it, and I only ship what I can explain to myself. Work page has the long version; this is the short one.
Code is for the next reader.
In three months I'll read this code as a stranger. Header comments, honest names, and notes about why are the breadcrumbs I leave for that stranger.
Files stay small enough to hold in one head. When a file gets fat, split it — don't compress it.
/* * file.js — one-line purpose. * Dependencies: ... * Invariants: ... * Non-goals: ... */
Tools I reach for, and when.
-
Node
Default for anything web-shaped on the server side. The stdlib is most of a framework — if you bother to read it.
→ servers · tooling · scripts -
Web
The browser is the system. First paint, prefetch, View Transitions, view-source — most of what you need is already in the platform once you read the docs.
→ first paint · prefetch · view transitions -
Zig
For write paths that have to be deterministic. No hidden allocator, no surprises — and comptime makes hand-written serialization worth the typing.
→ hot loops · parsers · binary formats -
Unity / C#
For games. Sixteen milliseconds a frame, and the engine draws whether your code is ready or not — which is the best forcing function for "fast enough" I've ever worked under.
→ gameplay · tooling · prototypes
Things I tried and dropped.
Eight decisions on this site that lost. The version that shipped is better because of the version that didn't.
-
Separate tags per mode.
First sketch had
<terminal-card>and<swiss-card>as distinct elements; the router would swap tags on mode change. Killed it the first time I switched modes mid-scroll — swapping tags throws out the DOM node, so focus, scroll position, and any in-flight transition reset with it. Now there's one<project-card>shell and a per-mode renderer registry; the element survives the swap, only its innards change. -
One renderer with
if (mode === ...)branches.Considered a single mode-aware function per shell. Dropped it because adding a third mode would mean editing every renderer in the codebase — modes coupled through every shell. The registry inverts that: each mode owns its own file and registers itself. (Adding Blueprint mode later is a new file, not a sweep through old ones.)
-
Shadow DOM.
The obvious choice for custom elements. Pulled it out anyway — the page told one story and devtools told another, and the thesis of this site is that the source is the product. View-source has to match what's actually rendering. Renderers now write straight to
host.innerHTML. -
A Proxy-based observable store.
Started sketching one for cross-component state and stopped about thirty lines in. The actual need was "late subscribers should see the current value" — the event bus solves that in forty lines by replaying the last published value to new subscribers in a microtask. (The fancier option was solving a problem I didn't have.)
-
The reaction-diffusion sim on the CPU.
First version stepped Gray-Scott in plain JS over a Canvas2D buffer — two float arrays, a double loop every frame. Correct, and far too slow: a few hundred cells in, the frame budget was gone and the pattern crawled. A static gradient was the fallback sketch — honest, but a gradient is a texture, and the page is supposed to be the system, not paint a picture of one. Moved the update onto the GPU: state in an RGBA texture, the step as a fragment shader, ping-pong framebuffers to advance time. The CPU does nothing per frame now but ask for the next draw.
-
Mode from
prefers-color-scheme.The first cut had no toggle — read
matchMediaand pick terminal or swiss off the OS theme. It collapsed two unrelated axes: the modes aren't dark-vs-light, they're a different reading of the page, and the OS has no opinion about that. A?mode=URL param came next — shareable, but it doesn't survive a click to the next page and clutters every link. Now an explicit choice persists tolocalStorage, and an inline<head>script replays it before first paint so the switch never flashes. -
Project content as a JSON manifest.
Card data started as one
content.json— title, summary, tags, body as an HTML string. Two problems: editing prose inside JSON strings is miserable, and a body of pre-baked HTML is exactly the opaque-source thing this site argues against. Inline-HTML-per-card had the same second problem. Settled on one markdown file per project with frontmatter, and a hand-written parser for the small markdown subset actually used — ~90 lines, not CommonMark, and a card's source reads as the card. -
Prefetch every link on load.
The easy version walked every in-page link on load and injected
<link rel="prefetch">for each. It works, and it's wasteful — spending the visitor's bandwidth on pages they'll never open, on a site whose whole pitch is that every byte is deliberate. An IntersectionObserver that prefetched links as they scrolled into view was better but still speculative. Hover won: once the cursor lands on a link the intent is real — one<link>, deduped per pathname, almost always on disk by the click.
If you care how it's built, not just that it ships — linh@lelinh.dev, github.com/Linh35, or LinkedIn. Or see the work page for the principles in full.