Skip to main content

Overview

Site health is a checker over the draft, not the published site. It walks every page’s blocks, applies a set of rules, and writes what it finds to a durable store. The editor shows the result as a badge in the header and a panel grouped by page. Most of the time the panel should be empty, and that is the case it is designed for. A checker with something to say every day is one you stop opening. What is left is making the two or three things that are wrong immediately actionable.

What it checks

Rules read field metadata from the block manifest, never block type names. This matters to you if you register your own blocks: a rule naming Hero would silently exempt every custom block on your site. A block that declares an image field and an imageAlt field is checked exactly like a built-in one, whatever you called it. Rules are grouped by agent (seo, a11y, content) because reconciliation is per agent: a run of only the SEO rules must not mark this morning’s accessibility findings fixed without having looked at them.

When it runs

The draft tier is a few milliseconds of in-memory work and costs nothing, which is why publish is on by default — the moment content ships is when anybody cares whether it is broken. on_apply fires on every edit, so it is opt-in. An ambient linter should be something you turn on having decided to, not something you discover in a CPU graph.
Both automatic triggers are inert under NODE_ENV=test, so a test suite never has a background task writing findings into a store its assertions are reading.

Findings

A finding is the identity of a problem, not of an occurrence of it. Its fingerprint is sha256(scopeKey, slug, ruleId, key) and deliberately excludes the offending value — include the value and half-fixing a title produces a second finding instead of an updated one, orphaning the first and silently voiding the dismissal somebody made last week. Fingerprints are unique per scope, never globally: one site’s findings are invisible to another, and a finding id from one session cannot be acted on from another.

Status

fixed is reconciliation’s word, and a client cannot assert it — that would put a finding into a state the next run immediately contradicts. Reconciliation is the whole trick: a finding that a run stops emitting is closed, so nothing has to notice that you fixed something. It is bounded to the pages the run actually scanned, so a run over two pages never closes findings on the other forty-three. Snoozed findings are reconciled too. A snooze postpones the report, not the problem — otherwise something you deferred on Monday and fixed on Tuesday comes back on Friday as an open finding about a page that is fine.
There is no scheduler in the orchestrator process. A snooze expires on the next read of the findings list, which is the only moment its expiry is observable.

The panel

Open Site health from the stethoscope button in the editor header. The badge on it carries the count and the worst severity present. Page groups fold. Forty findings over nine pages is a scroll, not a list. Groups start collapsed, each carrying its count, so the panel opens as a contents page — which pages, how many problems each — and you open the one you want. What you fold or unfold is remembered per browser. Each finding names its block, not just a path. A page with three Card Grids produces three findings that all read cards[0].imageAlt; the block’s own heading is the only thing that tells them apart. Each finding offers three actions:

Go to

Selects the page, the block, and the field: the preview scrolls the block into view and outlines the offending field; the property panel opens it and marks it.

Snooze

Hides it for seven days, then shows it again if it is still wrong.

Dismiss

Hides it for good. Survives every subsequent run.

What “Go to” points at

A renderer emits data-editable-target for what it draws, and plenty of editable props are never drawn: alt text is an attribute on an image, a link target is an attribute on an anchor. The preview resolves the highlight in three steps, most specific first:
  1. The field’s own node, when it has one.
  2. For alt text, the image it describes — the only thing on the page that alt text is about.
  3. The nearest enclosing node: the card, the list item, the block.

Durability

Findings live in the same SQLite file as session state, in their own row-addressed tables (findings, check_runs) rather than in the snapshot-rewritten ones. They survive a restart, and so do dismissals — a checker whose dismissals evaporate is a checker people turn off in week two. If SQLite is unavailable, the store falls back to memory and keeps working, and every checks response carries a durable flag saying so. The panel shows a warning when it is false: findings written to the fallback look identical to durable ones until the process restarts, and a scheduled run is exactly when nobody is watching a log.

HTTP API

Available in both the standalone orchestrator and library mode (createOrchestrator()) — the logic lives in transport-agnostic actions, so a findings panel is never empty in one and populated in the other.
endpoint
Scan the session’s draft and reconcile. Body: session, siteId, optional slugs (restrict the scan — cross-page rules still see the whole site), and trigger. Returns the run record, the open findings, and durable.
endpoint
What is currently wrong. Query: session, siteId, optional slug, agent, status (comma-separated; defaults to open), limit.
endpoint
The ledger: every run with what it opened, what it closed, and what it cost.
endpoint
Dismiss, snooze, or reopen one finding. Body: session, siteId, id, status (dismissed | snoozed | open). A finding belonging to another scope returns 404.

Extending it

A rule is a small object with an id, an agent, a severity, and a run that receives the page, its flattened fields, and a view of the rest of the site. Add yours to DRAFT_RULES in packages/orchestrator-core/src/checks/rules-draft.ts. Two things to get right:
  • Read field kinds, not block types. ctx.fields gives you every field with its kind (text, richtext, url, image, imageAlt, enum, color, number, boolean, headingLevel), its editable path, and its container. Naming a block type exempts every custom block.
  • Make keys unique per block, not per page. A rule’s key becomes part of the fingerprint, and two findings from one rule sharing a key are one finding as far as the store is concerned. On a page with two blocks of the same type, a key without the block id loses the second finding.
A rule that throws costs its own findings and nothing else. A checker that goes dark because one custom block had an unexpected prop shape is worse than one that reports twelve of thirteen rules.

Publishing your changes

Health checks can run on apply and on publish — CHECKS_ON_APPLY and CHECKS_ON_PUBLISH.

Coverage checks

The other kind of number: whether the integration itself is complete.