CLI reference
manni a11y exposes one subcommand, check. It crawls a site
from one or more seed URLs and runs axe-core against every page it reaches.
There is no default subcommand: manni a11y <url> is a usage error, and the
verb is always spelled.
manni a11y [options] check [urls...] [command options]Global options
Section titled “Global options”Global options are accepted before the subcommand.
| Option | Description |
|---|---|
-V, --version | Print the manni version and exit. |
-h, --help | Print help for the program or a subcommand. |
The accessibility tool. a11y accepts its own -V, --version (the same
version) and -h, --help. Run with no subcommand it prints its help on
stderr and exits 2.
manni a11y [options] check [urls...] [command options]Options
Section titled “Options”| Option | Description |
|---|---|
--no-color | Disable colored output. Color is on only when stdout is a TTY, and never under NO_COLOR. The meanings match manni meta’s: ✓ green, ✗ red, severity from dim through yellow to red, URLs cyan. |
a11y check
Section titled “a11y check”Crawl a site from the given seeds, or from a collection’s url: or a11y.urls
in config, and run axe-core against every page. Exit 1 when any page keeps a
violation at or above the severity floor.
manni a11y check [urls...] [options]Arguments
Section titled “Arguments”| Argument | Description |
|---|---|
[urls...] | Seed URLs, space-separated, each http: or https:. Links are followed on every seed’s host, and sitemap URLs stay on the first seed’s host. Optional, and falls back to a collection’s url: or config a11y.urls; none of the three is an error (exit 2). See where seeds come from. A seed that is not http(s) is rejected before any browser launches. |
Options
Section titled “Options”| Option | Argument | Default | Description |
|---|---|---|---|
-f, --format | <pretty|json|github> | pretty | Output format. An unknown value is an error (exit 2). |
--crawl | n/a | on | Crawl from the given URLs, following the sitemap and same-host links. This is the default, so pass it to undo crawl: false in config for one run. |
--no-crawl | n/a | on | Check exactly the given URLs, with no sitemap lookup and no link following. Crawling is on by default. Overrides config crawl:. |
--max-pages | <n> | Cap on pages checked. Whatever is still queued at the cap is reported as skipped. Without it every discovered page is checked. Must be an integer of 1 or more (exit 2 otherwise). Overrides config maxPages:. | |
--no-max-pages | n/a | off | Check every page found, ignoring a maxPages: in config. No cap is the default, so this is how a configured one is lifted for one run. --max-pages and --no-max-pages set the same option, so the one written later on the command line wins. |
--exclude | <glob> | Glob matched against a URL’s path; a match keeps that page out of the crawl. Repeatable, one glob per occurrence, never comma-separated. A pattern must start with /. A pattern that excludes a seed is an error, and it names every excluded seed under the source that pattern came from. Both exit 2. See what a pattern matches. Replaces config exclude: rather than adding to it. | |
--tags | <list> | Comma-separated axe tags, given once; restrict axe to rules carrying any of them, for example wcag2a,wcag2aa,wcag21aa,best-practice. Whitespace around commas is trimmed and empty items are dropped. A second --tags replaces the first. Without it axe runs its default rule set. Overrides config tags:. | |
--severity | <level> | notice | Minimum severity reported and counted, on the family scale: notice, warning, or error. Violations below it are dropped before the score and the exit code are decided. The floor is also the gate: whatever it keeps fails the run, so a page whose worst finding is a warning still exits 1. See exit codes. An unknown level is an error (exit 2). axe’s four impact levels are folded onto these three when a page is read. Every finding keeps axe’s own value as impact, and the github format has the table. Overrides config severity:. |
--timeout | <ms> | 30000 | Per-page navigation timeout in milliseconds. Overrides config timeout:. |
-q, --quiet | n/a | off | In pretty output, hide pages with no remaining violations. Has no effect on json or github. |
--progress | n/a | auto | Report progress on stderr while the crawl runs. The default is on only when stderr is a terminal. Pass it to see each page in a CI log. |
--no-progress | n/a | off | Never report progress, even on a terminal. |
--collection | <name> | every collection | Seed from the url: of a configured collection. Repeatable, one name per occurrence, never comma-separated; repeats collapse. Cannot be combined with positional URLs, needs a config file, and a named collection that declares no url: is an error (exit 2 for all three). See where seeds come from. |
-c, --config | <path> | discovered | Path to a config file. When set, the file must exist or the run exits 2. |
--no-config | n/a | off | Ignore any discovered config file. -c and --no-config set the same option, so the one written later on the command line wins. |
Where seeds come from
Section titled “Where seeds come from”Four sources can name a seed. The first non-empty one wins, and the rest are ignored:
- Positional
[urls...], as typed. - The
url:of each collection named by--collection, in the order the collections are declared. a11y.urlsin config.- The
url:of every declared collection that has one, in declaration order.
a11y.urls sits ahead of the collections because a site has entry points no
documentation collection covers, such as a marketing or status page. Nothing
that already sets a11y.urls changes behaviour by adding a collection url:.
Naming a collection that declares no url: is an error, while a collection
without one simply contributing nothing to rung 4 is not. --collection blog
asked for something specific and has to be told it cannot be done; a bare
manni a11y check gets whatever is publishable.
manni: collection "blog" has no url: to check.manni: --collection selects a configured collection; it cannot be combined with URLs.manni: --collection needs a config file to select from.manni: No URLs to check. Pass one or more, set url: on a collection, or set a11y.urls in manni.config.yaml.All four exit 2.
The crawl
Section titled “The crawl”Scope is the host. Two URLs are on the same site when their hostnames match
case-insensitively and their non-default ports match. http:// and https://
of one host are one site, and www.example.com is not example.com. Each seed
adds its own host to the scope for links. Sitemap URLs stay on the first seed’s
host, as proposal 0035 records.
The frontier is filled in this order:
- Seeds, as given, normalized (fragment dropped, hostname lowercased, default port dropped, path and query kept as written).
- The sitemap. One lookup per run, from the first seed. The candidates, in
order, are every
Sitemap:line in<origin>/robots.txt, in the order the file lists them, followed by a walk of directories. The walk goes from the seed’s own directory up to the site root, nearest directory first and the root last. In each directory it triessitemap.xml,sitemap-index.xml, andsitemap_index.xml. For a seed ofhttps://example.com/a/b/page.htmlthat is/a/b/, then/a/, then/, three names in each. The first candidate that returns2xxand parses is used, and nothing after it is fetched. A<sitemapindex>is followed into its child sitemaps (depth 3, 50 children at most); a<urlset>contributes its<loc>values. Only same-host page URLs are kept. A missing, failing, or unparseable sitemap means “no sitemap” and is never an error. Gzipped sitemaps are not read. - Links. After each page loads, the absolute
hrefof everya[href]in the rendered DOM is queued if it is same-host,http(s), and looks like a page.mailto:,tel:,javascript:, anddata:are skipped, and so is a path whose last segment ends in an asset extension (.pdf,.zip,.png,.jpg,.svg,.css,.js,.json,.xml,.txt,.mp4,.woff2, among others).
Each normalized URL enters the frontier once, and a trailing slash does not make
a second page. /install and /install/ are one entry, under whichever
spelling arrived first. Where the browser lands after an HTTP redirect counts
as visited too. A queued spelling of that page is then dropped without loading and
reported as a duplicate. Pages are analyzed sequentially, in discovery order,
and every result records its source: seed, sitemap, or link. The run
goes on until the frontier is empty. With --max-pages, it stops once that many
pages have been checked, and the frontier left behind is the skipped count.
A page that redirects in the browser is not checked. That covers a meta refresh
to another page, and a page that navigates away while it is read. Its
destination joins the frontier with the same source, under the same scope and
exclusion rules, and is checked once. It is followed under --no-crawl too,
because it is the page you named under a new address. The redirecting page does
not count toward --max-pages, and summary.redirects counts it. A crawled
page that redirects off-host or to an excluded path is still counted, and its
destination is not checked.
Failures split by where they happen. A seed that fails to load or analyze stops
the run with exit 2, because nothing was checked. A crawled page that fails is
recorded with an error on that page, the crawl continues, and the page counts
as failed for the exit code.
A seed can also redirect in the browser to a URL that is not http(s), to
another host, or to an excluded path. That stops the run with exit 2 too. Nothing on the seed’s host would be checked,
and the message names the destination.
manni: http://127.0.0.1:4321/old/ redirects to javascript:void(0), which is not an http(s) URL.manni: http://127.0.0.1:4321/old/ redirects to https://example.com/new/, which is on another host. Check https://example.com/new/ instead.manni: --exclude "/legacy/**" excludes http://127.0.0.1:4321/legacy/new/, where the seed http://127.0.0.1:4321/old/ redirects.The last message names where its pattern came from, as the seed exclusion message does.
What a pattern matches
Section titled “What a pattern matches”--exclude and config exclude: take globs, and a glob is matched against the
URL’s path alone. Not the scheme, not the host, not the port, not the
query, not the fragment. So /proposals/** covers
https://example.com/proposals/0035/ and
http://127.0.0.1:4321/proposals/0035/ alike, which is what lets one pattern
serve a local preview and production. A pattern cannot tell two hosts apart.
Run them separately with --collection when that matters.
The base path is part of the path. On a site served under /manni/ the
pattern is /manni/proposals/**, not /proposals/**.
A trailing slash is not part of the comparison. The path is matched with any
trailing slash removed, so /proposals/ and /proposals are one thing. A
pattern ending /** covers the directory itself as well as everything under
it, so /proposals/** reaches the section index without a second pattern.
Both rules exist because a crawl produces both spellings. A sitemap usually
lists /proposals/ and a bare link usually points at /proposals.
Matching otherwise follows the same rules as a collection’s paths:. A *
stops at a path segment, a ** crosses segments, and a dot segment is matched
like any other. A pattern that does not begin with / could never match a
path, and is an error rather than a silent near-miss:
manni: --exclude "proposals/**" must start with "/": it matches a URL path.The filter runs where a URL enters the frontier, beside the same-host check
and the asset-extension check. An excluded URL is never fetched, never
analyzed, and never counted in discovered. summary.excluded counts it
instead, once per URL however many pages linked to it. The
pretty footer adds ; K excluded when the count is not
zero, beside the clauses for a cap and for dropped duplicates.
A seed an exclusion pattern matches is a contradiction, and both halves of it are what the user asked for. The run refuses rather than guessing, and names every excluded seed before it exits, so two wrong patterns cost one run rather than two:
manni: --exclude "/proposals/**" excludes the seed https://example.com/proposals/.manni: --exclude "/blog/**" excludes the seed https://example.com/blog/.Each line names where its pattern came from. A pattern that came from config is named by its file and its entry, so a run with no flag typed never reports one:
manni: manni.config.yaml: "a11y.exclude[0]" excludes the seed https://example.com/proposals/.Every one of these messages exits 2, and nothing is checked.
The score
Section titled “The score”score = round(100 × passes ÷ (passes + violations))passes is the number of axe rules that ran and passed on the page.
violations is the number of rules that failed at or above the --severity
floor, counted per rule rather than per element. The result is the share of
applicable rules that passed after severity filtering. It is not a Lighthouse
score: nothing is weighted, and a page with one error still fails the run
whatever its score says. The score is null when the page did not load
or when no rule applied.
Rules axe could not decide are reported as incomplete in json output only,
and never counted as failures.
Output formats
Section titled “Output formats”pretty (the default) is for people. It prints a header, one line per page,
the violations under each failing page, and a footer:
Checked 3 of 3 pages (sitemap: http://127.0.0.1:4173/sitemap.xml)✓ http://127.0.0.1:4173/index.html score 100✗ http://127.0.0.1:4173/about.html score 78 4 errors error button-name (axe: critical) 1 node Buttons must have discernible text axe-core 4.13.0 button-name: Ensure buttons have discernible text Guide: https://dequeuniversity.com/rules/axe/4.13/button-name?application=playwright button → Fix any of the following: Element does not have inner text that is visible to screen readers; aria-label attribute does not exist or is empty; aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty; Element has no title attribute; Element does not have an implicit (wrapped) <label>; Element does not have an explicit <label>; Element's default semantics were not overridden with role="none" or role="presentation" <button></button> error color-contrast (axe: serious) 1 node Elements must meet minimum color contrast ratio thresholds axe-core 4.13.0 color-contrast: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds Guide: https://dequeuniversity.com/rules/axe/4.13/color-contrast?application=playwright p → Fix any of the following: Element has insufficient color contrast of 2.84 (foreground color: #999999, background color: #ffffff, font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1 <p style="color:#999;background:#fff">Low contrast text.</p> error html-has-lang (axe: serious) 1 node <html> element must have a lang attribute axe-core 4.13.0 html-has-lang: Ensure every HTML document has a lang attribute Guide: https://dequeuniversity.com/rules/axe/4.13/html-has-lang?application=playwright html → Fix any of the following: The <html> element does not have a lang attribute <html><head> <meta charset="utf-8"> … error image-alt (axe: critical) 1 node Images must have alternative text axe-core 4.13.0 image-alt: Ensure <img> elements have alternative text or a role of none or presentation Guide: https://dequeuniversity.com/rules/axe/4.13/image-alt?application=playwright img → Fix any of the following: Element does not have an alt attribute; aria-label attribute does not exist or is empty; aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty; Element has no title attribute; Element's default semantics were not overridden with role="none" or role="presentation" <img src="x.png">✗ http://127.0.0.1:4173/orphan.html score 94 1 error error image-alt (axe: critical) 1 node Images must have alternative text axe-core 4.13.0 image-alt: Ensure <img> elements have alternative text or a role of none or presentation Guide: https://dequeuniversity.com/rules/axe/4.13/image-alt?application=playwright img → Fix any of the following: Element does not have an alt attribute; aria-label attribute does not exist or is empty; aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty; Element has no title attribute; Element's default semantics were not overridden with role="none" or role="presentation" <img src="y.png">
5 violations on 2 of 3 pagesThe header names the sitemap that was found, or reads (no sitemap; followed links). Under --no-crawl it reads (no crawl). Finding a sitemap
is not the same as using one. When it supplied some of the discovered pages,
the header adds its count and followed links for the rest. When it supplied
none, the header adds 0 pages; followed links. Each violation line
carries the severity, the rule id, and axe’s own impact in parentheses. Then
come the node count and axe’s short help text. The severity is what the floor
and the exit code use. The impact is there for lookup, since the Deque page and
the axe DevTools report both name it. Two dim lines follow. The first names the
axe version and the rule, and says what the rule checks. The second reads
Guide: and the rule’s Deque University URL. Then the first three failing
elements print as a selector and axe’s failure summary. Each has the element’s
html dim beneath it, cut at 200 characters. (+N more) follows past three. A page that
could not load prints could not load: <reason>. The footer adds
; K skipped (--max-pages) when the cap was hit, and ; K duplicates dropped
when a redirect landed on a page that was also queued under another spelling.
-q hides pages with no remaining violations.
A page that redirected in the browser gets one line after the page lines, in
the order it was visited. It reads
↪ http://127.0.0.1:4173/old.html redirects to http://127.0.0.1:4173/new.html,
with ↪ dim and the URLs cyan. -q hides these lines too. The footer then
adds ; N redirects after the duplicates clause.
json prints one object, the whole run, for scripts:
{ "results": [ { "url": "http://127.0.0.1:4173/orphan.html", "violations": [ { "id": "image-alt", "severity": "error", "impact": "critical", "help": "Images must have alternative text", "helpUrl": "https://dequeuniversity.com/rules/axe/4.13/image-alt?application=playwright", "tags": [ "cat.text-alternatives", "wcag2a", "wcag111", "section508", "section508.22.a", "TTv5", "TT7.a", "TT7.b", "EN-301-549", "EN-9.1.1.1", "ACT", "RGAAv4", "RGAA-1.1.1" ], "nodes": [ { "target": "img", "html": "<img src=\"y.png\">", "summary": "Fix any of the following:\n Element does not have an alt attribute\n aria-label attribute does not exist or is empty\n aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n Element has no title attribute\n Element's default semantics were not overridden with role=\"none\" or role=\"presentation\"" } ], "guidance": { "id": "axe-core 4.13.0", "rule": "image-alt", "description": "Ensure <img> elements have alternative text or a role of none or presentation", "guide": "https://dequeuniversity.com/rules/axe/4.13/image-alt?application=playwright" } } ], "passes": 15, "incomplete": 0, "source": "seed", "score": 94 } ], "redirects": [], "summary": { "discovered": 3, "checked": 1, "skipped": 2, "duplicates": 0, "redirects": 0, "excluded": 0, "failed": 1, "violations": 1, "bySeverity": { "notice": 0, "warning": 0, "error": 1 }, "sitemap": "http://127.0.0.1:4173/sitemap.xml", "sitemapPages": 3, "crawl": true }}That run was capped with --max-pages 1, so summary counts three pages
discovered, one checked, and two skipped.
Each entry of results[]:
| Field | Type | Meaning |
|---|---|---|
url | string | The normalized URL that was checked. |
source | "seed" | "sitemap" | "link" | How the URL entered the run. |
violations | array | Rules that failed at or above the severity floor. Each has id, severity, impact, help, helpUrl, tags, nodes[] with target (axe’s CSS selector path), html (the element’s outer HTML), and summary (axe’s failure summary), and guidance. Empty when error is set. |
violations[].severity | "notice" | "warning" | "error" | The family level, mapped from impact. What the floor, the exit code, and the github format use. |
violations[].impact | "minor" | "moderate" | "serious" | "critical" | axe’s own value, kept for lookup against its rule pages. A rule axe reports with no impact is recorded as "minor". |
violations[].guidance | object | What judged the finding. id is axe-core and its version, rule is the rule id, description is axe’s description of the rule, and guide is its helpUrl. |
passes | integer | Rules that passed on this page. |
incomplete | integer | Rules axe could not decide. Reported, never counted as failures. |
score | integer | null | See the score. null when the page did not load or no rule applied. |
error | string, optional | Present when the page could not be loaded or analyzed. The page counts as failed. |
Each entry of redirects[] is a page that redirected in the browser, in the
order it was visited. A redirecting page is never in results[]. The array is
empty when no page redirected.
| Field | Type | Meaning |
|---|---|---|
url | string | The page that redirected, as queued. |
to | string | Where it redirects, normalized. Present whether or not the destination was checked. |
source | "seed" | "sitemap" | "link" | How the redirecting page entered the run. Its destination inherits it. |
And summary:
| Field | Type | Meaning |
|---|---|---|
discovered | integer | Distinct URLs that entered the frontier, whether seeds, sitemap entries, or links. |
checked | integer | Pages actually analyzed. |
skipped | integer | Discovered but not checked because --max-pages was reached. 0 when no cap is set. |
duplicates | integer | Discovered, then dropped without loading because a redirect had already landed on the same page under another spelling. |
redirects | integer | Discovered, then not analyzed because the page redirects in the browser. checked + skipped + duplicates + redirects equals discovered. |
excluded | integer | Distinct URLs an exclusion pattern kept out of the crawl, counted once each however many pages linked to them. It sits outside the identity above, because an excluded URL never entered the frontier. This is the authoritative total for a finished run, and it can exceed what the progress line reported. 0 when nothing was excluded. |
failed | integer | Pages with at least one remaining violation, of any severity, or an error. Drives exit 1. |
violations | integer | Remaining violations across all pages, counted per rule rather than per node. |
bySeverity | object | Violation counts keyed notice, warning, error. All three keys are always present. |
sitemap | string | null | The sitemap URL that was found, or null when none was. Finding one is not the same as using one, which sitemapPages reports. |
sitemapPages | integer | In-scope page URLs that sitemap contributed to the frontier. 0 when no sitemap was found, and 0 when one was found whose entries were all out of scope. |
crawl | boolean | Whether crawling was on for the run, with a sitemap lookup and link following. false under --no-crawl. |
github prints one workflow command per finding, and nothing when the run
is clean. The message reads <help>: N node(s) on <url>. Up to three failing
elements follow %0A. Each is a selector with axe’s failure summary, then its
html, cut at 200 characters. The guidance and Guide: lines come last:
::error title=a11y/button-name::Buttons must have discernible text: 1 node on http://127.0.0.1:4173/about.html.%0Abutton → Fix any of the following: Element does not have inner text that is visible to screen readers; …%0A <button></button>%0Aaxe-core 4.13.0 button-name: Ensure buttons have discernible text%0AGuide: https://dequeuniversity.com/rules/axe/4.13/button-name?application=playwright::error title=a11y/color-contrast::Elements must meet minimum color contrast ratio thresholds: 1 node on http://127.0.0.1:4173/about.html.%0Ap → Fix any of the following: Element has insufficient color contrast of 2.84 …%0A <p style="color:#999;background:#fff">Low contrast text.</p>%0Aaxe-core 4.13.0 color-contrast: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds%0AGuide: https://dequeuniversity.com/rules/axe/4.13/color-contrast?application=playwright::error title=a11y/html-has-lang::<html> element must have a lang attribute: 1 node on http://127.0.0.1:4173/about.html.%0Ahtml → Fix any of the following: The <html> element does not have a lang attribute%0A <html><head> <meta charset="utf-8"> …%0Aaxe-core 4.13.0 html-has-lang: Ensure every HTML document has a lang attribute%0AGuide: https://dequeuniversity.com/rules/axe/4.13/html-has-lang?application=playwright::error title=a11y/image-alt::Images must have alternative text: 1 node on http://127.0.0.1:4173/about.html.%0Aimg → Fix any of the following: Element does not have an alt attribute; …%0A <img src="x.png">%0Aaxe-core 4.13.0 image-alt: Ensure <img> elements have alternative text or a role of none or presentation%0AGuide: https://dequeuniversity.com/rules/axe/4.13/image-alt?application=playwright::error title=a11y/image-alt::Images must have alternative text: 1 node on http://127.0.0.1:4173/orphan.html.%0Aimg → Fix any of the following: Element does not have an alt attribute; …%0A <img src="y.png">%0Aaxe-core 4.13.0 image-alt: Ensure <img> elements have alternative text or a role of none or presentation%0AGuide: https://dequeuniversity.com/rules/axe/4.13/image-alt?application=playwrightA violation whose rule and elements, html included, are the same on several
pages is one command. Its first line reads <help>: N node(s) on K pages, first <url>. and adds The same element fails on each page, so the fix likely belongs in a shared template. The pretty and json formats still list it under each
page.
GitHub renders each as an annotation on the workflow run. There is no file or
line to anchor to, since the finding is about a URL. The command’s level is the
finding’s severity. The family scale was chosen to match GitHub’s, so the
annotations are one-to-one. axe’s four impact levels were folded onto that scale
when the page was read, and this is the map. A page that did not load gets one
::error title=a11y/load:: command naming the URL and the reason.
| axe impact | Severity | Annotation |
|---|---|---|
critical | error | ::error |
serious | error | ::error |
moderate | warning | ::warning |
minor | notice | ::notice |
Progress
Section titled “Progress”The report waits for the whole crawl, and a fifty-page site takes a minute. So
check narrates the run on stderr while it works. It says when the browser is
starting, which sitemap it found, and which page it is on, as
[3/51] https://docs.example.com/install/. On a terminal that is one status
line, rewritten in place and cleared before the report prints. Off a terminal
it is one line per event, each prefixed manni: , with no escape codes.
Progress is on by default only when stderr is a terminal, so a CI log stays as
it was. Pass --progress to see it in a CI log anyway. Pass --no-progress
to silence it. Either way stdout carries the report alone, so
-f json | jq keeps working.
A page that redirects in the browser gets a line in place of its result. The counter counts checked pages, so the next page reuses the number. Off a terminal the lines read:
manni: [3/52] http://127.0.0.1:4321/manni/meta/proposals/ai-context/manni: [3/52] ↪ redirects to http://127.0.0.1:4321/manni/meta/reference/schemas/manni-ai-context/manni: [3/53] http://127.0.0.1:4321/manni/meta/reference/schemas/manni-ai-context/On a terminal the status line reads [3/52] ↪ redirects to and the
destination, with ↪ dim and the URL cyan.
An exclusion gets one line of its own, written once discovery settles and only
when something was kept out. It reads manni: excluded 41 pages (2 patterns).
There is no line per excluded URL, because a pattern that removes four hundred
pages would bury the ones that were checked.
That line counts what the seeds and the sitemap gave the run and a pattern
then removed, before the browser started. It lands early on purpose. A pattern
that matched far more than intended is then visible while the run is still
cheap to stop. Links are followed afterwards, and a page excluded that way is
added to the total later. So the line can report fewer pages than the run ends
up excluding. summary.excluded in -f json is the authoritative total for a
finished run.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 | Every page loaded and none has a violation at or above the severity floor. |
1 | At least one page has a remaining violation, or a crawled page failed to load. summary.failed is greater than zero. |
2 | Usage or operational error. The causes are an unknown flag value, a non-http(s) URL, or no URLs and no config. They also include a seed that could not be loaded, or no browser found. So is a seed that redirects to a non-http(s) URL, another host or an excluded path. Most print one line on stderr prefixed manni: . A missing or unknown subcommand prints the help or commander’s own message instead. |
Severity is read once, at the floor. Every violation the floor keeps fails its
page, whatever its level, so a run whose worst finding is a warning or a
notice still exits 1. manni meta validate and manni cite check differ
here, and fail on error-severity findings alone. Both let you move a finding
onto the level you want. axe assigns a11y’s severities, and no config moves
one, so --severity is this domain’s only lever over the gate. That is why it
holds both jobs. The
family severity page
has the comparison, and
proposal 0064
the reasoning.
Examples
Section titled “Examples”Get started covers the first
three rungs, a bare crawl, --no-crawl, and the zero-argument config form.
Continuing from there:
# Several seeds: links on both hosts; sitemap URLs from the first seed's host only.manni a11y check https://docs.example.com/ https://blog.example.com/
# One collection, seeded from its url: — the same name meta validates by.manni a11y check --collection guides
# Two of them, in the order the config declares, whatever order they are named.manni a11y check --collection blog --collection guides
# Gate only on what matters: WCAG 2.1 AA rules, errors only, hide clean# pages. Exit 1 only if an error remains; warnings and notices are dropped.manni a11y check https://docs.example.com/ --tags wcag2a,wcag2aa,wcag21aa --severity error -q
# CI: GitHub annotations, bounded run, explicit config, no color.manni a11y check https://docs.example.com/ -f github --max-pages 50 -c ci/manni.config.yaml --no-color
# Scripts: the JSON envelope, piped to jq.manni a11y check https://docs.example.com/ -f json | jq '.summary'
# Leave a section out of the gate. One glob per --exclude, never a comma.manni a11y check https://docs.example.com/ --exclude "/proposals/**"
# Two sections out, and the count of what went on stderr as it happens.manni a11y check https://docs.example.com/ \ --exclude "/proposals/**" --exclude "/reference/api/**" --progress
# Config says exclude: ["/proposals/**"]; this run checks the proposals and# leaves the API reference out instead. The flag replaces the list.manni a11y check --exclude "/reference/api/**"
# Config says crawl: false and maxPages: 50; this one run ignores both and# walks the whole site. The rest of the file, collections: included, still# applies, which is what separates this from --no-config.manni a11y check https://docs.example.com/ --crawl --no-max-pages
# Every option at once. Two seeds (space-separated); sitemap and links; cap# 250 pages; two exclusion globs, one per occurrence; one comma-separated# --tags list; warning floor; 60 s per page; quiet; ignore any discovered# config; json; no color.manni a11y check https://docs.example.com/ https://www.example.com/ \ --max-pages 250 \ --exclude "/proposals/**" \ --exclude "/reference/api/**" \ --tags wcag2a,wcag2aa,wcag21aa,best-practice \ --severity warning \ --timeout 60000 \ --quiet \ --no-config \ --format json \ --no-colorUsage errors exit 2 and write to stderr. A missing or unknown subcommand gets
the help or commander’s message. Every other error is one line prefixed
manni: :
manni a11y# (prints the a11y help)
manni a11y https://x.example/# error: unknown command 'https://x.example/'# (add --help for usage)
manni a11y check not-a-url# manni: Not an http(s) URL: "not-a-url".
manni a11y check https://x.example/ --severity high# manni: Unknown --severity "high". Use notice | warning | error.
manni a11y check https://x.example/ --max-pages 0# manni: --max-pages must be an integer >= 1.
manni a11y check https://x.example/ -f sarif# manni: Unknown --format "sarif". Use pretty | json | github.
manni a11y check https://x.example/ # with no browser installed# manni: No browser found. Install one with `npx playwright install chromium`, or install Google Chrome or Microsoft Edge.
manni a11y check https://x.example/ --exclude "proposals/**"# manni: --exclude "proposals/**" must start with "/": it matches a URL path.
manni a11y check https://x.example/proposals/ --exclude "/proposals/**"# manni: --exclude "/proposals/**" excludes the seed https://x.example/proposals/.
manni a11y check https://x.example/ --exclude# error: option '--exclude <glob>' argument missingConfiguration
Section titled “Configuration”a11y reads its own key of the shared manni.config.yaml, discovered the same
way meta discovers it, walking up from the working directory. -c <path>
names the file explicitly, and --no-config ignores any discovered one. A flag
wins over the same key in config, and config wins over the default. Every key
is optional, and an empty a11y: section is the same as none.
collections: - name: site paths: ["docs/"]
a11y: urls: ["https://docs.example.com/"] crawl: true maxPages: 500 tags: ["wcag2a", "wcag2aa"] severity: error timeout: 30000 exclude: ["/proposals/**"]| Key | Type | Default | Description |
|---|---|---|---|
urls | string[] | none | Seed URLs, each http: or https:. A fallback for [urls...]; positional URLs win when both are present, and this key wins over a collection’s url:. See where seeds come from. |
crawl | boolean | true | Whether to discover pages from the sitemap and same-host links. --no-crawl sets it to false for one run. |
maxPages | integer ≥ 1 | none, no cap | Cap on pages checked per run. Without it every discovered page is checked. Mirrors --max-pages. |
tags | string[] | none | axe tags to restrict the rule set to. Mirrors --tags, one item per entry. |
severity | notice | warning | error | notice | Minimum severity reported and counted, on the family scale. Mirrors --severity. |
timeout | integer ≥ 1 | 30000 | Per-page navigation timeout in milliseconds. Mirrors --timeout. |
exclude | string[] | [] | Globs matched against a URL’s path; a match keeps that page out of the crawl. One glob per entry. An explicit [] means the same as the key being absent. Each entry must start with /, and the error names the entry by its position. A typed --exclude replaces this list rather than adding to it. See what a pattern matches. |
An unknown key, a wrong type, an empty urls entry, or a urls entry that is
not http(s) is an error. The error names the file and the key (exit 2).
urls is optional. A repository that declares its documents as
collections can give each
one a url: and leave the a11y: section to the options above, or omit it
entirely:
collections: - name: guides paths: ["docs/guides/**/*.md"] url: https://docs.example.com/guides/
a11y: severity: warning