Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| sidebar_position: 12 | |
| displayed_sidebar: developersSidebar | |
| # Accessibility testing (jurisdiction websites) | |
| Bulk accessibility checks for canonical government homepages in **`intermediate.int_jurisdiction_websites`**. Results are stored in Postgres bronze tables for analysis, dashboards, and compliance reporting. | |
| This guide covers **how to read these docs**, **what each engine does**, and **exact commands to run** from the repo root. | |
| ## How to read this documentation | |
| ### On the web (local) | |
| 1. From the repo root, start the documentation site: | |
| ```bash | |
| cd website && npm install && npm run start | |
| ``` | |
| 2. Open the guide in your browser: | |
| - **Local dev:** [http://localhost:3000/docs/guides/accessibility-testing](http://localhost:3000/docs/guides/accessibility-testing) | |
| - If that 404s, try the site home at [http://localhost:3000](http://localhost:3000) and use the left nav: **Developers & Technical Users → How-To Guides → Accessibility testing**. | |
| 3. Production (when deployed): [https://www.communityone.com/docs/guides/accessibility-testing](https://www.communityone.com/docs/guides/accessibility-testing) | |
| ### In the repository | |
| | Location | Contents | | |
| |----------|----------| | |
| | This page (`website/docs/guides/accessibility-testing.md`) | User-facing runbook | | |
| | `packages/accessibility/src/accessibility/README.md` | Maintainer notes (env vars, file layout) | | |
| | `packages/accessibility/src/accessibility/*.py`, `*.mjs`, `*.sh` | Implementations | | |
| --- | |
| ## What gets tested | |
| | Layer | Source | Engines | Bronze table | | |
| |-------|--------|---------|--------------| | |
| | **HTML homepages** | One URL per `jurisdiction_id` from `int_jurisdiction_websites` | [axe-core](https://github.com/dequelabs/axe-core) + Puppeteer, [Pa11y-CI](https://github.com/pa11y/pa11y-ci) | `bronze.bronze_jurisdiction_website_accessibility` | | |
| | **PDF documents** | `.pdf` links discovered on those homepages | [veraPDF](https://verapdf.org/) (PDF/UA, PDF/A) | `bronze.bronze_jurisdiction_pdf_verapdf` | | |
| HTML tools check **WCAG-oriented** issues in the DOM. veraPDF checks **machine-verifiable** PDF/UA and PDF/A rules (industry standard for accessible PDFs). | |
| --- | |
| ## Prerequisites | |
| 1. **Postgres** with dbt intermediate built: | |
| ```bash | |
| ./scripts/dbt.sh run --select int_jurisdiction_websites | |
| ``` | |
| 2. **Python** (repo `.venv`): | |
| ```bash | |
| pip install -r requirements.txt | |
| ``` | |
| 3. **Database URL** in `.env` (first non-empty wins): `OPEN_NAVIGATOR_DATABASE_URL`, `NEON_DATABASE_URL_DEV`, or `NEON_DATABASE_URL`. | |
| 4. **HTML scans only — Node 18+:** | |
| ```bash | |
| cd packages/accessibility/src/accessibility && npm install | |
| npx puppeteer browsers install chrome | |
| ``` | |
| 5. **PDF scans — Docker** (recommended; pulls `verapdf/cli` on first use): | |
| ```bash | |
| docker --version | |
| docker run --rm verapdf/cli:v1.30.1 -l # list PDF/UA and PDF/A profiles | |
| ``` | |
| --- | |
| ## Quick start (recommended) | |
| Run everything from the **repository root** (`open-navigator/`). | |
| ### HTML: axe on one state | |
| ```bash | |
| ./packages/accessibility/src/accessibility/run_accessibility_scan.sh --engine axe --state AL | |
| ``` | |
| Exports URLs → scans with axe (default concurrency 5) → upserts `bronze.bronze_jurisdiction_website_accessibility`. | |
| ### HTML: Pa11y-CI with higher parallelism | |
| ```bash | |
| PA11YCI_CONCURRENCY=8 WORKER_POOL_SIZE=6 \ | |
| ./packages/accessibility/src/accessibility/run_accessibility_scan.sh --engine pa11y --state AL | |
| ``` | |
| ### PDF: veraPDF on one state | |
| ```bash | |
| ./packages/accessibility/src/accessibility/run_verapdf_scan.sh --state AL --max-pdfs-per-site 3 | |
| ``` | |
| Discovers PDFs on homepages → validates with PDF/UA profile `ua1` by default → upserts `bronze.bronze_jurisdiction_pdf_verapdf`. | |
| --- | |
| ## Step-by-step: HTML (axe or Pa11y) | |
| ### 1. Export homepage manifest | |
| One canonical URL per `jurisdiction_id` (same source priority as jurisdiction discovery): | |
| ```bash | |
| .venv/bin/python -m accessibility.export_urls --state AL \ | |
| --out data/cache/accessibility/urls-al.json | |
| ``` | |
| **Sharding** for large batches (~20k sites): | |
| ```bash | |
| .venv/bin/python -m accessibility.export_urls --limit 50 --offset 0 --batch-id shard-0 | |
| .venv/bin/python -m accessibility.export_urls --limit 50 --offset 50 --batch-id shard-1 | |
| ``` | |
| ### 2. Run a scanner | |
| **axe-core + Puppeteer:** | |
| ```bash | |
| cd packages/accessibility/src/accessibility | |
| AXE_CONCURRENCY=10 node run_axe_scan.mjs \ | |
| --urls ../../data/cache/accessibility/urls-al.json | |
| ``` | |
| **Pa11y-CI (worker pool):** | |
| ```bash | |
| cd packages/accessibility/src/accessibility | |
| PA11YCI_CONCURRENCY=8 WORKER_POOL_SIZE=6 WORKER_CHUNK_SIZE=25 \ | |
| node run_pa11y_workers.mjs --urls ../../data/cache/accessibility/urls-al.json | |
| ``` | |
| ### 3. Persist to Postgres | |
| ```bash | |
| .venv/bin/python -m accessibility.persist_results --ensure-ddl \ | |
| --scanner axe --input data/cache/accessibility/axe-<batch-id>.ndjson | |
| .venv/bin/python -m accessibility.persist_results --ensure-ddl \ | |
| --scanner pa11y --input data/cache/accessibility/pa11y-<batch-id>/pa11y-results-merged.json | |
| ``` | |
| ### 4. Query HTML results | |
| ```sql | |
| SELECT jurisdiction_id, website_url, scanner, violation_count, status, scanned_at | |
| FROM bronze.bronze_jurisdiction_website_accessibility | |
| WHERE batch_id = '20260515T120000Z' | |
| ORDER BY violation_count DESC NULLS LAST | |
| LIMIT 50; | |
| ``` | |
| **DDL (Neon):** | |
| ```bash | |
| psql "$OPEN_NAVIGATOR_DATABASE_URL" -v ON_ERROR_STOP=1 \ | |
| -f packages/hosting/scripts/neon/migrations/032_create_bronze_jurisdiction_website_accessibility.sql | |
| ``` | |
| --- | |
| ## Step-by-step: PDF (veraPDF) | |
| ### 1. Discover PDF links on homepages | |
| ```bash | |
| .venv/bin/python -m accessibility.export_pdf_urls --state AL \ | |
| --out data/cache/accessibility/pdf-urls-al.json \ | |
| --max-pdfs-per-site 3 | |
| ``` | |
| Use `--from-manifest path.json` to skip crawling if you already have a PDF URL list. | |
| ### 2. Download and validate | |
| **Default (Docker wraps veraPDF):** | |
| ```bash | |
| .venv/bin/python -m accessibility.run_verapdf_scan \ | |
| --manifest data/cache/accessibility/pdf-urls-al.json | |
| ``` | |
| **Multiple profiles** (e.g. PDF/UA-1 and PDF/UA-2): | |
| ```bash | |
| VERAPDF_FLAVOURS=ua1,ua2 .venv/bin/python -m accessibility.run_verapdf_scan \ | |
| --manifest data/cache/accessibility/pdf-urls-al.json | |
| ``` | |
| **Local veraPDF binary** (no Docker): | |
| ```bash | |
| VERAPDF_USE_DOCKER=false VERAPDF_BIN=verapdf \ | |
| .venv/bin/python -m accessibility.run_verapdf_scan \ | |
| --manifest data/cache/accessibility/pdf-urls-al.json | |
| ``` | |
| ### 3. Persist PDF results | |
| ```bash | |
| .venv/bin/python -m accessibility.persist_verapdf_results --ensure-ddl \ | |
| --input data/cache/accessibility/verapdf-<batch-id>.ndjson | |
| ``` | |
| ### 4. Query PDF results | |
| ```sql | |
| SELECT jurisdiction_id, pdf_url, profile_flavour, is_compliant, | |
| failed_rules, failed_checks, status | |
| FROM bronze.bronze_jurisdiction_pdf_verapdf | |
| WHERE batch_id = '20260515T120000Z' | |
| ORDER BY failed_checks DESC NULLS LAST | |
| LIMIT 50; | |
| ``` | |
| **DDL (Neon):** | |
| ```bash | |
| psql "$OPEN_NAVIGATOR_DATABASE_URL" -v ON_ERROR_STOP=1 \ | |
| -f packages/hosting/scripts/neon/migrations/033_create_bronze_jurisdiction_pdf_verapdf.sql | |
| ``` | |
| ### Docker worker (batch / Lambda-style) | |
| ```bash | |
| docker build -f packages/accessibility/src/accessibility/docker/Dockerfile.verapdf-worker \ | |
| -t open-navigator/verapdf-worker . | |
| docker compose -f docker-compose.verapdf.example.yml run --rm verapdf-worker \ | |
| --state AL --limit 50 | |
| ``` | |
| Inside the image, `VERAPDF_USE_DOCKER=false` — the `verapdf` CLI is already on `PATH`. | |
| --- | |
| ## Scale (~20,000 jurisdictions) | |
| | Approach | When to use | | |
| |----------|-------------| | |
| | Raise concurrency (`AXE_CONCURRENCY`, `PA11YCI_CONCURRENCY`, `WORKER_POOL_SIZE`) | Single powerful machine | | |
| | `--limit` / `--offset` shards | Multiple machines or cron jobs | | |
| | `packages/accessibility/src/accessibility/lambda_handler.py` | AWS Lambda + Step Functions | | |
| **Lambda event examples:** | |
| ```json | |
| { "engine": "axe", "state": "AL", "offset": 0, "limit": 50, "batch_id": "shard-0", "persist": true } | |
| ``` | |
| ```json | |
| { "engine": "verapdf", "offset": 0, "limit": 30, "batch_id": "pdf-shard-0", "persist": true } | |
| ``` | |
| Use a **container image** with Chromium for HTML engines, or `Dockerfile.verapdf-worker` for PDF. Target **10–50 URLs per invocation** so a full national batch finishes in minutes when fan-out across hundreds of functions. | |
| --- | |
| ## Environment variables | |
| | Variable | Default | Purpose | | |
| |----------|---------|---------| | |
| | `OPEN_NAVIGATOR_DATABASE_URL` | — | Postgres for export + persist | | |
| | `AXE_CONCURRENCY` | `5` | Parallel Puppeteer tabs (axe) | | |
| | `PA11YCI_CONCURRENCY` | `5` | URLs per Pa11y-CI process | | |
| | `WORKER_POOL_SIZE` | `4` | Parallel Pa11y-CI child processes | | |
| | `WORKER_CHUNK_SIZE` | `25` | URLs per Pa11y child | | |
| | `PA11Y_STANDARD` | `WCAG2AA` | Pa11y standard | | |
| | `VERAPDF_USE_DOCKER` | `true` | Run `docker run verapdf/cli …` | | |
| | `VERAPDF_FLAVOURS` | `ua1` | e.g. `ua1`, `ua2`, `1b`, `2b` | | |
| | `VERAPDF_WORKERS` | `4` | Parallel PDF download + validate | | |
| | `VERAPDF_MAX_BYTES` | `15728640` | Skip PDFs over 15 MiB | | |
| | `ACCESSIBILITY_USER_AGENT` | Open Navigator bot | HTTP User-Agent | | |
| --- | |
| ## Troubleshooting | |
| | Symptom | What to try | | |
| |---------|-------------| | |
| | Empty export | Run `dbt run --select int_jurisdiction_websites`; confirm rows in `intermediate.int_jurisdiction_websites` | | |
| | Chromium fails on WSL | `npx puppeteer browsers install chrome`; see meetings Playwright notes in discovery docs | | |
| | veraPDF “not available” | Install Docker or set `VERAPDF_USE_DOCKER=false` with local `verapdf` on PATH | | |
| | Many `download_failed` PDFs | Site blocks bots; increase timeout; check `ACCESSIBILITY_USER_AGENT` | | |
| | Timeouts on large states | Use `--limit` and shard with `--offset` | | |
| --- | |
| ## Related documentation | |
| - [Jurisdiction discovery](/docs/data-sources/jurisdiction-discovery) — how homepage URLs enter the warehouse | |
| - [dbt quick reference](/docs/dbt/quick-reference) — rebuilding `int_jurisdiction_websites` | |
| - [Quick start](/docs/quickstart) — general repo setup | |