Maintenance repo for centerforcooperativemedia.org — the WordPress site for the Center for Cooperative Media.
This repo does NOT contain the site itself. It is a work queue for audit findings, content refresh tasks, and WordPress-level issues that scheduled-wake Claude sessions burn through one issue per session. The live site is on Nestify-hosted WordPress; edits land via the WP REST API or SFTP.
Scheduled-wake sessions on houseofjawn pick the stalest open GitHub issue from a small set of repos every two hours and work it for 15–30 minutes. Each session needs bounded, well-defined issues — not “review the homepage.” This repo turns “the CCM site needs ongoing maintenance” into a queue of single-page rollups with checkboxes a wake session can tick off.
Six pages anchor the audit, drawn from the primary nav at wp/v2/menus/12 (locations: primary, mobile). The live nav menu itself is not a page target — it is checked separately as a site-level audit (primary-menu), so a non-link item like the JS-only “Search the Archives” trigger is validated against the menu schema instead of read as a broken page link (issue #43):
| # | Title | URL | WP id | Notes |
|---|---|---|---|---|
| 1 | Homepage | / |
2863 | Page id, but feature blocks are rendered by the theme — content field does not reflect latest posts |
| 2 | About the Center | /about/ |
9421 | ?slug=about also matches the nested /civicscience/about/ (id 24392) — resolve by URL path, not bare slug (issue #29) |
| 3 | NJ News Commons | /njnewscommons/ |
TBD | |
| 4 | Resources | /resources/ |
TBD | |
| 5 | Events | /events/ |
TBD | |
| 6 | Research | /portfolio-item/research/ |
11440 | Portfolio-CPT permalink; resolves through wp/v2/pages?slug=research |
Each page is audited along five axes:
wp/v2/pages?_fields=modified,content plus content-string scanning.<a href> in the rendered content. HEAD requests bypass the Anubis bot-challenge wall, so link-check runs without a browser.yoast_head_json field on each page; no HTML re-parsing needed.wp/v2/posts and the Airtable Programs table referenced in ccm-audit.Two scripts produce the findings the rollup issues are built from. Both write a dated JSON snapshot to audits/ in the same schema, so file_issues.py rolls either up with no changes.
audit_ccm.py — REST + HEAD dimensionspython3 scripts/audit_ccm.py --pass <N>
Reads the six page targets via wp-json, runs freshness, broken-link, og-meta, and homepage-feature checks, plus a site-level primary-menu check of the live nav, and writes audits/<date>-pass-<N>.json. It emits an a11y placeholder finding per page — a deferral that audit_a11y.py fills in. Needs only requests and WP REST API credentials. Primary live-audit credentials are pass show claude/services/ccm-wp-audit-user plus pass show claude/services/ccm-wp-audit-app-password; pass show claude/services/ccm-wp-app-password is a temporary legacy administrator fallback until issue #30 is fully closed. HEAD and wp-json bypass the Anubis wall, so no browser is involved.
audit_a11y.py — accessibility dimensionAccessibility needs the rendered DOM, which means a real browser that solves the Anubis proof-of-work challenge. The script drives Playwright, injects axe-core, and runs the WCAG 2.1 A/AA rule sets against each target. --browser firefox (default, the documented Webwright toolchain) or --browser chromium — either engine solves the challenge and runs axe identically.
Dependencies — only the live scan needs them; the pure logic and the test suite do not:
pip install playwright
python3 -m playwright install firefox # or: chromium
axe-core is loaded into the page from a local file (--axe-js path/to/axe.min.js or $CCM_AXE_JS) or, by default, a pinned jsdelivr build (axe-core@4.10.2). A local copy is the offline/air-gapped option.
Two modes:
# standalone — write a11y-only findings to audits/<date>-a11y-pass-<N>.json
python3 scripts/audit_a11y.py --pass 3
# merge — replace each page's a11y placeholder in an existing pass JSON
python3 scripts/audit_a11y.py --merge audits/<date>-pass-<N>.json \
--out audits/<date>-pass-<N>-merged.json
Each axe violation becomes one finding (dimension: a11y) with severity mapped from axe impact: critical → p1, serious → p2, moderate/minor → p3. Evidence carries the axe rule id, WCAG tags, failing-element count, and a few sample CSS selectors. A page with no violations drops its placeholder and adds nothing. The primary nav menu is not a separate a11y target — it renders inside each page’s chrome, so the page scans cover it, and audit_ccm.py validates its link targets in the primary-menu site check.
Running it where it can reach content. A headless browser launched fresh from a server IP gets a WAF block (“Access Denied: error code …”), not the page. The script detects a block/challenge/error body and skips that target rather than scanning near-empty markup and reporting a false clean. A fresh context is incognito, so it never holds the Anubis proof-of-work cookie. Pass --storage-state path/to/state.json — a Playwright storage state exported from a browser session that already cleared the challenge (e.g. Webwright’s Firefox on houseofjawn, via context.storage_state(path=...)) — so the scan reaches real content. Without it, expect the targets to skip as blocked.
The mapping, merge, and envelope logic is covered offline by scripts/test_audit_a11y.py; the browser layer is not exercised in tests, matching the rest of scripts/.
The audit logic is covered by an offline test suite that lives beside the modules in scripts/. The same two checks gate every PR through .github/workflows/ci.yml, so install the test deps and run both locally before pushing:
pip install ruff pytest requests
ruff check . # lint — pyflakes + a pycodestyle subset (see ruff.toml)
pytest scripts/ -q # offline: no network call, no browser, no credentials
requests is in that list because audit_ccm.py imports it at module load, so the suite needs the package present even though the tests stub out every HTTP call. Tests sit next to the code they exercise (scripts/test_*.py) rather than in a separate tests/ tree, which keeps the import path equal to the module under test and makes the whole suite pytest scripts/. Playwright and the WP REST API credentials are only for the live audit scans (the section above) — the tests need neither.
CI is PR-only by design (on: pull_request): main is branch-protected, so every change arrives through a PR and the check job runs once per PR update. There is no push trigger because there are no direct pushes to protect.
One rollup issue per page. Each issue body is a markdown checklist — every finding is one checkbox. A wake session picks the next unchecked finding, fixes it (or files a follow-up when the fix needs Joe’s approval), ticks the box, and stops. The issue closes when every box is ticked.
This shape exists because scheduled wakes have ~15–30 minutes per session — single findings fit, whole-page rewrites do not.
ccm-site/
├── README.md — this file
├── CLAUDE.md — wake-session ground rules
├── .github/
│ ├── ISSUE_TEMPLATE/ — page audit, page refresh, WP issue
│ └── copilot-instructions.md
├── scripts/
│ ├── audit_ccm.py — audit driver, REST/HEAD dimensions (writes audits/<date>.json)
│ ├── audit_a11y.py — accessibility dimension (axe-core via Playwright)
│ └── file_issues.py — file/refresh one rollup GitHub issue per page from a pass JSON
├── audits/ — dated JSON snapshots from each audit run
├── data/ — cached wp-json snapshots (.gitignored except .gitkeep)
└── reference/
└── ccm-audit-notes.md — relevant findings mined from the archived ccm-audit repo
pages.centerforcooperativemedia.org). Separate surface, separate maintenance loop.Active. Pass 1 audit findings filed as issues. Scheduled-wake registration: see ~/.claude/wake-session-repos.txt.