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 namingHero 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 issha256(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 readcards[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 emitsdata-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:
- The field’s own node, when it has one.
- For alt text, the image it describes — the only thing on the page that alt text is about.
- 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 anid, 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.fieldsgives you every field with itskind(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
keybecomes 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.
What to read next
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.