sitecheck
Static website auditor. Point it at a build folder (dist/, public/, _site/, site/) or a local/remote URL and it reports broken links and anchors, missing or duplicated SEO metadata, accessibility basics, and performance-budget violations, with exit codes, a baseline file and JSON, JUnit, SARIF and HTML reports that drop straight into CI.
Features
- Two crawlers. Filesystem mode serves your output folder virtually (root-relative and relative URLs,
index.htmldirectory indexes,<base href>, case-sensitivity checks). HTTP mode is asyncio + httpx: same-origin crawl with a concurrency limit, robots.txt, sitemap seeding, depth/page limits, retries with exponential backoff. - Links. Broken links and resources (including
srcsetand CSSurl()), missing#fragmentanchors, redirects and redirect chains/loops, mixed content, case mismatches (works on macOS, 404s on Linux hosting), orphan pages, and optional external link checking (--external, HEAD then GET fallback, cache, per-host rate limit). - SEO. Title and description presence/length, duplicates across pages, canonical validity, Open Graph/Twitter completeness, h1 count, sitemap consistency in both directions, noindex warnings, hreflang validity and reciprocity.
- Accessibility. Images without alt, unnamed links and buttons, unlabeled form controls, heading skips, missing
lang, duplicate ids, positive tabindex, missing<main>, and WCAG contrast for inline-styled text when both colors are statically known. - Performance budgets. Page weight, request count, per-asset size, oversized images vs their declared width, missing lazy-loading, gzip savings estimate, render-blocking scripts. Budgets live in
sitecheck.toml. - CI friendly.
--fail-onseverity threshold, baseline file to ignore known issues, per-rule severity overrides, JSON/JUnit/SARIF/HTML output.
Install
Requires Python 3.11+.
uv venv .venv
uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/sitecheck --help
Quickstart
# audit the bundled example site (a deliberately broken ~20-page site)
.venv/bin/sitecheck check examples/site --config examples/sitecheck.toml
# same thing over HTTP
(cd examples/site && python -m http.server 8159) &
.venv/bin/sitecheck check http://127.0.0.1:8159/ --config examples/sitecheck.toml --quiet
kill %1
# all report formats at once, fail the build on warnings
.venv/bin/sitecheck check examples/site --config examples/sitecheck.toml \
--json reports/r.json --junit reports/r.xml --sarif reports/r.sarif --html reports/r.html --fail-on warning
examples/site is regenerable: .venv/bin/python examples/make_assets.py rebuilds the images (Pillow), fonts and bulk CSS/JS.
Usage
sitecheck check TARGET [options] TARGET is a directory or an http(s):// URL
sitecheck rules [--category C] list every rule with default severity
| Option | Meaning |
|---|---|
--config FILE |
sitecheck.toml; default is ./sitecheck.toml, then TARGET/sitecheck.toml |
--external / --no-external |
check external links over the network (off by default) |
--site-url URL |
public origin; absolute URLs on it are verified locally |
--max-pages N, --max-depth N, --concurrency N, --no-robots |
crawl limits |
--only links,seo,a11y,perf |
report only these categories |
--fail-on error\|warning\|info\|none |
exit 1 if an issue at/above this level remains (default error) |
--baseline FILE / --write-baseline FILE |
hide known issues / record current issues |
--json/--junit/--sarif/--html FILE |
write a report |
--quiet |
summary only |
Exit codes: 0 clean at the threshold, 1 issues at or above --fail-on, 2 usage/config/crawl-start error.
Example: filesystem mode
sitecheck check examples/site --config examples/sitecheck.toml (output trimmed; the real run lists all 21 pages, exit code 1):
sitecheck examples/site (fs mode, 21 pages, 52 URLs checked)
/
info text-uncompressed /css/site.css is 119 KB raw, about 14 KB gzipped
/about.html
error case-mismatch /Team/ exists as /team/index.html; this 404s on case-sensitive hosts (line
22)
warning contrast #999999 on #ffffff is 2.85:1 (needs 4.5:1) (line 23)
warning duplicate-description same description "Acme Widgets builds reliable, well documented widg" as
/team/
warning duplicate-title same title "About Acme Widgets - our story" as /team/
warning heading-skip h2 followed by h4 ("Awards") (line 21)
info redirect /team redirects (301): /team -> /team/ (line 22)
/blog/
error broken-link /blog/post-3.html returns 404 (line 20)
/blog/post-1.html
warning description-missing no meta description
warning duplicate-title same title "Announcing the Widget 2.0 release with a very long" as
/blog/post-2.html
warning heading-skip h1 followed by h3 ("Details") (line 16)
warning title-length title is 76 characters (allowed 10-60)
/blog/post-2.html
warning canonical-invalid canonical "/blog/post-2.html" is not an absolute URL
warning duplicate-title same title "Announcing the Widget 2.0 release with a very long" as
/blog/post-1.html
warning title-length title is 76 characters (allowed 10-60)
/contact.html
error button-name <button> has no accessible name (line 24)
error duplicate-id id "name" used 2 times (line 20)
...
Summary
┏━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━┳━━━━━━┓
┃ category ┃ error ┃ warning ┃ info ┃
┡━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━╇━━━━━━┩
│ links │ 11 │ 3 │ 2 │
│ seo │ 2 │ 19 │ 2 │
│ a11y │ 10 │ 7 │ 0 │
│ perf │ 0 │ 5 │ 7 │
│ total │ 23 │ 34 │ 11 │
└──────────┴───────┴─────────┴──────┘
Notable findings in the full output, each planted in examples/site:
/about.html error case-mismatch /Team/ exists as /team/index.html; this 404s on case-sensitive hosts
/products/ error broken-anchor /products/widget.html#reviews: no element with that id or name
/products/ warning image-oversized /img/hero-huge.jpg is 2400px wide but displayed at 600px
/products/ warning page-weight 2433 KB total exceeds budget of 1024 KB
/contact.html warning contrast #444444 on #336699 is 1.62:1 (needs 4.5:1)
/mixed.html error mixed-content insecure script http://cdn.example.com/widget.js loaded from an https page
/en/ warning hreflang-no-return /fr/ (fr) has no hreflang link back
/sitemap.xml error sitemap-url-broken sitemap lists /gone.html which returns 404
/hidden.html warning orphan-page no page links here (listed in the sitemap)
Example: HTTP mode
Against python -m http.server 8159 serving the same folder:
sitecheck http://127.0.0.1:8159/ (http mode, 18 pages, 44 URLs checked)
Summary
┏━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━┳━━━━━━┓
┃ category ┃ error ┃ warning ┃ info ┃
┡━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━╇━━━━━━┩
│ links │ 10 │ 1 │ 3 │
│ seo │ 2 │ 17 │ 2 │
│ a11y │ 10 │ 7 │ 0 │
│ perf │ 0 │ 5 │ 7 │
│ total │ 22 │ 30 │ 12 │
└──────────┴───────┴─────────┴──────┘
The numbers differ slightly from filesystem mode: a real Linux server cannot serve /Team/, so the case mismatches show up as plain 404s (broken-link); robots.txt is honoured so /private/ is skipped (robots-blocked); and pages that nothing links to and the sitemap does not list (/orphan.html) cannot be discovered over HTTP at all.
Baselines
$ sitecheck check examples/site --config examples/sitecheck.toml --write-baseline reports/baseline.json
wrote baseline with 68 issue(s) to reports/baseline.json
$ sitecheck check examples/site --config examples/sitecheck.toml --baseline reports/baseline.json --fail-on warning --quiet
...
68 issue(s) hidden by baseline
$ echo $?
0
Report formats
- JSON:
{tool, target, mode, summary, pages[], issues[]}; each issue hascode, severity, category, page, target, line, message. - JUnit: one suite per page, one testcase per issue; errors and warnings are
<failure>. - SARIF 2.1.0: upload to GitHub code scanning.
- HTML: a single self-contained file with a sortable, filterable issue table and a page list.
Configuration
sitecheck.toml controls crawl limits, external checking, budgets, SEO thresholds, per-rule severities (error|warning|info|off) and ignores. The commented example is examples/sitecheck.toml; every key is documented in docs/configuration.md. Every rule is described in docs/checks.md.
Architecture
sitecheck/
cli.py click commands, option merging, exit codes
config.py sitecheck.toml dataclasses and validation
rules.py rule catalog (code, category, default severity)
urls.py resolve/normalize URLs
parser.py html.parser state machine -> ParsedPage
contrast.py WCAG color parsing and ratio math
images.py PNG/GIF/JPEG pixel size from headers
fetch.py Fetcher base (redirects), FsFetcher, HttpFetcher (retries)
crawler.py async crawler, seeding (sitemap, disk, robots)
sitemap.py sitemap/sitemapindex parsing
external.py external link probing with host rate limits
model.py Resource, Page, Ref, Site, Issue
checks_links.py checks_seo.py checks_a11y.py checks_perf.py
audit.py orchestration, severity/ignore handling, baselines
report/ console, json, junit, sarif, html
tests/ 92 tests (parser, each check, HTTP crawler against a local server, reports, CLI)
docs/ checks.md, configuration.md, design.md
examples/ site/ (planted issues), sitecheck.toml, make_assets.py
Design notes and worked examples (URL resolution, contrast math, page weight, redirects, identity of / vs /index.html) are in docs/design.md.
Tests
.venv/bin/python -m pytest -q
HTTP-mode tests run against a throwaway http.server on an ephemeral port; no external network is used.
Limitations
- Static analysis only: no JavaScript execution, so links and images inserted client-side are invisible.
- Contrast is limited to inline styles; stylesheet rules are not evaluated.
- Page weight ignores external resources and does not follow
@importchains orsrcsetselection. - Filesystem mode does not apply
robots.txtand does not emulate clean URLs,_redirectsor server rewrite rules. - Fragment checks only work for pages that were crawled (inside
max_depth/max_pages). - External checks cannot distinguish bot-blocking (403/429) from dead links, so those are warnings.
- Sitemaps are read as plain XML (gzip-compressed sitemaps are not supported).