The checks
A green build does not mean the pages are fine. This toolchain places one check at each layer, each catching a class of "build green, page broken" silent failure — and two of them look at pages the way a reader does, in a real browser.
§1Five gates, five layers
| Check | Ships with | Looks at | When to run |
|---|---|---|---|
| check-content | engine, scripts/check-content.mjs | every md/mdx source file | content-repo CI, or after writing |
| check-wikilinks | engine, scripts/check-wikilinks.mjs | the [[wikilink]] graph | content-repo CI, or after renaming a note |
| check-dist | engine, scripts/check-dist.mjs | the astro build output | after every build (postbuild) |
| ui_probe | this package, scripts/ui_probe.mjs | pages rendered in a real browser | after style/layout changes |
| contrast_probe | this package, scripts/contrast_probe.mjs | every text node's contrast, both themes | after any token change |
§2check-content: the source layer
Compiles every md/mdx headed for a page with the exact same dialect as the site — syntax errors and silent deformations (emphasis markers that cannot pair, braces swallowed by MDX evaluation, list markers born from line wrapping, single-line $$, formulas KaTeX cannot render — the same set the kitchen-sink closes with) all turn CI red.
Why this script must come from the engine: a checker whose plugin set differs from the site's is worse than no checker — one missing the math plugin misreads formula braces as JSX expressions, one without GFM waves tables through. The dialect is written once in the engine; site rendering, CMS save validation and this script consume the same one, always.
node <engine>/scripts/check-content.mjs . --glob '**/index.{md,mdx}' --mathBeyond compiling, it catches two frontmatter classes of silent loss: an unquoted # in a value (YAML reads it as a comment and silently truncates the rest) and YAML that does not parse at all, reported with its line.
§3check-wikilinks: the link graph
Dead [[wikilinks]] deliberately do not fail the build — a garden must be allowed to link to notes it hasn't grown yet. But link rot still belongs in CI, so the engine ships a lint that resolves every wikilink with the library's own parser and resolution rules (alias, brand, title, locale mirrors) and reports the missing, the ambiguous, and the dubious anchors. --strict turns dead links into a failing exit — that is how this repository's CI runs it:
node scripts/check-links.mjs§4check-dist: the output layer
In the built dist/, every internal reference a reader can click must actually exist. It catches the silent gaps beneath a green build:
- internal links/assets pointing at files that don't exist (the kind that appears by the dozen after a route reshuffle);
- in-page anchors pointing at ids that don't exist;
- doubled locale segments in paths (
/en/en/— the classic result of i18n fallbacks stacking a prefix onto already-prefixed routes); <a>nested inside<a>(the HTML parser closes the outer one early and buttons fall out of their cards);- KaTeX error residue (the formula is red text on the page while the build stays green).
This demo wires it into postbuild: a green npm run build means the output check passed too.
node vendor/astro-inkbrush/scripts/check-dist.mjs dist --base ${DEMO_BASE:-/}§5The render-layer probe
Source and output can both be right while the page is still broken — the classic case being landing cards that no stylesheet rule matches, rendering as one crammed line of bare text: link and anchor checks stay green because they never look at a rendered page. ui_probe drives a real browser over every page in dist at four viewport widths (1440/1024/768/430) and measures: horizontal overflow of the page, elements wider than their container with no scroll box to live in, classes no stylesheet rule styles, skipped heading levels, duplicate ids, images without an alt attribute, in-page anchors and aria-controls that point at nothing. It reports only what a machine can prove — no aesthetic judgment.
npm run build
node ../scripts/ui_probe.mjs dist # serves dist itself; pass a baseUrl to probe a live serverIt inspects the whole document — chrome, sidebar and dialogs included.
Green means the report's last line reads SAMPLES WITH FINDINGS: 0 (a
sample is one route at one width).
§6The contrast probe
The tokens claim AA, so the claim is measured rather than asserted. contrast_probe loads every page of dist in a real browser — light and dark theme, desktop and phone width — and measures every text run rendered in the default state: HTML text, SVG text and the generated text of ::before / ::after, with every <dialog data-probe-open> overlay probed open (and a query typed into a search box found there) on a representative page — the marker is the site's declaration that the dialog is complete as authored. The ground is not read from a stylesheet; the page is rendered with every glyph made transparent, screenshotted, and the pixel under each run is its ground, so gradients, color-mix() tints, translucent layers and the night palette are all measured as they render. A run's foreground carries the accumulated opacity of its element and ancestors, so dimmed text is measured at the strength a reader actually sees. Hover and focus states are reviewed, not probed. Small text is held to 4.5:1, large text (24px, or 18.66px bold) to 3:1; a run whose color cannot be parsed or whose ground cannot be sampled counts as a finding. The report names the page, theme, width, selector, the two colors and the ratio of every run under the bar:
node ../scripts/contrast_probe.mjs dist # PROBE_THEMES / PROBE_WIDTHS narrow the matrixui_probe and contrast_probe over the output — all green before committing. The demo doubles as the package's test bed.