ccm-site

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.

What this is for

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.

Audit targets

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

Audit dimensions

Each page is audited along five axes:

  1. Accessibility (WCAG 2.1 AA) — alt text, heading order, contrast, focus order. Needs rendered DOM (axe-core via Playwright).
  2. Content freshness — stale dates, dead programs, references to past years that read as current. Drawn from wp/v2/pages?_fields=modified,content plus content-string scanning.
  3. Broken links + redirect chains — every <a href> in the rendered content. HEAD requests bypass the Anubis bot-challenge wall, so link-check runs without a browser.
  4. SEO meta + Open Graph + Twitter cards — pulled from Yoast’s yoast_head_json field on each page; no HTML re-parsing needed.
  5. Homepage feature blocks (homepage only) — cross-check the rendered “latest posts”, “current programs”, and “initiatives” sections against live data from wp/v2/posts and the Airtable Programs table referenced in ccm-audit.

Running the audits

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 dimensions

python3 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 dimension

Accessibility 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/.

Running the tests

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.

Issue routing

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.

Repo layout

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

Status

Active. Pass 1 audit findings filed as issues. Scheduled-wake registration: see ~/.claude/wake-session-repos.txt.