Skip to content

Accessibility scanning

Alongside dead-link detection, Overwatch can run an accessibility scan over the pages it crawls, powered by axe-core. It is opt-in per site and adds a scored accessibility report next to the dead-link report.

Pages are fully rendered in a headless browser before axe runs, so the scan sees what a visitor sees — client-side rendering, injected widgets, and computed styles included — and the complete axe ruleset applies (including color-contrast on real computed colors).

Set a11yEnabled when registering a site (POST /v1/sites) or toggle it later (PATCH /v1/sites/{id}):

Terminal window
curl -X PATCH https://api.overwatch.weareheavy.dev/v1/sites/<site-id> \
-H "x-api-key: wah_…" -H "content-type: application/json" \
-d '{"a11yEnabled": true}'

The scan runs on its own clock, independent of the crawl: a11yCadenceMinutes defaults to monthly (43200), sized to the rendering allowance — see Quotas & usage. Each run renders the crawl’s persisted page set; the first run simply waits for a crawl to populate it. You can also trigger a scan on demand — no re-crawl — via POST /v1/sites/{id}/a11y-scan, and a manual full crawl chains a scan by default (chainA11y: false skips it).

Each accessibility run records a score plus per-impact counts (visible on RunRow as a11y_score, a11y_criticala11y_minor, a11y_pages_scanned, a11y_issue_count, and a11y_resolved — how many violations from the previous scan are now gone). Runs that didn’t scan have a11y_score: null.

GET /v1/sites/{id}/a11y-report parallels the dead-link report: the latest run, the list of findings, a summary, and the site’s suppressions and manual review states.

Each finding is a (page, rule) pair:

  • rule — the axe-core rule id (e.g. image-alt, color-contrast), with help/help_url explaining it, and top-level maps descriptions and w3cUrls linking each rule to its one-line description and W3C Understanding page.
  • impactcritical, serious, moderate, or minor.
  • typeviolation is a confirmed failure; incomplete means axe couldn’t decide and a human should review.
  • count and nodes — how many elements matched, with capped example nodes (selector + snippet) to locate them.
  • standards, sc, principle — the WCAG level (A/AA/AAA), whether the rule falls under EN 301 549 (the EU Accessibility Act’s standard) and US Section 508, its success criterion, and its POUR principle.

The summary rolls the findings up several ways: severity counts, a byPrinciple POUR rollup, compliance tallies for EN 301 549 and Section 508, and resolvedSinceLastScan. gainByRule ranks rules by the score points fixing each would recover, and byPage lists every affected page worst-first with its own score.

axe-core doesn’t produce a score, so Overwatch synthesizes a 0–100 number per run: confirmed violations are weighted by impact and damped by the number of pages scanned — a large, mostly clean site scores high; a small site with serious issues scores low. A run with any confirmed violation can never score 100, so “100” always means “no confirmed violations”. Treat it as a trend indicator between runs, not a compliance certificate.

Separately, summary.verdict is a pass/fail conformance judgment at the AA target level: any confirmed violation of a level-A or -AA success criterion breaks it (AAA and best-practice rules don’t), and a risk band scales with severity × breadth. Score and verdict move independently — a high score with a lone level-A violation still fails.

coverage classifies every WCAG success criterion for the run: fail (an active violation maps to it), pass (auto-testable and clean), or manual (no automated rule can test it — a human must). It’s grouped principle → guideline → criteria, ready to render as a matrix, with per-SC page counts, failing rules, and W3C links.

Automation can’t test every criterion (e.g. 2.1.4 Character Key Shortcuts). PUT /v1/sites/{id}/a11y-manual-checks records a manual state per criterion — verified, not_applicable, or failed, each with a note — and DELETE …/a11y-manual-checks/{sc} clears it back to “needs review”. Manual states are a documentation/audit layer only: they surface in the report’s manualChecks and overlay the coverage matrix, but never affect the score or verdict.

Not every finding is actionable — a third-party widget, an intentional design decision. Suppressions let an editor ignore a finding without losing the audit trail, via POST /v1/sites/{id}/a11y-suppressions.

Scope is set by which fields you provide:

Fields Suppresses
rule The rule, site-wide
rule + selector That element, on every page
rule + selector + pageUrl That one occurrence

Suppressions apply retroactively and reversibly: they are stored per site, and the report applies them at read time — suppressed findings drop out of issues and the score is recomputed from what remains. Deleting a suppression (DELETE /v1/sites/{id}/a11y-suppressions/{suppressionId}) brings the findings straight back. The site’s excludePaths globs are applied the same way, so excluding a section removes its findings immediately.

The report also returns each suppression with a matchedCount — how many of the latest run’s findings it currently hides — so a UI can show an “Ignored” section and let editors un-ignore with confidence.