Accessibility — the WCAG build check & daily sweep

StaticOwl checks your pages for common accessibility problems in two places: on every build, over the HTML it just compiled, and on a daily sweep, over the HTML your live site is actually serving. Both produce the same kind of report — a list of findings mapped to WCAG criteria, with a count of errors, warnings, and info items — so you can fix issues before publish and keep an eye on them after.

This is a static HTML check — it reads the markup, not a running browser. It flags things that are unambiguously wrong in the HTML itself (a missing alt, an empty <title>, an unlabeled input). It deliberately does not attempt CSS-derived checks like real color contrast computation or focus order.


What it checks

Each rule maps to a WCAG 2.1 criterion and a severity:

Rule WCAG Severity Flags
img-alt 1.1.1 error An <img> with no alt attribute.
page-title 2.4.2 error A missing or empty <title>.
html-lang 3.1.1 error <html> with no lang.
duplicate-id 4.1.1 error The same id used more than once.
link-text-empty 2.4.4 error A link with no text.
button-name 4.1.2 error A button with no accessible name.
input-label 3.3.2 error A form control with no associated label.
heading-order 1.3.1 warning A heading level skipped (e.g. h2 → h4).
link-text-vague 2.4.4 warning Vague link text like "click here" or "read more".
inline-contrast 1.4.3 warning Low-contrast colors set in inline styles.
skip-link 2.4.1 info No "skip to content" link found.

An error is something to fix; a warning is worth a look; info is a suggestion. Each finding carries a short, copy-pasteable fix and a snippet of the surrounding markup so you can find it.


The build check

On every build, the check runs over each compiled HTML page. The report is stored in your site's history (so the dashboard can show it) and emitted as /accessibility.json next to your pages:

curl https://<your-site>/accessibility.json

Its shape:

{
  "scannedAt": "2026-10-16T12:00:00.000Z",
  "pagesScanned": 42,
  "pages": [
    { "path": "/about/", "errors": 1, "warnings": 0,
      "findings": [ { "rule": "img-alt", "severity": "error", "wcag": "1.1.1",
                      "message": "Image is missing an alt attribute.",
                      "fix": "Add alt=\"…\" …", "context": "<img src=\"/logo.png\">" } ] }
  ],
  "byRule": [ { "rule": "img-alt", "severity": "error", "wcag": "1.1.1", "occurrences": 3 } ],
  "totals": { "errors": 3, "warnings": 5, "info": 1 }
}

The daily sweep

A build only re-checks a site that rebuilds. The daily sweep covers the gap: it re-audits your live site on a cadence and checks the HTML your visitors actually receive, not the in-build artifacts.

Once a day it visits each published site that has a public URL (sandboxes and unpublished sites are skipped), reads its sitemap.xml to find pages (falling back to the homepage if there's no sitemap), fetches up to 50 pages, scans each, and stores a report on the site. A swept report is the same shape as a build report.

The sweep is on by default. To turn it off, set the environment variable STATICOWL_A11Y_SWEEP=off on the server.

Check a site on demand

You don't have to wait for the daily run. Ask for a live scan right now:

curl "https://app.staticowl.com/api/sites/<siteId>/a11y-health?check=1" \
  -H "Authorization: Bearer $STATICOWL_API_KEY"

With ?check=1 it scans the live pages immediately and returns a health summary (pages scanned, error/warning totals, and the worst pages). Without it, you get the most recent cached or stored result. If the site has no public URL yet, the response says so — publish it first, then the sweep can reach it.


Reports, history, and regressions

Three admin endpoints read back the stored reports. They need a write role (admin, editor, or publisher) on the site.

Route MCP tool Returns
GET /api/a11y/latest a11y_latest The most recent report (build or sweep), or null.
GET /api/a11y/history?limit=N a11y_history Past reports, newest first, to chart issues over time.
GET /api/a11y/regression a11y_regression A diff of the two most recent scans.

The regression diff is the one to watch in a review or an ops gate. It returns the current and previous totals and, most usefully, introduced — how many errors and warnings are new since the last scan:

{
  "current":  { "errors": 4, "warnings": 6, "info": 1 },
  "previous": { "errors": 3, "warnings": 6, "info": 1 },
  "introduced": { "errors": 1, "warnings": 0 }
}

introduced is 0/0 when there's no previous report to compare against. Gate your process on "no new errors" by refusing to ship when introduced.errors > 0.


See also

Related docs

Resolved at build time via {% similar %} — cosine similarity over embeddings, not tag overlap. Zero arguments.

Performance budget — catch heavy pages at build time
73% match
Publishing & environments
69% match
Compliance — prove your posture from live data
68% match