Skip to content
Development

A repeatable a11y check for imperfectsystems.com

By Victor Da Luz
accessibilitycicloudflaredev-logsite

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

Development

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.

Read