Blog Devlog
Engineering This Website
A portfolio site is a strange project: the content barely matters at launch, but the build quality is the portfolio. So I treated this site like an embedded project — a hard resource budget, no dependencies I couldn’t justify, and instrumentation from day one. Here’s how it’s put together.
Static-first with Astro 5
The site is Astro 5 with output: 'static', deployed to Cloudflare Pages on every push. There is no client-side framework — no React, no hydration, no islands with runtime. Every page ships as HTML and CSS, plus one hand-written script bundle that currently sits at 6.1 kB gzipped.
That constraint sounds austere, but it forced better decisions. Astro’s content collections give me Zod-validated frontmatter (this post’s schema would fail the build if date were a string), and the whole site builds in about 4 seconds, which keeps the edit-preview loop tight.
A motion engine with zero dependencies
I wanted the page to feel alive without paying for an animation library. The motion layer is three pieces, all vanilla:
Scroll reveals use one shared IntersectionObserver instead of per-element listeners. Elements opt in with a data attribute; the observer flips a class and immediately unobserves:
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
if (!e.isIntersecting) continue;
e.target.classList.add('is-visible');
io.unobserve(e.target); // reveal once, then stop paying for it
}
}, { threshold: 0.15, rootMargin: '0px 0px -10% 0px' });
Magnetic buttons track the cursor within a 100 px radius and translate toward it at 0.25× strength, lerped in a requestAnimationFrame loop that self-suspends when the delta drops below 0.1 px. The first version ran the rAF loop unconditionally and cost 3–4% CPU at idle. Lesson learned: animation loops need a sleep state, always.
The signal field is a canvas of drifting nodes with proximity-based connecting lines. Naive pairwise distance checks are O(n²), which was fine at 60 nodes and unacceptable at 200, so nodes live in a spatial hash grid and only check neighboring cells. All motion respects prefers-reduced-motion — the entire engine no-ops behind one media query check.
Generative circuit art, seeded per page
Each page header has an SVG circuit-trace pattern that is unique to that page but stable across visits. The trick is seeding a PRNG with a hash of the page slug at build time:
function mulberry32(seed) {
return () => {
seed |= 0; seed = (seed + 0x6D2B79F5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
The generator walks a grid laying down 45°-constrained traces, pads, and vias — the same aesthetic rules a PCB autorouter follows. Because it runs at build time, the output is inline SVG in static HTML: zero runtime cost.
CSP without unsafe-inline
The strict part: script-src 'self' with no unsafe-inline, set via Cloudflare Pages _headers. Astro likes to inline small scripts, so I forced them into external files with build: { inlineStylesheets: 'never' } plus is:inline audits. Getting the last inline event handler out took longer than writing the motion engine.
The budget
| Metric | Target | Current |
|---|---|---|
| Lighthouse (all four) | 100 | 100 / 100 / 100 / 100 |
| JS shipped (gzip) | < 10 kB | 6.1 kB |
| LCP (throttled 4G) | < 1.5 s | 0.9 s |
The scores are the easy part on a static site; keeping them while adding motion and generative art was the actual project. The rule that made it work: anything that can happen at build time, must.
Keep reading
More from the bench
Taming Stepper Motor Resonance with TMC2209s
Why my robotic arm sang like a violin at 400 mm/min, and how StealthChop, microstepping and a firmware notch filter fixed it.
Read postServing a Realtime Web UI from an ESP32
WebSockets over LittleFS with gzip-baked assets: how Helios pushes LED state to four browsers at once with ~11 ms median latency on a $4 microcontroller.
Read post