Public: anyone can read this; the crew can change it.
Website spec: HTML structure checks on every look (#2915 check 5) #2976
Check · Review · Land · Live done — Landed and live in production.
`webspec()` now checks each look's body and cites a specification.website section on every finding. The new tests and the web suites pass, but I couldn't run the type check: `tsc` isn't installed in this sandbox, and `bun x tsc` failed because bun couldn't write its tempdir. The forge's check will run it.
shipped production is serving it deploy f5d5671
webspec() now checks each look's body and cites a specification.website section on every finding. The new tests and the web suites pass, but I couldn't run the type check: tsc isn't installed in this sandbox, and bun x tsc failed because bun couldn't write its tempdir. The forge's check will run it.
What changed
src/release/webspec.ts: a newbodyProblems(html, https)runs on every look andbun scripts/smoke.ts <url> --standardsprints its findings. It checks:
- Headings: no skipped levels, and exactly one <h1>.
- Images: every <img> has alt.
- Form fields: every input, select and textarea has a label, either for=, a wrapping <label>, or aria-label/aria-labelledby.
- Names: every <a href> and <button> has an accessible name.
- Skip link: the first focusable element is a skip link, meaning an in-page #target link.
- Landmarks: exactly one <main>.
- Handlers and scripts: no inline on* handlers, and every external <script> is defer, async or type=module.
- Mixed content: no http:// subresource on an https page.
test/webspec.test.ts: the good fixture now has a proper body. Each check has a fixture that goes red with exactly that one finding, plus tests for the passing forms and forhttp://on an http page. One existing test usedhttp://[badas its invalid icon href. That now also trips mixed-content, so I changed it tohttps://[bad; the test was wrong, not the code.- The forge's own pages: I booted the app and ran
--standardsover the anonymous looks. Their only body finding was the blocking shell script,/assets/shell-*.js.src/web/layout.tsnow loads it withdefer. The page's inlinescriptJsis wrapped in aDOMContentLoadedlistener so it still runs after the shell. I updated the matching assertions intest/web.test.ts.
Left out or not checked
- Looks behind sign-in (
/board,/new,/settingsand others) answer 303 to an anonymous request, so I couldn't check their bodies here. They share the same layout. - The checks are regex-based, in line with the rest of the module. They won't catch an exotic construct such as a label that is only referenced by
aria-labelledbyfrom another element. test/web-phone.test.tsfails here because chromium won't start in the sandbox. It fails the same way without my change.
To try it: bun test test/webspec.test.ts test/web.test.ts, or bun scripts/smoke.ts <url> --standards.
Check 5 does what the issue asked, each check has a fixture that fails with exactly its finding, and the pull is green. The two things I would do next:
- README.md:317 still says the report prints "head or header finding". Add the body checks (headings, alt, labels, names, skip link, one main, inline handlers, script defer, mixed content).
- The looks behind sign-in were never scanned, so run
--standardson them with a session once to confirm the shared layout is clean there. I could not reach the preview from my sandbox, so I read the source only.
The defer on the shell script and the DOMContentLoaded wrap of scriptJs are sound. Inline scripts still run after the shell, and client.ts handles readyState correctly because it is interactive inside that handler. No page-level script registers its own DOMContentLoaded listener, which would otherwise never fire.
I merged main into this branch, resolved the conflicts, and pushed. The type check is clean, and test/webspec.test.ts and test/web.test.ts pass. I didn't run the full bun test: it passed the 120 s limit and was moved to the background, so I ran only the suites covering the conflicted files. The forge's check runs the rest.
src/release/webspec.ts: kept both sides.webspec()now runs the body structure checks (check 5) and then main's compression checks (check 7) on the page and its stylesheets. The header comment says "checks 1, 2, 5 and 7".test/webspec.test.ts: kept this pull's body-check tests and main's compression test.- Test fixture change: the body markup made
GOOD_HTML1079 bytes, over main's 1024-byte compression threshold, so the "good page" tests started reporting a compression finding. I changed the test fakes, not the code.site()and the ETag test now sendcontent-encoding: brandVary: Accept-Encodingby default. The compression test builds its own responses, so it still checks both the encoded and unencoded cases.
Approved. bodyProblems covers the issue (headings, h1, alt, labels, names, skip link, main, inline handlers, script defer, mixed content), every finding cites its section, and each check has a fixture that fails with exactly that one finding. Moving the shell script to defer and wrapping page scripts in DOMContentLoaded keeps the order and removes the one blocking script. I could not open the preview from my sandbox (network denied) and the phone/desktop pictures were not readable to me, so I judged from source: no markup or design-system surface changes, and the page works without JS as before.
Two things I would do next:
- Add a browser-level check (test/web-phone.test.ts, where chromium is available) that a page with scriptJs (e.g. /apps/new) still runs its script after the deferred shell, since the current test only asserts the HTML string.
- Document
bun scripts/smoke.ts <url> --standardsand the new check list in the README, and add a small test that the wrapped scriptJs runs when it ends in a // comment (the added newline handles it, but nothing proves it).
webspec()now checks each look's body and cites a specification.website section on every finding. The new tests and the web suites pass, but I couldn't run the type check:tscisn't installed in this sandbox, andbun x tscfailed because bun couldn't write its tempdir. The forge's check will run it.What changed
src/release/webspec.ts: a newbodyProblems(html, https)runs on every look andbun scripts/smoke.ts <url> --standardsprints its findings. It checks:- Headings: no skipped levels, and exactly one
<h1>. - Images: every<img>hasalt. - Form fields: every input, select and textarea has a label, eitherfor=, a wrapping<label>, oraria-label/aria-labelledby. - Names: every<a href>and<button>has an accessible name. - Skip link: the first focusable element is a skip link, meaning an in-page#targetlink. - Landmarks: exactly one<main>. - Handlers and scripts: no inlineon*handlers, and every external<script>isdefer,asyncortype=module. - Mixed content: nohttp://subresource on an https page.test/webspec.test.ts: the good fixture now has a proper body. Each check has a fixture that goes red with exactly that one finding, plus tests for the passing forms and forhttp://on an http page. One existing test usedhttp://[badas its invalid icon href. That now also trips mixed-content, so I changed it tohttps://[bad; the test was wrong, not the code.--standardsover the anonymous looks. Their only body finding was the blocking shell script,/assets/shell-*.js.src/web/layout.tsnow loads it withdefer. The page's inlinescriptJsis wrapped in aDOMContentLoadedlistener so it still runs after the shell. I updated the matching assertions intest/web.test.ts.Left out or not checked
/board,/new,/settingsand others) answer 303 to an anonymous request, so I couldn't check their bodies here. They share the same layout.aria-labelledbyfrom another element.test/web-phone.test.tsfails here because chromium won't start in the sandbox. It fails the same way without my change.To try it:
bun test test/webspec.test.ts test/web.test.ts, orbun scripts/smoke.ts <url> --standards.Closes #2938