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

MetricTargetCurrent
Lighthouse (all four)100100 / 100 / 100 / 100
JS shipped (gzip)< 10 kB6.1 kB
LCP (throttled 4G)< 1.5 s0.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.