Skip to content

Quality · the audit's own record

What we got wrong

28 repairs, each one a claim this site made that its own audit found false: a number typed where a count belonged, a check described that nothing ran, a rule three files disagreed about. The rows below are parsed from the ledger's repair entries (502–530 in the open book), not retold — the same bytes the repository versions are the bytes this page prints.

Site audit repair — headings

ledger row 502

every page carries exactly one `<h1>`: the demo scene rendered inside component previews was an `<h1>`, which turned a template page into an eleven-heading document; it is a styled `<div>` now, and the harness fails if any sitemap page has 0 or 2+

Site audit repair — dead links

ledger row 503

three links inside guide bodies pointed at routes that do not exist (`/learn/print-inspired-editorial`, `/prompts/easing-cheatsheet-deep-dive`, `/learn/motion-spec-only`); retargeted to the component/prompt pages that do exist, and the harness now fetches all 340 distinct internal links

Site audit repair — metadata window

ledger row 504

descriptions were clamped at 300 characters, which a search result truncates anyway: the clamp is 200 with a word-boundary ellipsis, two over-long prompt titles were shortened, and `/community` (no canonical — it inherited the homepage) now declares its own

Share cards for every family

ledger row 505

`/og/<slug>` covers prompts, guides and backgrounds in addition to components, each card built from that record's own numbers; a page without a bespoke card inherits the site card at `/og/default`, so no page unfurls as an empty frame

Regression gates for the above

ledger row 506

`check:exports` walks the sitemap (279 pages: status, single h1, own canonical, og:image, description length) and fetches every distinct internal link, plus one card check per family — the audit that found these problems is now the check that prevents them

Markup accessibility repair

ledger row 507

the automated half of a11y, run over every built document: fixed 134 pages whose logo gradient carried the same id three times per document (header, footer, press kit), every mock heading inside demo scenes and card previews, six panel titles that rendered as h3 directly under a page h1, four unlabeled lab/palette controls, and the badge page's per-card SVG ids

/quality/aria — the audit page

ledger row 508

the seven decidable checks (alt text, control names, label association, duplicate ids, heading order, document language) with live counts, the real findings from the first two runs and the repairs they caused, and an explicit list of what markup alone cannot decide (focus order, contrast in context, screen-reader output)

Markup gate in the export harness

ledger row 509

the same seven checks run from outside the build over all 291 documents and fail on any finding, so /quality/aria cannot report green while the served HTML is not; the page renders on demand precisely so its count is the finished build rather than a mid-build slice

Invented movement removed from the homepage

ledger row 510

the hero stat band printed "+8 this drop", "+2 this week", "+0.6 pt" and "+12.4%" as literals, and the trending list printed `+{((copies * 7) % 13) + 5}%` — a percentage manufactured from the copy count by an arbitrary formula; nothing in the repository records a change over time, so both were invention, not measurement. Each tile now carries a fact derived from the same data as its number (MIT count, models per prompt, recorded runs, dependency count and size), the copies tile says "catalog counter, all time — not a 30-day measurement", and the dead `COMMUNITY_STATS` block (612 assets / 318 prompts, contradicting the 133 / 74 catalog) is deleted

Demo copy separated from catalog data

ledger row 511

demo scenes printed numbers about this catalog as string literals — "300 verified prompts" beside a catalog of 74, "62 assets" beside 133, "39 components", and a scene headed "motif by the numbers" whose four figures (128 assets, a 4.2k-star launch week, "300+ teams", a 1.9s demo load) were all invented. Every catalog figure in a scene is now read from the catalog at build time (assets, verified prompts, guides, recorded runs, a11y median, median bundle size), the playground chrome carries a "sample copy" chip and every component FAQ answers "Are the numbers inside the demo real?", and the export harness fails if any scene file hard-codes a catalog count or a social-proof figure

Per-route JavaScript budgets

ledger row 512

`src/lib/budgets.ts` sets an own-JS limit per route family with roughly 10% headroom (component detail 730 KB, admin consoles 620, the lab 560, catalogs ~500, content pages 90 by default) and `/quality/speed` publishes the same table; the export harness applies it to all 141 measured routes and fails when one exceeds its limit. Building the table exposed the structural cost: `demos/Demo.tsx` is one 380 KB module holding every scene, so a page rendering one demo pays for all of them — the budgets for `/pricing`, `/shuffle` and `/lab/layers` say so in their `why` field, and the scene-by-scene split is named as the next measured win rather than claimed as done

One rule for what counts as a page

ledger row 513

three tools each decided for themselves which files in `.next` are pages, and the three answers disagreed. The trigger was in the export harness: a batch-73 check asked for a changelog slug that had already been retitled, and ORed its assertion with a second slug, so it passed for twenty batches while testing nothing — and because a cold Next server caches the not-found result as an `__next_error__` document under `.next/server/app`, each run wrote a phantom document. The measurement counted it as a page (inflating the HTML total and the font denominator) and `/quality/aria` reported it as a live `no-lang` finding against a URL that is not a route; the harness skipped it silently. The rule now has one home in `src/lib/markup-a11y.ts`, read by the page, `npm run check:a11y` and the report, which publishes the count it skipped; the harness reads a real slug off the changelog page and cleans up the one 404 it probes; and a `prebuild` step clears the prerender output so a build can only mean what that build wrote

`npm run lint` was red and called green

ledger row 514

the gate was reported clean while `eslint` exited 1 on five errors: `scripts/check-exports.cjs` and `scripts/check-demos.cjs` were parsed as TypeScript, where `require()` is forbidden. Both are Node scripts with no app code, so they are now `.mjs` with `import` statements, matching `measure-build.mjs`; two dead locals the linter also found (`catalogWords` in the harness, an unused `fs` import on `/quality/aria`) are gone. `eslint` now prints nothing at all, which is the actual definition of the green that was claimed

The demo scenes checked against their own markup

ledger row 515

the pass covered names and ids but not the wiring inside a widget, so four rules were added and three real defects fell out: `role="switch"` sat on a `<span>` in the admin settings demo (a switch that cannot be reached by Tab is not a switch, and its `aria-checked` disagreed with its own colour), and the combo-box and disclosure-list scenes both pointed `aria-controls` at a panel that only existed while open — a reference to nothing in the state a screen reader reads first. The listbox and the FAQ panels now stay mounted and are `hidden` when closed, the switches are real buttons (the one flag the demo wires is operable, the rest are disabled and say so), and the rules are: a focusable control inside an `aria-hidden` subtree, an `aria-*` reference to an id that does not exist, an operable role on something that cannot take focus, an ARIA state value the spec does not allow, and a role missing the property it requires. The same rules now also run over HTTP (`npm run check:a11y:served`) across all 283 sitemap URLs, which is the only way the 133 on-demand component pages ever get checked

A manual checklist where the automation stops

ledger row 516

the pass is green, and green is exactly when a checklist is worth writing, because the list of what a machine cannot decide is otherwise folklore. `/quality/aria` now carries seven ordered checks — a Tab walk with no mouse, closing an overlay while focus is inside it (the runtime case the static `hidden-focusable` rule cannot see), where focus returns after a dialog, focus rings against the surface they sit on, what a screen reader actually says, reduced motion scene by scene, and touch targets at 200% zoom — each naming the automated half that exists and the pages to run it on. The section says in the same breath that none of the seven has been run in a browser by this project, because the build has no browser in it: a checklist is a plan, not a result. The old sentence calling the demo scenes exempt from the pass is gone, since the rules now run over them

The demo scenes load per page

ledger row 517

every one of the 162 scenes lived in a single 380,911-byte module, so a page that rendered one demo paid for all of them: the component detail route measured 1,112.4 KB of JavaScript in the build report, and `/quality/speed` had carried that cost for three batches as "the one structural cost these budgets expose" rather than fixing it. The scenes are ten modules now — the registry in `src/components/demos/Demo.tsx`, the shared kit, seven scene sets under `scenes/` and `scenes-17.tsx` — behind a loader map of 162 static `() => import(...)` entries, each resolved through `dynamic(..., { ssr: true })` so the scene is still in the server-rendered HTML. Measured: a component page 1,112.4 → 696.2 KB, an embed 866.2 → 54.2 KB, the homepage 916.0 → 499.9 KB, with the shared shell unchanged at 446.2 KB; chunk files 48 → 58, because a lazily-loaded module is its own chunk. The guards moved with the code: `check:demos` verifies that every loader names a component its module exports and that no export goes unclaimed instead of counting `case` lines, the demo-copy gate scans all ten of them, the build report's baseline is the file set the most routes share rather than the lightest route (a lazy embed page had made "own JS" on /quality read as 422 KB of shell), and every budget was re-cut from the same measurement

Copy the scene split invalidated

ledger row 518

the refactor made five sentences false on pages whose whole argument is that their numbers are checkable: `/brand/voice` used the demo module as its example of a countable fact and printed "13 files and 428 KB of JS", which was neither true after the split nor checked by anything; `/lab/layers` said it "ships the demo module" when it now pulls one scene set; `/perf/no-js` and `/integrations/vscode` still spoke of a single demo module; the split-panel note in `src/components/perf-ui.tsx` listed "the demo module" among the per-route chunks. The voice line is read from the source tree now (`motionAudit()`), like the figure on /lab/layers, and the export harness compares both served pages against the files — "11,437 lines across 10 modules" — so the next refactor fails in the commit that makes it false instead of leaving it to be noticed

The crawl surface now matches the rule it states

ledger row 519

the crawl rule lived in three places that had to agree by hand — the sitemap in `src/app/sitemap.ts`, the `Disallow` list in `src/app/robots.ts` and the prose on `/quality/crawl` — and nothing compared them. The audit found the consequence: **103 indexable pages were absent from /sitemap.xml** (the whole studio log and its 13 entry permalinks, `/brand/*`, `/perf/*`, `/pro/*`, `/integrations/*`, `/community/*`, `/lab/layers`, `/studio`) and, on five surfaces the page called "kept out of the index", the only thing keeping them out was a robots.txt rule — which asks a crawler not to *fetch*, not to forget, so a linked URL can still be indexed. Two more pages were indexable and unlisted but invisible to a disk walk because they render on demand: `/quality/aria` and `/roadmap`, plus the two `/community` form pages and `/roadmap/embed`. The rule is one module now (`src/lib/crawl.ts`): robots.ts compiles its exclusion list, the sitemap appends its page list, and the studio log is derived from `CHANGELOG`; the five excluded surfaces carry `robots: { index: false }` (the admin console gets it from its layout); the changelog entry pages got the metadata every other page has — a 200-character description instead of a raw 280-character slice, and their own share card as the fifth `/og` family; `/quality` and `/es` got their descriptions clamped and the site card back. Measured: 278 → **382 sitemap URLs**, the build itself unchanged (141 routes · 291 documents · 58 chunks · 2,791.2 KB), the served markup pass 283 → 384 URLs with no findings, `/quality/aria`'s typed-in "283 URLs" replaced by the count the pass walks. Guarded by five new checks in `check:exports` — the exclusions are readable (8 rules), every built page with a canonical and no noindex is listed, every excluded surface carries noindex, every static route in the build manifest is either listed or noindex (133 routes, both directions, which is what catches on-demand pages), and `/quality/crawl` prints the count the sitemap serves. A deliberate dead link on `/brand/mascot` (the live-404 demo, newly visible to the link walk now that `/brand/*` is listed) is named in the harness and asserted to still return 404

Titles and descriptions had no owner

ledger row 520

the sitemap audit in 519 made every page reachable to the harness, and the next pass compared the pages with each other: **93 of the 382 served titles read "… — Motif UI · Motif UI"** because 103 page metadata blocks wrote the brand suffix that the root layout's title template already appends, **twelve hub pages shared one description** inherited from the layout (`/`, `/components`, `/prompts`, `/backgrounds`, `/learn`, `/lab`, `/pricing`, `/mission`, `/samples`, `/shuffle`, `/studio`, `/templates`), and four changelog entry titles ran past 75 characters once the template was applied. The per-page rule in the harness could not see any of it: it checked that a description existed, never that two pages disagreed, and a page inheriting the layout copies passed every check. The suffix is stripped from the 103 metadata titles (the layout adds it once), the twelve hubs got their own titles and descriptions written from their own content, the changelog entry title dropped its "— studio log" tail, and the shuffle deck is titled "Shuffle the library" rather than "Get inspired — Motif UI shuffle". Guarded four ways, all derived from the served pages: no title carries the brand twice, no title is longer than 75 characters, no two pages share a title, no two pages share a description

The social card was inherited whole

ledger row 521

with the sitemap honest, the next pass read each page's preview card against the page itself: **101 of the 382 sitemap pages inherited the root layout's `openGraph` block**, so sharing `/perf/fonts` or `/brand/voice` produced a card titled "Motif UI — Copy less. Ship more.", described with the site's own sentence, and linking to the homepage while the page's canonical pointed at itself. Every per-page rule missed it because every one of those pages had its own title, description and canonical — the wrong values lived one level up, in the file that sets defaults. The layout no longer sets `title`, `description` or `url` in `openGraph` (type, site name and the fallback image are not page-specific and stay), and the framework fills the two textual fields from each page's own metadata: 382 cards now carry the page's title, and a page that emits no `og:url` is read by platforms through its canonical instead of through the homepage. Also verified in the same pass: all 285 URLs referenced by JSON-LD resolve 200, and three new harness checks hold the line — no page advertises another page's URL in its card, no page borrows the site tagline as its card title, and a sample of the 281 distinct card images resolves to an image

The cache rules were not the rules the server sent

ledger row 522

`/perf/caching` printed a table of Cache-Control rules and told the reader the harness checked two of them every batch; nothing checked any of them, and the table had drifted from the wire. Measured on the batch-89 build: `/_next/static/chunks/*.js` answered `public, max-age=0, s-maxage=300, stale-while-revalidate=86400` — **0 of the 58 fingerprinted chunk files were immutable** — `/embed.js` answered the same while its route sets `max-age=3600` (a claim printed on `/roadmap/embed`), and `/community/rss.xml` and `/api/brand/<slug>` each lost the header their own handler sets. The cause is order: Next applies every matching rule and the last one wins, and `CACHE_RULES` listed the catch-all last, so it won for every path; the comment above the list asserted the opposite reading of the same precedence. The catch-all is first now and every path with its own policy has a rule after it — `/_next/static` (immutable, verified on the wire), `/embed.js` (3600), `/community/rss.xml`, `/api/brand/*`, `/og/*` (3600 each) — while `/_next/image` documents the optimizer's own `max-age=14400, must-revalidate`, which a config rule does not override. Guarded: the harness reads `CACHE_RULES` out of the module `next.config.ts` imports, asks for every rule over HTTP (7/7 match), and fails if the catch-all stops being first — which is the bug, and no header comparison can see it if the order flips back

Four claims about the harness that the harness did not back

ledger row 523

The audit turned on the site's own promises about itself. `/quality/aria`'s manual checklist said "the demo harness checks that each scene declares a reduced-motion branch" — nothing read the scenes for it, and **63 of the 102 scenes that animate name no reduced-motion branch at all**; the same page said "the three scenes that handle `prefers-reduced-motion` are tested by the demo harness in the same sense — markup and state, not a real key press" — the count was stale by an order of magnitude (39, not three) and none of them were tested. That page's whole argument is that its automated half is real, so the fix measures instead of asserting: a new `src/lib/motion-audit.ts` reads the scene modules and counts 162 scenes · 102 that animate · 39 that name the preference, the checklist item reports that count and says the other 63 are hand-only, the runtime sentence reports the same numbers, and `check:demos` reads the module three ways — its scene count against the loader keys, a floor of 39 so the number can only rise, and the served `/quality/aria` printing the string the audit measured, so page and harness cannot disagree. Two more sentences went the other way and were made weaker rather than stronger: "the demo harness asserts the behaviours a scene declares" is a comparison of the declared list with the catalog's list, not an assertion about code, and `/integrations` said the harness "checks that the bytes it returns are the bytes the page printed" — it fetches every endpoint and parses both served scripts, but the page comparisons are five specific ones (token colours present, the print block present, the catalog against /quality's counted total, the feed against the entry list, each badge against its stored score). Both sentences and the harness's own docstring now name what happens. `/perf/caching`'s "the same two checks" became the thing the gate actually does: compare every served header with the documented rules. The demo guard's header also said "nine modules" for the ten the split produced

The recorded numbers on /perf/chunks had lost their labels, and one was a typed-in literal

ledger row 524

`SplitPanel` shows a before/after pair measured on real builds, and the panel printed neither build's name — while `/quality/speed` prints the same pair with `batch 55 (commit 1fb5142) → batch 60` beside it, and the changelog entry for the same improvement quotes a third figure (427.6 KB) from its own build. The numbers are all honest measurements; the page just did not say which builds they came from, so a reader could not reconcile them. The literal was worse: the panel said `keyframes.tsx (41 lines)` and the file has been 40 lines since the batch that touched it. Both fixed at the source — `MEASURED.keyframesLines` is the count and the panel prints it, the panel's stat and code block name `${before.build}` and `${after.build}`, and the note now says the 7,416-line single demo file and the 80 routes are records of that build rather than the current layout. `check:exports` gained two checks: the file's real line count must equal the number on the page, and the served panel must print both build labels. Verified by lowering the constant to 39 and watching the gate fail

32 scenes drove motion from JavaScript and none of them asked about reduced motion

ledger row 525

The 100-suggestion bank opened with the 63 animating scenes the batch-91 audit had found without a branch. Measuring them properly changed the number and the rule. `globals.css` already collapses every CSS animation and transition under `prefers-reduced-motion: reduce`, and the built stylesheet carries it — so the 88 scenes whose motion is a class or an inline `animation:` were always covered without naming anything, and demanding a literal from them would have been ceremony. What was not covered is the motion JavaScript writes itself, where a media query has no say: **32 scenes** drive values from `requestAnimationFrame`, `setInterval`, an `IntersectionObserver` or a scroll/pointer listener, and **all 32 ran at full speed for a reader who had asked for less**. Three of them were caught by the stricter rule rather than the old one: `TocSpine`, `StickySubNav` and `BackToTop` (plus `ReadingDots`) call `scrollTo({ behavior: 'smooth' })`, which overrides the stylesheet's `scroll-behavior: auto`, so smooth scrolling was unswitchable. `TextCycle` was worse than unguarded: its own caption read "reduced-motion safe" while a 2.6s interval rotated the headline for everyone. The fix is one shared `useSceneMotion()` in scene-kit.tsx (plus `useReducedMotion()`), each of the 32 scenes deriving a still version of itself rather than writing state from an effect — count-ups print their final numbers, the odometer shows its target, the scramble never reshuffles, the upload jumps to done, the three scroll jumps land instantly, and the two countdowns keep ticking because a stopped clock is wrong information rather than calm information. `motion-audit.ts` was rewritten to classify CSS-covered versus JS-driven and to report the token that made a scene count, `/quality/aria` prints the split (88 through CSS · 32 from JavaScript) and both harnesses read the same module

Two families of dated records had no feed, and one indexable page was linked from nowhere

ledger row 526

Three text surfaces on this site publish a feed and two did not: **/learn/feed.xml** (60 dated guides, the largest unsubscribable family) and **/changelog/feed.xml** (13 entries whose only feed was the export copy at `/api/exports/changelog.xml`, which cannot link an entry's permalink because it is generated without the slug helper). Both now exist, both are built from their own data at request time with the origin taken from the request, and both carry the same `public, max-age=0, s-maxage=3600` policy as the catalog feed — including the two cache rules the config needs so the catch-all does not win for them. Separately, **/digest/copy-of-the-week** had a canonical and a title and was referenced from zero pages: `/` and `/digest` both returned nothing for it, so a finished, designed page was unreachable by any route a reader could take. `/digest` now links it in the sentence that explains what it is, and **/sitemap** exists as the same list as the XML for a person, importing `sitemap()` so the two cannot disagree (383 URLs, grouped by the site's own path structure, with the 8 excluded surfaces printed at the end). The batch-92 ratchet caught this batch's own drift while it was being written: editing the `keyframes.tsx` header changed the file from 40 lines to 42, and `check:exports` failed on the number the page prints until `MEASURED.keyframesLines` followed the file — which is the whole point of recording it

The site had nowhere to say what it cannot do

ledger row 528

Two pages existed for the audit's successes and none for its absences. **/gaps** gathers both registers: the six infrastructure-blocked bets, derived from `BETS` so a bet that goes live leaves the page in the same commit, and eight environment-blocked numbers — a real Lighthouse score, a second measurement series, field vitals, audience counts, the CodeSandbox deep link, a live domain, absolute-URL round trips from the harness, and a browser-run accessibility session — each with what it needs and what exists instead. **/accessibility** is the statement the site never had, and it is deliberately not `/quality/aria`: that page is the engineer's record of every rule and finding, this one answers the four questions a reader has, including the uncomfortable one — all seven manual checks are named and the page states that none has been run in a browser by this project. Both are in the footer, in the sitemap, and gated against the data they render. Writing them proved the new gates work: the claim map caught `/gaps` for saying "the harness" with no entry (`185 passed, 3 failed`), the broken-link gate caught a link to `/quality/gates`, which is a suggestion here and not a page yet, and the 200-character description rule caught `/gaps` at 201. The bet-title comparison also exposed a gate bug — React writes `&` as `&amp;`, so a title match on raw HTML text fails for the one bet with an ampersand in its name, and the check decodes entities now rather than comparing escaped text

The thumbnail poster was a landing page printed on a postcard

ledger row 529

Reported as text that had become noise, and the count agrees: every card on /prompts composed about fourteen text elements into 390 by 244 pixels — a faux navbar with a logo, three links and a call to action, a 30px `font-black` white headline with a drop shadow, an 8px sub-line, two buttons, up to four block chips, a stack list and two badges. The headline is an industry sample, not the prompt, so at card size the loudest thing in the box was also the least useful. `PromptPoster` now renders two compositions from one component. The thumbnail keeps the ambient scene, the fidelity badge and one anchored block — accent rule, kicker, two 14px truncated title lines — and the wide hero on `/prompts/[slug]` keeps the whole faux landing page. Two things turned up on the way. The scaffold was in the markup of all 74 thumbnails, `hidden` behind a class: hidden is not absent, so it is rendered conditionally now, and /prompts went from 506.8 KB of HTML to 279.5 KB. And the poster carried the card link's accessible name: a screen reader got "Poster scene mounts on scroll Verified 89 avg fidelity · concept render FINTECH / TRUST Security you can actually read Open prompt" before the prompt's own title. The poster is `aria-hidden` now — it is a concept render, not content — and the status pill that moved out of the picture into the card body is real text — deliberately not hidden, since with the poster hidden it is the only place a screen reader can learn the status — so a card link reads "Open prompt AI tool landing with terminal motif technical, confident Featured Next.js HTML 93 /100 · 3 runs". Verified against the running build: 0 occurrences of the navbar links and 0 of "See it live" on /prompts, 1 each on a prompt detail page. Reading the accessibility tree out of rendered HTML turned out to be its own trap — two hand-rolled strippers reported a name that was both too long and too short, because one glued the skipped subtree back on at the next closing tag and the other decremented its skip counter on any end tag, not the matching one. The number above comes from a depth-tracking parse of the served HTML. Gates green

An index of the gates, generated from the gates

ledger row 530

Four suites, 193 passing checks, and no page that listed them, so "gated in CI" was a sentence a reader had to take on faith. **/quality/gates** is built from the sources rather than typed: the names and the conditions come from the `ok(…)` calls in the two scripts, parsed at request time, so renaming a check changes the page in the same commit; the accessibility rules come from `MARKUP_CHECKS`, which is already the list of failures they prevent; and the pass counts come from the last recorded run in `docs/check-report.json`. The column nobody could derive from a script was the one the suggestion asked for by name — the batch that added each check — so `npm run gates:index` writes a `git blame` index of every `ok(…)` line into a checked-in file, keyed by check name rather than line number, so inserting a check above another one does not rewrite it. `check:exports` re-derives that index and fails when it is stale, which is the only thing that keeps a generated file true. The page also prints what it cannot show: the 11 names that are composed at run time (a check inside a loop prints one name per route, shown here as `…` and tagged runs per item), the fact that the condition column is code rather than prose because only the 12 markup rules ship a sentence about the failure, that a blame commit is the last edit rather than the first idea, and the 7 manual checks that no command runs. Two of those claims are in the claim map, so the sentence promising the freshness gate cannot outlive the gate. Writing it turned up its own bug: the check that the page prints its derived counts matched text across tag boundaries until it stripped them first — and then a check named `<slug>` would have been stripped as markup, so the tags come off before entities are decoded, not after

The gates these rows created are listed on /quality/gates · the absences are named on /gaps.