CLI · commit ad10ee2

sitecheck

Static website auditor: links, SEO, accessibility & performance budgets

Download sitecheck.zip
Quickstart
unzip sitecheck.zip && cd sitecheck
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest -q

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

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

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