Codync
免費、開源的 1:1 替代方案,取代 Grok Bot 與 Muse。從 iPhone、Mac 或 Linux 桌面將 Claude Code、Codex、Cursor 與 40+ 程式碼代理人作為持續機器人傳訊。群組聊天、主題串…
AI 翻譯
尚未收集儲存庫資訊。
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
WCAG 2.2 checks for small business websites with plain-language fixes (European Accessibility Act).
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗▶ Live demo in your browser · Case study · Part of Daniil Dushin's portfolio
Crawls a small website, runs 17 automated checks on the HTML of every page, maps each finding to a WCAG 2.2 success criterion, and says in plain language what to fix. Reports are stored in SQLite and can be downloaded as a standalone HTML file or as JSON. An optional Claude summary explains the result to a shop owner who does not know what "WCAG" means.
It is a first report, not an audit: static HTML checks cover only part of WCAG (see What it cannot check).
The European Accessibility Act, Directive (EU) 2019/882, applies from 28 June 2025 (Art. 31). Among other services it covers e-commerce services provided to consumers (Art. 2(2)(f)): selling online through a website or an app. The Directive asks for websites that are "perceivable, operable, understandable and robust" (Annex I) and gives a presumption of conformity to harmonised standards cited in the Official Journal (Art. 15). The European standard for this is EN 301 549; its version V4.1.1 (2026-09) was prepared for the Act and aligns its web requirements with WCAG 2.2 levels A and AA. Microenterprises that provide services (fewer than 10 people and at most EUR 2 million turnover or balance sheet) are exempt (Art. 4(5), Art. 3(23)).
A small online shop or a hotel with a booking form usually has no idea where its website stands. Professional audits cost money, and browser extensions such as axe check one page at a time and speak developer language. This tool crawls the whole small site in seconds, groups the findings by WCAG criterion, and gives one clear "what to fix" line per problem, so the owner has something concrete to hand to their web developer.
It does not say whether the law applies to a business or whether a site complies with it.
*/$ wildcards and Crawl-delay), page limit (default 20),
2 parallel requests with a 0.5 s pause, redirects followed hop by hop, status codes recorded.#777 on white is 4.48:1 and fails; #767676 is 4.54:1 and
passes.uv run python -m app scan https://example.com --max-pages 10.| Before: the broken sample shop | After: the fixed version |
|---|---|
flowchart LR
U[URL from the form,<br/>CLI or API] --> G{Private<br/>address?}
G -->|yes| X[Refused]
G -->|no| R[robots.txt<br/>RFC 9309]
R --> C[Crawler<br/>same origin, 2 workers,<br/>0.5 s pause, page limit]
C -->|HTML| P[BeautifulSoup + lxml]
P --> RU[17 rules<br/>WCAG 2.2 A/AA]
RU --> A[Summary per rule,<br/>per criterion, per page]
A --> DB[(SQLite)]
DB --> W[Web report]
DB --> E[HTML / JSON export]
DB --> L{Explainer}
L -->|offline| F[Fixed texts]
L -->|AI_PROVIDER=anthropic| CL[Claude]
Each rule returns how many elements it looked at and what it found. That gives three outcomes per page: fail (or warning for advisory rules), pass, and not applicable (no images on the page, say). A criterion takes the worst status of its rules.
| Rule | WCAG 2.2 | Level | Type |
|---|---|---|---|
Images without a text alternative (missing alt, file name as alt) | 1.1.1 | A | failure |
| Form fields without a label | 1.3.1, 4.1.2 | A | failure |
| Labels pointing to a duplicated id | 1.3.1, 4.1.2 | A | failure |
| Missing or empty page title | 2.4.2 | A | failure |
| Page language not declared or not a valid language code | 3.1.1 | A | failure |
| Links without text (icon links) | 2.4.4, 4.1.2 | A | failure |
| Buttons without text | 4.1.2 | A | failure |
<iframe> without a title | 4.1.2 | A | failure |
Viewport meta that blocks zoom (user-scalable=no, maximum-scale < 2) | 1.4.4 | AA | failure |
| Text contrast, where colour and background are both inline | 1.4.3 | AA | failure |
| Vague link texts ("click here", "mehr", "подробнее", "devamı" ... in 6 languages) | 2.4.4 | A | advisory |
| Fields labelled only by a placeholder | 3.3.2 | A | advisory |
| Several pages with the same title | 2.4.2 | A | advisory |
Heading structure: no <h1>, skipped levels, empty headings | 1.3.1 | A | advisory |
No <main> landmark and no skip link, or a broken skip link | 2.4.1 | A | advisory |
Audio or video that plays by itself without muted | 1.4.2 | A | advisory |
| Duplicate ids | 4.1.1 (removed) | – | advisory |
Notes on the less obvious ones:
<label for> or aria-labelledby points to
a duplicated id, the label may land on the wrong field, and that is reported as a failure under
1.3.1 and 4.1.2.aria-label that names the target passes.font-size; otherwise the message says that 3:1 would be enough if the text is large.
Semi-transparent text is blended over the background; a background image or gradient makes the
backdrop unknown, so that text is skipped rather than guessed.aria-labelledby,
aria-label, alt or <label>, the element's text (for links, buttons, headings), title.Static HTML checks see the markup, not the rendered page. They cannot judge:
A clean report is a good start, not proof of accessibility or of compliance with any law. Test with a keyboard and a screen reader (NVDA, VoiceOver), and use tools that render the page, such as axe DevTools or Lighthouse, for the rest.
Requires uv (it installs Python 3.12 itself).
cp .env.example .env
uv sync
uv run python -m app --demo
Open http://127.0.0.1:8108. In demo mode the app scans its two sample shops at start-up
(about 2 seconds each), so both reports are ready, and the shop-owner summary uses the fixed
offline texts. The sample pages themselves are at /samples/broken/ and /samples/fixed/.
DEMO_MODE=true in .env (or DEMO=1) does the same as --demo.
With Docker: cp .env.example .env && docker compose up --build (the Dockerfile and compose
file follow the other projects in this repository but were not run on this machine, which has
no Docker).
From the browser: enter the address on the start page. From the terminal:
uv run python -m app scan https://example.com --max-pages 10 --json report.json --html report.html
uv run python -m app scan --sample broken --explain # the offline demo, in the terminal
The CLI prints a summary per criterion and per failed rule, writes the exports, and stores the report in the same SQLite database as the web UI. Exit code 0 = scan finished, 1 = scan failed, 2 = the URL was refused.
Scan only sites you own or are allowed to test. The crawler identifies itself as
AccessibilityCheckBot and obeys robots.txt.
curl -X POST http://127.0.0.1:8108/api/scans -H "content-type: application/json" \
-d '{"url": "https://example.com", "max_pages": 10}'
# {"id": 4, "status": "running", "report": "/api/scans/4"}
curl http://127.0.0.1:8108/api/scans/4
All variables are in .env.example with comments.
| Variable | Default | Meaning |
|---|---|---|
DEMO_MODE (or DEMO) | false (true in the example) | Scan the sample shops at start-up; fixed texts only |
MAX_PAGES | 20 | Requests per scan (the form and CLI can change it, up to 200) |
CONCURRENCY | 2 | Parallel requests to the scanned site |
REQUEST_DELAY_S | 0.5 | Pause after each request; a robots.txt Crawl-delay overrides it |
REQUEST_TIMEOUT_S | 15 | Per request |
MAX_PAGE_BYTES | 3000000 | Larger pages are recorded but not analysed |
USER_AGENT | AccessibilityCheckBot/0.1.0 (...) | Sent with every request; robots.txt matches the first word |
ALLOW_PRIVATE_TARGETS | false | true allows 10.x, 192.168.x, 127.0.0.1 ... (own network only) |
MAX_PARALLEL_SCANS | 2 | Scans running at the same time |
AI_PROVIDER | fixed | fixed = offline texts, anthropic = Claude |
ANTHROPIC_API_KEY | empty | Empty = the SDK's own lookup (env variable or ant auth login) |
ANTHROPIC_MODEL | claude-opus-5-5 | Model for the summary |
ANTHROPIC_EFFORT | medium | low … max |
DATABASE_PATH | data/reports.db | SQLite file |
HOST / PORT | 127.0.0.1 / 8108 | Web UI address (0.0.0.0 in Docker) |
urllib.robotparser applies the first matching line
instead of the longest one and ignores * and $, which shop platforms use a lot
(Disallow: /*?sort=). The parser here follows RFC 9309: longest match wins, Allow wins a
tie, a missing robots.txt (4xx) allows everything, an unreachable one (5xx, network error)
disallows everything.http://shop.test usually redirects to
https://www.shop.test/; the start URL may move once, and that becomes the origin. Later
redirects to another site are recorded, not followed./ and /index.html serve identical HTML;
the crawler compares a SHA-256 of the body, so the home page's problems are not counted twice
(see BUGFIX.md, bug 1)..pdf, .jpg, .zip ...) are not
requested at all.#777 on white is 4.4776, shown as
4.48:1 (WebAIM's checker truncates and shows 4.47:1; both say it fails). A failing 4.497 is
shown as 4.49:1, never as a passing-looking 4.50:1. The decision uses the unrounded value.httpx.ASGITransport, so the crawler code path is identical to a real scan.I scanned my own portfolio site (https://site-production-898c.up.railway.app) with
--max-pages 15: 15 pages in 7.5 s, robots.txt found (Allow: /), the start page / redirected
to /en. Result: 0 failed checks, 0 warnings, 13 passed, 4 not applicable (no forms, no inline
colours and no iframes on those pages). That only means the markup basics are in place; keyboard
and screen reader testing is still to be done by hand.
[Result on a client's website]
app/
crawler.py same-origin crawler: robots.txt, limits, redirects, duplicates
robots.py RFC 9309 robots.txt parser
urls.py URL normalisation, same-origin rule, private-address guard
rules/ the 17 rules: base.py (criteria, accessible names), content.py,
forms.py, document.py; __init__.py has the registry with fix texts
contrast.py CSS colour parsing, relative luminance, contrast ratio
scanner.py one scan: crawl + rules + summary
report.py aggregation per rule and per WCAG criterion
explain/ shop-owner summary: fixed texts, Claude (anthropic_client.py)
service.py starts scans in the background, explanations, demo reports
web.py FastAPI routes; templates/ has the HTML (Jinja2, no JavaScript)
export.py standalone HTML and JSON export
samples.py serves the Sample Shop; sample_site/ has the broken and fixed pages
db.py SQLite storage (aiosqlite)
main.py CLI: serve, scan
tests/ pytest suite
docs/ screenshots
uv run pytest
uv run ruff check . && uv run ruff format --check .
208 tests (110 test functions, many parametrised) run in about 7 seconds, offline. They cover
every rule with passing and failing HTML, the contrast maths against known values, the robots.txt
parser, the crawler against fake sites on httpx.MockTransport (same origin, robots.txt, page
limit, status codes, redirects, concurrency and delay, Crawl-delay, duplicates, size limit,
private-address guard), the aggregation, full scans of both sample shops with the exact expected
findings, the Claude explainer with a fake SDK client (request shape, refusal, bad JSON), the
web UI and API, the CLI, and one test that runs the checker on the app's own pages: they pass
every rule.
See BUGFIX.md.