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 }
}
pageshas one entry per scanned page, with its findings and counts.byRuleaggregates occurrences across the whole site, so you can see which issue is most widespread.totalsis the site-wide roll-up.
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
- Performance budget — the other build-time quality check, which runs in the same pass.
- Publishing & environments — how builds and publishing fit together.
- Connect your AI assistant — read a11y reports over MCP with
a11y_latest,a11y_history, anda11y_regression.