Working inside the Rize coursedev system? The governing standard is
standards/interactive-pages.md in the Workbench repo, which says when an
interactive page is the right call at all and makes registration here a condition of
shipping. Read that first; this page is the how, that one is the whether.
The problem, stated precisely
Rize courses link out to externally-hosted HTML: Khan-Academy-style articles with embedded comprehension checks, simulations, practice exams, interactive graphs. They are pedagogically worth it, and they rot in ways that nobody notices until a student is stuck.
The failure mode that matters is not the 404. It is the page that returns HTTP 200 and is quietly empty.
A real example, found by the first run of this checker. A live ASM I practice exam rendered its title, its instructions, a countdown timer ticking down from 2:30:00, an answered counter reading 0/30, and a Submit button. It rendered zero of the thirty questions. One JavaScript error on load had dropped every item. A link checker calls that page healthy, because it is: HTTP 200, correct title, plenty of text.
So the check has to open the page in a real browser and look at what rendered. That is the whole idea. Everything below is mechanics.
What to check, and how hard to complain
The severity split matters more than the check list. A board that goes red every week gets ignored, and then it is worth nothing. Third-party links rot constantly and are usually not the course's fault, so they warn. Only things that break the page for a student fail.
| Check | Severity |
|---|---|
| The page loads: not a 404, not a timeout | Fail |
| Its scripts and stylesheets load | Fail |
| It rendered a real amount of visible text, not an empty shell | Fail |
| No uncaught JavaScript error on load | Fail |
The page's own window.__selfCheck() passes | Fail |
| Images, fonts and other subresources load | Warn |
| Outbound links to third-party material still resolve | Warn |
No dependency on a floating CDN version (mathjax@3, @latest) | Warn |
The idea that keeps this from becoming a system
Greg's condition was explicit: no bespoke solution that the team then has to maintain. The move that satisfies it is pushing the domain knowledge into the pages instead of the tool.
Each page exposes one function:
window.__selfCheck() // → { ok: boolean, checks: [{ name, ok, detail? }] }
The page author writes it while building the page, because they are the only one who
knows what "working" means for it. The checker calls it and fails the run on any
ok: false. The checker itself never learns anything about calculus, or about
any particular widget, so it never has to change. A new page inherits the check for free
by starting from the template.
What a good self-check looks like
SelfCheck.register('slider changes the slope', () => {
gap.value = '2'; update();
const wide = readSlope();
gap.value = '0.05'; update();
const tight = readSlope();
const ok = Math.abs(wide - 4) < 0.01 && Math.abs(tight - 2.05) < 0.01;
return ok || { ok: false, detail: `expected 4 and 2.05, got ${wide} and ${tight}` };
});
Check the things that fail silently, roughly one per interactive element: a visual
mounted, an input changes the output, every comprehension check grades correctly, and
data-driven content is present. Do not check prose or styling; a human notices those
immediately. The detail string lands verbatim on the status board that
instructors and managers read, so make it a diagnosis.
Be honest about the limit. A self-check verifies the page does what its own code says. If the answer key itself is wrong, the key and the grading logic agree and every check passes. That line, between mechanical health and pedagogical correctness, is the right one. Never let a green board stand in for a content review.
The procedure
-
One checker repo, not one per page
Pages live in many small repos. Put the checker in a single repo that checks all of them. Adding a workflow to each page repo is fifty copies of one file to maintain, which is the thing you were asked not to build.
gh repo create <org>/rize-interactive-health --public # copy in manifest.json, package.json, scripts/, template/, and # workflow/check.yml → .github/workflows/check.ymlThen in repo settings: Pages source =
mainbranch/docsfolder, and Actions → Workflow permissions → Read and write, so the run can commit the status board back. Optionally add aSLACK_WEBHOOKsecret and aHELPDESK_URLvariable. -
Build the manifest from Sanity, not from memory
The list of what to check comes from what courses actually link to, so it cannot drift. You run this query through the Sanity MCP; CI never holds Sanity credentials.
*[defined(richTextContent)]{ _type, "c": course->code, "h": richTextContent[].markDefs[].href }{ "k": _type + "~" + coalesce(c,"?") + "~" + array::join(h[string::startsWith(@, $prefix)], ";") }[k match "*github*"]{k}Use
string::startsWith, nevermatch, on these hrefs. GROQ'smatchreturns true against the null hrefs insidemarkDefsand will silently select every document in the dataset. It looks like it worked. It did not.Save the rows as
COURSE|URLlines and runpython3 scripts/build_manifest.py --rows rows.txt. URLs that drop out of Sanity are markedretiredrather than deleted, because an unwiring is sometimes a mistake and a silent deletion hides it.Keep the exact URLs courses link to, including deep links and query strings (
/macro-interactives/u1.html,/formula-trainer/?unit=3). A deep link can 404 while the repo's home page is perfectly fine. -
Schedule it around terms, not just weekly
Weekly year-round, plus daily through December, January, July and August, so a break surfaces weeks before students arrive rather than during week one. That daily-in-the- run-up cron is the part that answers "ahead of upcoming semesters"; a plain weekly cron does not.
-
Break something on purpose before you believe it
Point one manifest entry at a URL that does not exist, run the checker, confirm it goes red and names the page, then put it back. A check nobody has seen fail is not yet known to work. Do the same for a self-check: comment out the line that draws the graph and confirm the run fails with a useful message.
-
Hand over three things
The status board URL for the instructor briefing folder, with one line: if a page here is red, or a page looks wrong while you are teaching, file a helpdesk ticket and say which page. The page template, to start every new page from. And one sentence for their manager: it runs weekly and before each term, it costs nothing, and nobody has to remember to run it.
Instructors are the backstop for what the robot cannot see. They are not the primary detector, and a status indicator that is green because nobody has looked is worse than no indicator.
Standing advice when you build these pages
- Write the self-checks in the same session as the page. Part of building it, like alt text. Not an optional extra offered afterwards.
- Vendor every library; no CDN. A page with no external runtime dependency can only break when somebody edits it, and that is caught on the next push. Pinned is better than floating; vendored is better than pinned.
- No API keys. A key that expires on a schedule nobody tracks is the single most reliable way to have a dead page in week three.
- Put the pages repos in the Rize org, not a personal account. The eventual handoff to a product-team repo becomes a transfer rather than a rebuild.
- Do not tighten the warnings into failures. Tempting, and it is how the board becomes noise.
Where everything lives
| Path | What it is |
|---|---|
manifest.json | What gets checked, and which course each page serves |
scripts/check.mjs | The checker. Playwright, about 240 lines, the whole engine |
scripts/render_status.mjs | Builds the status board from results.json |
scripts/build_manifest.py | Regenerates the manifest from a Sanity inventory |
template/interactive-page-template.html | Start every new page from this. Khan-style article with comprehension checks and self-checks wired in |
SELFCHECK.md | The page-author contract, in full |
FOR-CLAUDES.md | The same procedure as this page, in the repo, with the gotchas |
The local source of truth on Samuel's machine is
projects/coursedev/interactive-health/.