Rob Wiscount Back to the site
Colophon

How this site works.

No framework, no build step, no bundler for the page itself. It's plain HTML, CSS and JavaScript on GitHub Pages, plus one small Cloudflare Worker for the live bits. Here's what's under the hood, for the curious.

The stack

Boring tech, on purpose.

Every page is a static file. The JavaScript is ES5-style, one file per feature, each wrapped in its own scope. They talk through a tiny window.RW API (toast, confetti, terminal commands, an egg() event), so a feature can be added or deleted without touching the others.

index.htmlThe whole page, hand-written and semantic, with JSON-LD for search engines and an llms.txt for AI agents.~860 lines
css/site.cssOne stylesheet built on design tokens (--blue, --surface…). The terminal's theme command re-skins everything by swapping four of them.~16 KB gz
js/site.jsNav, reveals, the typed hero, the GitHub squares, LeBronify, the Fetch player, the terminal, confetti, and the RW API the others use.~16 KB gz
js/eggs.jsAchievements and the trophy case, terminal tricks, end credits, Mile A Day mode, Gameday scores, the console note.~11 KB gz
js/live.jsLive cursors, the dot-matrix visitor map, the guestbook and the Letterboxd card.~10 KB gz
js/dog.jsThe fetch game: a 3D dog from my game, a thrown logo, and physics.~8 KB gz
js/pool.jsTurns the About cards into a pool rack and breaks it.~9 KB gz
workers/live/A Cloudflare Worker and one SQLite-backed Durable Object: WebSockets, visit counts, the guestbook, a Letterboxd proxy.~420 lines
How it fits together

Static page, live edges.

The page itself is static. Each live feature is a layer on top that fails quietly: if an API is down, you get the static version (a counted-up streak, an empty map, a link to GitHub) instead of an error.

Your browser index.html · site.css · site.js eggs.js · live.js · dog.js · pool.js three.js + dog model load on demand GitHub Pages static files, cached at the edge HTML · CSS · JS · fonts 3D dogs · Fetch (WebAssembly) Cloudflare Worker + one Durable Object (SQLite) WebSocket: live cursors visits · guestbook · sources Letterboxd RSS → JSON (cached) Public APIs Mile A Day: my streak + stats GitHub contributions ESPN scoreboards (Gameday) world-atlas map outline GET wss + fetch fetch (CORS)
Live mode

One Durable Object, everyone's cursor.

Every visitor connects to the same Durable Object called "global". It uses Cloudflare's hibernation API, so an idle room costs nothing, and pings are answered without waking it up.

  • Cursors go up about 15 times a second as fractions of the page, so they land in the right spot on any screen size. The server clamps, size-limits (200 bytes) and rate-limits (30/s) every message before relaying it, and the client smooths each one between updates.
  • Location comes from Cloudflare's request.cf: city, region and country, with latitude and longitude rounded to a tenth of a degree. No IP is ever stored or sent to anyone.
  • Visits are counted once per browser per day. The browser makes up a random id, and the server stores only a hash of that id plus the date, so it's useless by tomorrow.
  • The map is world-atlas land shapes, rasterized at 3× onto a hidden canvas, then sampled into a grid of rounded squares, the same visual language as the GitHub squares.
  • The guestbook takes plain text only (no links), one note per browser per day. Every note waits in a queue until I approve it.
The 3D dogs

Straight out of my game.

Type dog and one of the five dogs from Fetch runs onto the page. They're the game's own rigged models, put through a small pipeline to make them web-sized:

# 5–6 MB each → 300–700 KB: 512px WebP textures, quantized vertices
gltf-transform optimize luna.glb assets/dogs/luna.glb \
  --texture-compress webp --texture-size 512 --compress quantize

# just the parts of three.js the dog needs, as one script (~145 KB gzipped)
esbuild entry.js --bundle --minify --format=iife --global-name=RWThree

A camera in CSS pixels

An orthographic camera maps the WebGL strip 1:1 to the page, so the 2D fetch physics (gravity, bounces, the throw arc) didn't change at all. Only the drawing did.

The game's walk cycle

The rig's own animation blends in with speed and plays as fast as the dog is really moving. A procedural tail wag and a head that follows the ball sit on top.

A mouth socket

The logo rides an empty object parented to the head bone, placed using each dog's mouth_grip from the game's data, so it stays in their teeth mid-leap.

Pool physics

The About cards break.

Type break (I shoot in an APA league, so this one's personal). The cards fly into a 15-ball rack on a canvas felt, and the physics is real: equal-mass elastic collisions with overlap correction, 14 sub-steps per frame so fast balls don't tunnel through each other, cushions that bounce with damping, rolling friction and pocket mouths. Each ball is shaded by hand and its stripe rolls with it. When it settles, every card glides back to exactly where it was.

Easter eggs

Every egg is an event.

Features call RW.egg('pool') and know nothing about achievements. eggs.js listens for rw:egg events, shows the Xbox-style popup and saves progress in localStorage. Finding all of them unlocks a phone wallpaper drawn on a canvas. Some of them:

Terminal — a little shell with a command parser and a way for one feature to take over the prompt. That's how vim traps you until you press Esc and type :wq.
rm -rf / — everything on screen falls with the Web Animations API, then plays in reverse as "git" restores it.
Mile A Day mode — a runner rides the scroll bar. My own streak card reads the real number from the Mile A Day API, in Eastern time.
Gameday — when a Philly team is playing, ESPN's scoreboard turns into a live score pill and the nav takes the team's color.
Tab away — the Page Visibility API swaps the title, and the favicon becomes a spinning canvas hexagon.
Console — open DevTools. There's a rob object waiting for you.
Performance

Heavy stuff waits its turn.

Load on demand

three.js and a dog model download only on the first whistle. The Fetch game (Godot, WebAssembly) loads only when you press Play. The map waits until it's near the screen.

No idle loops

Every canvas animation (dog, pool, confetti, map pulses) stops its requestAnimationFrame loop when nothing's happening. An untouched page isn't redrawing anything.

Reduced motion

With prefers-reduced-motion on, the physics, confetti, pulses and wave effects step aside, and every egg still unlocks.

Tests

Green before it ships.

With this much going on, a small change can quietly break something three sections away. Every pull request runs a GitHub Actions job: Playwright drives the real page in Chromium at desktop and phone sizes, with every outside API mocked. It checks for script errors, sideways scrolling, terminal commands, achievements, the pool break and the dog cleaning up after themselves, and the live features. A Lighthouse run keeps an eye on performance and accessibility.

Privacy

Curious, not creepy.

No cookies, no ad trackers, no analytics scripts. The Worker keeps visit counts by city, the site that linked you here (just the domain, never the full address), and tags I add to links I send out, like ?r=acme on an application, so I know which ones got opened. Then it tidies that tag out of your address bar. Everything else lives in your own browser's storage.

Built with

Claude Code, GitHub Pages, Cloudflare Workers, three.js, gltf-transform, Playwright, Font Awesome, and Saira, Inter and JetBrains Mono. Designed and built in South Jersey.

Back to the site Read the source