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.

№ 01

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: ...
 */
Stack

Tools I reach for, and when.

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.

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

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

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

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

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

  6. Mode from prefers-color-scheme.

    The first cut had no toggle — read matchMedia and 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 to localStorage, and an inline <head> script replays it before first paint so the switch never flashes.

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

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