Search by

bpmore / statamic-a11y-report

bpmore

Scans every page of a Statamic site over time and keeps the evidence: findings, coverage, and the state of each problem from one scan to the next.

Package info

github.com/bpmore/statamic-a11y-report

Homepage

Type:statamic-addon

pkg:composer/bpmore/statamic-a11y-report

Statistics

Installs: 16

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.5 2026-09-08 03:16 UTC

README

Accessibility Gate stops a bad entry. Accessibility Report proves the site is clean, over time, in a document a compliance officer can file.

This addon depends on Accessibility Gate at 0.7 or later, which stays free. The report is a paid addon.

Status

Every pillar in the brief is built: the scan layer and both engines, the overview, the HTML and PDF conformance report, the accessibility statement, the remediation queue and its policy, the criteria worksheet, the recurring scan, and this page's open issues in the gate's own entry sidebar.

php please a11y:scan reads every published page on the queue, keeps every finding, records which engine read it and how much of it that engine could see, and follows each problem from one scan to the next by a fingerprint of rule, target, site and path.

This is 0.4.1. What is in it is in the changelog, and the reasoning behind the choices that are not obvious from the code is in the decision log. Before 1.0, a breaking change raises the minor version.

Install

composer require bpmore/statamic-a11y-report
php please a11y:report:install

Accessibility Gate comes with it. It is on Packagist and needs nothing said about it here.

The second command creates the tables. By default they go in a SQLite file the addon creates for itself under storage/a11y-report/, so a flat-file site needs nothing else. To use a database the site already runs, set A11Y_DB_CONNECTION to one of its connection names and run the command there.

If you skip the install and the addon is on its own SQLite file, the first scan runs the install itself. On any other connection it asks you to.

Engines

Two, and a scan records which one ran, at what version, and with which rules.

php is the default: the same checker Accessibility Gate runs before a publish, run across the whole site. It reads markup, needs nothing installed, and can speak to six success criteria.

axe runs axe-core in headless Chrome against the page as your site serves it. It speaks to around two dozen criteria, colour contrast among them, which is the failure most sites have most of and the one reading markup can never find. Set engine to axe in the config file, or A11Y_ENGINE=axe, or run one scan with php please a11y:scan --engine=axe.

axe-core is bundled with the addon, so there is nothing to install and nothing is fetched while scanning. It needs Chrome on the machine and your site reachable from it. Ask for axe without Chrome and the scan runs the PHP checker instead and says so; the scan row and the report always name the engine that actually ran.

Each page is read by the engine its own scan says read it, so --engine=axe holds on the queue and not only under --sync. Every machine that works the queue needs Chrome for that. A worker that cannot run the scan's engine marks the page as one that could not be read rather than reading it with the other one, because a row saying axe filled with the checker's findings would have the report name criteria nothing looked at.

axe has rules of its own that no success criterion requires, such as every page having landmarks. They are worth fixing and they are not conformance: they keep their own plain names, are listed apart from WCAG, and are never counted under a criterion. On a theme that was not built with landmarks in mind they can outnumber everything else, so there is a switch on the settings screen under Scanning, and axe.best_practices in the config file until the screen is saved.

Level AAA is never run by either. The conformance table has no AAA row.

axe-core is Deque's, MPL-2.0, bundled unmodified at resources/js/axe.min.js with its licence header intact.

Switching engines does not close the other engine's issues: two engines look for different things, and one not looking for something is not evidence it was fixed. They stay open in the queue until a scan by their own engine finds them gone, or you close them by hand.

Scan

php please a11y:scan                      queue every published page
php please a11y:scan --sync               run in this process; exit non-zero above the thresholds
php please a11y:scan --site=default --collection=pages --since="7 days ago"
php please a11y:scan --resume=<scan id>   pick up a scan that stalled
php please a11y:scan:cancel <scan id>     end one that never can

A scheduled scan is skipped while anything is queued or running, so a scan nobody can finish stops the schedule. --resume --sync finishes it in one process and needs no queue worker, which is the answer whenever the pages can still be read. a11y:scan:cancel is for when they cannot. What was read is kept; a scan that was ended can never be reported on.

--sync is for a deploy pipeline. It exits non-zero when a count is above the limits in config/statamic-a11y-report.php, and always when a page could not be read, because a build that went green while four pages failed to render is worse than no check at all.

The queue needs Laravel's job_batches table. The install creates it through your site's own jobs migration when you have one, so php artisan migrate keeps working afterwards.

On a schedule

scan.schedule in the config file takes daily, weekly (Sunday), monthly (the 1st), any cron expression, or false for none. scan.schedule_at is the time of day for the named ones, 02:00 by default. There is no hourly: a scan renders every published page, so write the cron expression if you really want that.

It needs Laravel's scheduler running on the server. One line in the crontab, which covers every scheduled task your site has and not only this one:

* * * * * cd /path/to/site && php artisan schedule:run >> /dev/null 2>&1

Check which php that line will find before you trust it. Cron runs with almost no environment and does not use your shell's PATH. Run which php: if it answers with anything other than a plain /usr/bin/php, which is the ordinary case with Herd, Valet, Homebrew or a version manager, put that whole path in the line instead, in double quotes if it contains a space:

* * * * * cd /path/to/site && "/Users/you/Library/Application Support/Herd/bin/php" artisan schedule:run >> /dev/null 2>&1

A crontab line naming a php that cron cannot find fails silently for ever, and a schedule that never runs looks exactly like one nobody set up. The overview says so after two missed runs, which is the second line of defence and not the first.

Without it nothing runs, so the report overview tells you rather than leaving you to notice from a document with an old date on it. It says the same thing again if scheduled scans were happening and have stopped. php artisan schedule:list shows what is registered.

A scheduled scan will not start while another is queued or running. It does not fail the way --sync does: issue counts and pages that could not be read are on the screen and in the report, and a weekly job that reports a failure every week is one whose mail stops being read.

In the entry sidebar

Accessibility Gate's panel on an entry screen shows this page's open issues from the last scan beneath its own result, with a link into the queue filtered to the page, so the gate and the queue agree in the one place an author looks.

That block carries the same mark as the cover of a report, when one is set, so an author can see whose report it is. The logo only, never the accent colour: the colour is checked against the white page a report is printed on, and no single colour reaches 4.5:1 against both of the control panel's themes, so a heading tinted with it would fail contrast in one of them. The mark is served from the control panel rather than embedded in the page, and a logo the report would refuse to print is one the panel does not wear either. It needs Accessibility Gate 0.8 or newer; an older gate ignores the mark, and the block is otherwise unchanged.

Remediation targets and accepted issues

The queue and the report measure open problems against targets you set: how many days a critical, serious, moderate or minor problem may stay open. Anything past its target is counted and can be filtered to, in the queue and in the document.

An issue you decide not to fix is accepted rather than closed. That needs a reason, which the report prints, and a date it runs out on, which cannot be further ahead than the review period in your settings. There is no permanent acceptance: past the date the issue counts as open again until somebody looks at it, and the record of who accepted it and why is kept.

Accepting an issue changes nothing a report claims. It is still a failure, it is still counted under its success criterion, and it is still listed in the document. The targets are a promise about what you will chase, and no setting here can move a criterion in the conformance table.

Every report carries a copy of the targets that were in force when it was generated, so a document you filed months ago still makes sense after you change them.

Settings

Addons, then Accessibility Report, then Settings. Not the same place as the utility, which is under Tools, then Utilities: the settings screen is where the addon is configured, and the utility is where its numbers are read.

The screen holds the WCAG version to report against, who evaluated the site, your mark on the cover of a report, the remediation plan and its targets, whether to report the findings that no success criterion requires, everything the public statement says about you, and which collections, sites and addresses a scan covers. It wins once it has been saved; until then the config file answers. The database, engine, browser, queue and deploy thresholds stay in the config file and .env, because a wrong value for any of them stops scans rather than changing a sentence.

The conformance report

php please a11y:report                       from the latest complete scan
php please a11y:report --site=default --format=html --out=./reports

It writes an Accessibility Conformance Report as a standalone HTML document and as JSON, under storage/a11y-report/reports/, and records who generated it and from which scan. Every WCAG Level A and AA success criterion is in the table. The default is "Not evaluated". A criterion the scan found failures under is "Does not support". Nothing becomes "Supports" unless a person assessed it and locked the assessment, and a locked assessment is never overwritten by a scan. The scope and limits statement is fixed text: you can add remarks, an evaluator and a remediation plan in config, and you cannot remove it. Every criterion in the document, and the standard on its cover, links to the W3C's own text for it, and the PDF describes each link for a screen reader.

Reports can also be generated, listed and opened from the control panel, by anybody with the generate accessibility reports permission.

Your mark on the cover. A logo, the words that stand in for it, and one heading colour, on the settings screen or under report.brand in config. A multisite install can give one site a mark of its own; a report covering more than one site wears none of them. The logo is embedded in the document as it is generated, so a report keeps the mark it was filed with, and it may be an asset, a file inside the site, or the name of a file in the container the picker uses. Three things are refused rather than printed: a logo with no words, because an untagged image would fail the PDF/UA validation the suite demands; a colour that does not reach 4.5:1 on white, which is what 1.4.3 asks of text that size; and a drawing that carries script or points outside itself. Each is left out with a warning in the log, and the report is generated either way. Nothing else about the document can be changed, and nothing a brand touches can reach the scope and limits statement.

The PDF. --format=pdf or --format=all prints the document with headless Chrome, which produces a tagged PDF with a structure tree, an outline, and the document language; the addon adds the XMP title, the display-title preference, and a role map. Chrome or Chromium is found on the path, or set A11Y_CHROME_PATH. The file declares PDF/UA-1, and the test suite validates that with veraPDF wherever it is installed (brew install verapdf on a Mac): a real site's report passes every check. That is the document's structure, verified; how it reads in a screen reader is not something a validator can say.

A scan that does not move. Scans are queue jobs on the connection the site's QUEUE_CONNECTION names. If nothing works that connection (a site running Horizon works Redis, not the database), a scan sits at "queued". The overview says so after ten minutes, with the two cures: a worker for that connection, or php please a11y:scan --resume=<id> --sync.

The accessibility statement

Every site gets a page at /accessibility. What it says about conformance comes from the latest report and cannot be configured: partially conformant when a criterion failed, not fully evaluated when any criterion has no determination, fully conformant only when a person assessed every criterion. What it says about you comes from the statement block in the config file: commitment, feedback, contact, escalation, and the enforcement body, with a per-site override for multisite installs. Pick section508 or en301549.

To put the statement inside a page of your own instead, use the tag:

{{ a11y:statement }}                     Antlers, headings from h2
<s:a11y:statement heading="1" />         Blade

A Blade site whose layout uses @yield has no layout Statamic can wrap the page in. The page then renders in a plain shell of the addon's own, or set statement.view to a Blade template of yours that calls the tag. After a new report, php please a11y:statement:refresh clears the page from the static cache.

In the control panel

Tools, then Utilities, then Accessibility Report: what is open now by impact, the last scan, a line of issues per scan over 90 days, and every scan so far. A button runs a scan on the queue; it needs the run accessibility scans permission, which is separate from seeing the page because it makes the site render every page.

Every row in the queue offers two ways to the page: its path opens the entry to edit, and "View page" opens the page itself on the site in a new tab, which is the only place a contrast failure can actually be seen. The edit link is left out where there is nothing to open, either because the entry has been deleted since the scan or because that person may not edit that collection. The document keeps public addresses only; its reader may have no control panel at all.

Issues, the second page under the utility, is the remediation queue: every problem with its status, filtered by status, impact, criterion, site, collection and assignee, oldest and most serious first. Anybody with the manage accessibility issues permission can tick issues and set a status, an assignee and a note, or apply the change to everything the filter matches. A scan never reopens an issue marked won't fix or false positive, and reopens a fixed one only if the problem comes back. An issue on a page that a full scan no longer meets, because the page was unpublished, deleted or moved, is marked "page removed" rather than fixed.

Criteria, the third page, is the worksheet: every success criterion with the automated evidence from the latest scan, the result the report will print, and your own status, remarks and lock, per site or as a global default. This is the only way a criterion becomes "Supports". Only rows you change are written, with your name and the date, and it needs the assess accessibility criteria permission. A locked row is never changed by a scan. Each criterion, here and in the queue, links to the W3C's Understanding page for it under the WCAG version in your settings.

A dashboard widget shows the open count by impact and a 30-day line. Add it to config/statamic/cp.php:

'widgets' => [
    'accessibility_report',
    // or, on a multisite install, one site:
    ['type' => 'accessibility_report', 'site' => 'default'],
],

What it reads with

The php engine is the same checker Accessibility Gate runs before a publish, with the same standard and the same opt-in checks, so a page the gate refuses and a page the scan flags are the same page for the same reason. It reads rendered HTML and cannot see anything a stylesheet decides, colour contrast included.

The axe engine reads the page in a browser with your stylesheets applied, so it does see contrast, and it speaks to around two dozen criteria rather than six. See Engines for how to choose one.

Neither proves a page is accessible. Most of WCAG is judged on meaning by a person, and automated testing finds roughly half of issues by volume and far fewer criteria. A page an engine finds nothing wrong with has not been proven accessible, every scan says so, and every report names which criteria were evaluated automatically and which were not evaluated at all.

Support

Report a bug or ask a question by opening an issue:

https://github.com/bpmore/statamic-a11y-report/issues

Every bug report gets a reply. Aim for a few working days. There is no guaranteed response time, and there is no support channel that can stop a site being scanned: nothing here phones home, so a support problem is never an outage.

A useful report says which version of the addon and of Statamic you are on and which engine ran. composer show bpmore/statamic-a11y-report statamic/cms gives the first two, and the overview in the control panel names the engine of the last scan.

Licence

Proprietary. See LICENSE.md.