I built TIER EVERYTHING — a site where you drag anything (dishes, mobs, languages, cities) into tier rows from GOATED down to Cooked, and then argue about each item in its own thread. 80 boards, ~830 items, real photos, no account needed to vote.
Most of the interesting work wasn't building features. It was chasing bugs that didn't throw. Six of them, in rough order of how long they hid.
1. A link preview that lied about its own size
Every board page emitted:
<meta property="og:image" content="https://tierhq-api.example.workers.dev/image/abc.webp">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
The declared size was 1200×630. The actual file was a 512×288 WebP, served from a different origin.
Two separate lies in one tag. Facebook, LinkedIn and Discord won't render a WebP link preview, and several crawlers refuse a cross-origin image outright. So every link anyone shared rendered a blank card — and nothing in my test suite noticed, because the markup was syntactically valid. The tag was well-formed. It was just wrong.
The fix was to generate real cards: 80 JPEGs at exactly 1200×630, same-origin, built with Pillow — a blurred cover crop, a gradient scrim, and the board name in the board's own tier colour.
The lesson I keep re-learning: a test that asserts a tag exists is not a test that the tag is true.
So the new assertion reads the actual bytes of the file and compares them to the declared dimensions:
function imageSize(buf) {
// JPEG: walk to the SOF marker. PNG: read IHDR.
// Returns { w, h } or null.
}
const res = await fetch(ogImage);
const sz = imageSize(Buffer.from(await res.arrayBuffer()));
check('og:image size claim matches the actual file',
sz.w === claimW && sz.h === claimH);
That one assertion would have caught the bug on day one.
2. run_worker_first matches paths, not prefixes
I'm on Cloudflare Workers. My Worker rewrites <title>, description, canonical and OG tags per route, so those routes have to hit the Worker before the static asset handler:
[assets]
run_worker_first = [
"/sitemap.xml",
"/robots.txt",
"/view",
"/item",
"/u",
"/topic",
"/share/*", # <- a glob, and this is the whole point
]
/share/abc.jpg returned a 404. Because those list entries are exact paths, not prefixes — /share matches /share and nothing else. It needed to be /share/*.
The nasty part: it failed silently. The asset handler answered first, the Worker never ran, and the page still returned HTTP 200. No error, no log, no exception. The route simply didn't execute.
If you're debugging a Worker that appears not to run, check run_worker_first before you check your code.
3. A fallback that quietly became the thing it was replacing
Same Worker, same file. When a share card was missing I wanted to fall back to a generic one:
// Wrong
const fb = await env.ASSETS.fetch(new Request(OG_CARD)); // absolute URL
if (!fb.ok) return res; // returns the 404 we were trying to avoid
ASSETS.fetch with an absolute URL doesn't resolve against the incoming request's host and port, so it returns not ok — and my "graceful fallback" cheerfully returned the 404 it existed to prevent.
// Right — resolve relative to the incoming request
const fb = await env.ASSETS.fetch(
new Request(new URL('/assets/og.png', url), { method: 'GET' })
);
A fallback path that is itself broken is worse than no fallback, because it looks handled.
4. Headless Chrome reports prefers-reduced-motion: reduce
This is my favourite, because it's a bug in my testing, and it produced a false green.
I added an animation layer — a score that rolls, a ring that expands when you vote and contracts when you take it back. Every animation is gated:
function dxMotion() {
try { return !window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
catch (e) { return true; }
}
Then I ran my four browser fixtures. All green.
Except: headless Chrome reports prefers-reduced-motion: reduce. So dxMotion() returned false on every run, and not one line of the new code executed. My fixtures had proven the layer was inert. They had proven nothing about whether it worked.
I only caught it because I asked what the fixture was actually exercising. The fix is one call before navigating:
await send('Emulation.setEmulatedMedia', {
media: 'screen',
features: [{ name: 'prefers-reduced-motion', value: 'no-preference' }]
});
Then assert the motion is genuinely running, not merely present:
const running = document.getAnimations()
.filter(a => a.animationName?.startsWith('dx-')).length;
"The node exists" and "the animation is playing" are different claims. Only one of them is testable by checking the DOM.
There's a sibling trap: one of my assertions passed trivially. I wanted to prove the entrance stagger doesn't re-fire on re-render — but a single vote doesn't trigger a re-render, so the assertion was vacuously true. A green check that couldn't have failed is not evidence. I had to force a re-render (click a filter chip) before asserting.
5. Anonymous opinions, accountable content
I wanted voting to cost nothing. No signup, no email, just an opinion.
But "anonymous" and "unmoderated" aren't the same thing, and I didn't want to conflate them. So the site draws a line:
- Votes and ratings are opinions → anonymous. A browser-local voter id, plus IP rate limiting server-side.
- Content is content → still needs an account. Boards, comments, covers. Because content has an owner, and an owner gives me a cleanup path.
One deliberate exception: board likes stay behind the wall. It's a bare counter with no per-voter dedupe, so opening it would just be a score-inflation hole.
For comments I wanted anonymous posting without the usual disaster, so there are three guardrails:
- A per-voter, per-board flood cap (5/hour), with a coarse per-IP backstop.
- A 15-minute self-delete window, decided server-side from the stored voter id — never from what the page claims.
- An admin override, and cascade deletion of replies when a parent goes (breadth-first, with a node cap so a pathological thread can't blow the request budget).
The subtle bug here was mine again: my fixture asserted the POST returned 200. It returns 201. So five successful posts were all counted as rejections, and the flood cap looked like it was firing on the first comment.
6. content-visibility vs. the DOM I thought I had
A directory page with 80 boards rendered all 80 cards. Fine for 80. Not fine for the direction it was heading.
I didn't want to write a virtualiser — the complexity rarely pays for itself at this scale. Instead:
.card { content-visibility: auto; contain-intrinsic-size: auto 250px; }
Layout and paint get skipped for off-screen cards while the DOM stays complete, so search, find-in-page and my fixtures all still work.
What I'd tell you
The pattern across all six: the failure mode was never an exception. It was a well-formed tag pointing at the wrong file, a 200 from a route that didn't run, a green test that exercised nothing, a fallback that returned the error it was meant to hide.
If you take one thing: when something "works", ask what your test would have looked like if it had been broken. If the answer is "identical", you don't have a test.
I'd genuinely like to know where the tiers are wrong. tier.fcthq.com — no account needed, just drag something and tell me my taste is bad.
Top comments (2)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.