A repeatable a11y check for imperfectsystems.com
While working an earlier UI change, I hit a gap: this site’s done-criteria for UI changes says to run npm run a11y before calling something done. That script didn’t exist on this site. I grepped for any a11y config, tool, or CI step and came up empty. So I ran npx @axe-core/cli against a local astro preview server as a one-off, got zero violations, and moved on. But a one-off isn’t a check, it’s a screenshot of a moment. The follow-up: make it a real, repeatable script.
I didn’t have to design this from scratch. vdaluz.com, in the same workspace, already runs a local a11y check the right way: pa11y-ci (the axe runner, WCAG2AA) driven by start-server-and-test, pointed at a wrangler dev server running the actual built Cloudflare Worker, not astro preview. That distinction matters, astro preview is Node’s dev server, and it doesn’t reflect the Worker environment the site actually ships in.
"a11y:server": "wrangler dev --config dist/server/wrangler.json --port 8787",
"a11y:run": "pa11y-ci",
"a11y:ci": "start-server-and-test a11y:server http://localhost:8787 a11y:run",
"a11y": "npm run build && npm run a11y:ci"
Porting the config was the easy part. The thing I wasn’t sure would carry over cleanly: this site has real Cloudflare bindings vdaluz.com’s config doesn’t, a KV namespace and a password secret, both wired up for a gated playtest route. I expected I’d need extra flags or a local bindings override to get wrangler dev to boot cleanly.
I didn’t. wrangler dev --config dist/server/wrangler.json just resolved everything to local, in-memory equivalents on its own:
env.SESSION KV Namespace local
env.GAME_FILES KV Namespace local
env.PLAYTEST_PASSWORD Environment Variable local
No config, no flags, it read dist/server/.dev.vars for the secret and spun up in-memory KV for both namespaces automatically. The server came up, served all six of my target URLs at 200, and pa11y-ci reported 6/6 passed with 0 errors.
The one page category I left out on purpose: the password-gated playtest routes. They’re gated and marked noindex, so they’re not really part of the public surface this check is meant to protect, and testing them would mean baking a fake auth cookie into the pa11y config for no real benefit.
Small task, but it closes a loop: future UI-change issues on this repo now have the same repeatable local check vdaluz.com has had for a while, instead of a workaround I’d have had to reinvent every time.
Related reading
Two triggers, one KV id: why preview builds were dark on imperfectsystems.com
Chasing down why non-production branch builds never ran on Cloudflare Workers Builds - a KV namespace nobody asked for, and a trigger model I'd misread.
Auto-merge assumed CI existed. It didn't.
The check that shows up on merged PRs is Cloudflare reporting a build that already ran, not a pre-merge gate - so the gate became a synchronous local build instead.
Testing a deploy gate accidentally deployed the thing I was testing
Adding astro check and Prettier before every deploy, a type error the sibling site had already solved, and a manual build trigger with no concept of a dry run.