Website rebuild target (Next.js + Fumadocs) — site/
The rebuild (T666) lives in site/ as a separate buildable target, and since the T670 cutover
(2026-09-25) it is the only docs site.
npm run site:install # install site/ deps ONCE (its own node_modules -- see the note below)
npm run site:dev # sync USAGE.md, then next dev on http://localhost:41800
npm run site:build # sync USAGE.md, then a static export into site/out
npm run site:start # serve the built export on :41800
npm run site:typecheck # tsc --noEmit inside site/
npm run site:lint:gate # eslint --quiet inside site/site/ has its own node_modules and its own package-lock.json, and that is required, not
untidy. Next 16 needs React 19; the root pins React 18 because Ink 5 requires it. A shared
dependency tree resolves one of the two wrong, and the symptom is either the TUI or the site
breaking at runtime with no type error. For the same reason turbopack.root is pinned in
site/next.config.mjs — Turbopack otherwise picks the outermost lockfile (the root's) and would
resolve the site against React 18.
The root gates deliberately do NOT cover site/: root tsconfig.json includes only src and
examples, and root eslint.config.mjs ignores site/**. The site's gates are the site:*
scripts above, run alongside the root's.
Markdown stays the source of truth, and NOTHING under site/content/docs is committed. Two
sync scripts feed the site at build time, both run by npm run site:sync (which site:build,
site:dev and site:qa all call first):
scripts/sync-docs.mjscopies the repo-rootUSAGE.mdintodocs/usage.md(whichsync-site-docs.mjsthen reads with the rest ofdocs/) andsite/content/docs/usage.md(with a prepended YAML frontmatter block, since plainUSAGE.mdcarries none).scripts/sync-site-docs.mjsmigrates the whole trackeddocs/**/*.mdtree intosite/content/docs/, derivingtitleanddescriptionper page and emitting ameta.jsonper section for deliberate nav order.
The generated tool/env catalogs keep generating into the same root USAGE.md, so
npm run docs:catalog:check — which gates CI — measures exactly the file both sites render.
docs/**/*.md remains the ONE tracked copy. Two tracked copies of a 6500-line recipe book and
~40 reference pages is a fork waiting to happen, and the drift would stay invisible until a
provider page disagreed with itself. The T670 cutover deleted docs/.vitepress and kept
docs/**/*.md, because that tree is the content.
This also means docs/providers/README.md keeps its filename, so the GitHub folder view and the
cyberine-project-provider-docs skill both keep working. website-spec.md warned that renaming it to
index.md would break that skill; syncing at build time instead of moving files makes the warning
moot rather than something to work around — only the generated copy is renamed.
The Fumadocs copy is .md, not .mdx, and that is load-bearing: MDX parses <...> as JSX,
and this file is full of bare angle brackets in command syntax ("<task>", --model <id>). As
.mdx the build fails with Unexpected character `/` ... expected a character that can start a name. Fumadocs picks the processor format from the extension, so .md compiles it as the plain
Markdown it is — which is what "the canonical file must not have to be MDX-safe" requires.
The six marketing routes (/, /how-it-works/, /judges/, /cost/, /providers/, /surfaces/) are declared once in site/src/lib/routes.ts with each route's angle, which the footer readout prints beside the same runId on every route. Since 2026-10-01 they share one design language, the "run record" (see docs/design/website-master.md): Fraunces / IBM Plex Sans / IBM Plex Mono, a light paper mode and a dark screen mode, and one blue accent reserved for the kept result. Motion is a hero replay of a recorded run (Replay button), scroll-scrubbed worktree diagrams, hover and focus links between a figure and its printed lines, and a cross-fade between routes; every sequence has a finished still state under prefers-reduced-motion.
Colour mode: marketing routes follow the OS until the visitor picks one; docs open dark for everyone. One choice, made with the mode control on either surface, is stored in localStorage['cyb-theme'] and honoured on both from then on. The pre-paint bootstrap (site/src/lib/theme.ts, THEME_BOOTSTRAP) stamps data-surface and data-theme on <html> before first paint, so no route flashes the wrong mode, and ThemeSync re-resolves on client navigation between the two surfaces. To check it: open / with the OS in light mode (light), open /docs/ (dark), flip the control, and reload either surface.
Known limitations: with JavaScript off the bootstrap cannot run, so every route renders the marketing light mode; the hero's bar lengths and timing are drawn, not measured (its caption says so).
Design QA gate — npm run site:qa
The MASTER's quality bar is only real if it is repeatable, so the whole pass is one committed
script (site/scripts/qa.mjs, kx7dm) rather than a checklist. It exits non-zero on any HIGH
finding and is wired into ci.yml's site job:
npm run site:qa # build + audit; the full pass
npm --prefix site run qa -- --no-build # audit the existing site/out
npm --prefix site run qa -- --json # machine-readable
npm run site:fonts # re-download the self-hosted woff2 + regenerate fonts.cssThese checks run with no browser: the static export builds; detect-slop over BOTH surfaces with
the right --surface flag each (two runs, because the display-type boldness floor is a marketing
gate and not a docs one); the locked palettes' contrast, every text role against each ground it sits on across 4 modes
(marketing light and dark, docs light and dark) plus focus and non-text marks against the 3:1
minimum, and every locked hex present in tokens.css; contrast measured from the REAL
stylesheets with opacity composited; token discipline (no hex below the raw tier, no hardcoded
#fff/#000); font self-host drift; a dead-internal-link scan over the emitted files; a
third-party-request scan over the shipped bytes; per-route social-metadata coverage; and a static
accessibility pass over the built export.
T707 rebuilt three of those, because they were reporting PASS while inspecting nothing:
detect-slopis vendored atsite/scripts/detect-slop.mjs. It used to be resolved only from~/.claude/skills/nf-design-system/scripts/, a machine-local uncommitted directory, so on any CI runner the script took itsif (!existsSync(detector))branch and the MASTER's first hard gate ("zero HIGH slop") had never executed in CI once — while the CI step was named after it. qa.mjs prefers the vendored copy and still falls back to the skill path. Re-vendor by hand when the detector gains a rule; never edit the copy to silence a finding.cssContrast()reads the stylesheets. The palette check compares the MASTER's own hex table and never opens a CSS file, which makes it a sound guard on the MASTER and a blind one on the site: no CSS change of any kind could make it fail. It reported PASS while.nav-set arendered at 4.17:1 —--cyb-inkIS AA on--cyb-base, andopacity: .58on top of it is what fell below. The new check resolves every custom property per mode (four modes since 2026-10-01: each:root[data-surface][data-theme]combination, the more specific block winning), measures each stylesheet only in the modes of the surface it paints (docs.csson docs,base.csson both, the rest on marketing), inherits a nested rule's ground from its ancestor selector in the same file, walks every rule undersite/src, compositesopacityandcolor-mix(..., transparent)alpha, and measures against the background the rule actually sits on, at the WCAG large-text threshold where the font size earns it.metadataCoverage()asserts the tags exist.socialUrls()only ever regex-matched pages that ALREADY emittedog:/twitter:/ canonical elements and then localhost-tested the matches — so a page emitting NONE produced zero matches and was counted clean. That is exactly how 43 docs routes shipped with no card at all. Coverage is asserted first; the URL check then means something.
The accessibility pass is HIGH on what no page may ship without — a document language, one
<main>, at least one <h1>, a "skip to content" link whose target id actually exists, alt on
every image, an accessible name on every form control — and MEDIUM on heading ORDER and duplicate
<h1>s, which the docs surface trips by pairing fumadocs' DocsTitle with each MDX file's own top
heading.
Known limitations and deferred work: cssContrast() is a static read of cascade-free rules, not a
browser's computed style — inheritance is resolved by walking a selector's own ancestor prefixes
within the same file (.foot a inherits .foot's colour), specificity conflicts between unrelated
selectors are not modelled, and a colour set by JS is invisible to it. Its color-mix maths is
exact against transparent (every mix in this stylesheet) and approximate for a future
opaque-to-opaque mix, because it interpolates in sRGB rather than oklab. Rules that are exempt
rather than broken live in the short, justified CONTRAST_EXEMPT list at the top of the script
(WCAG's inactive-component exception) — keep it
short, and never add an entry to turn a red build green. metadataCoverage() and the accessibility
pass skip Next's error shells (404.html, _not-found/), named explicitly so a real route cannot
fall through. Canonical is required only when CYBERINE_DOCS_ORIGIN is set, because with no origin
Next emits none by design.
It reports what it could not verify rather than passing silently. These checks need a real
browser — the layout-audit sweep (3 viewports x 2 modes x 7 routes = 42 combinations), 60fps
frame timing, and reduced-motion behaviour. Browser automation needs the user's explicit in-turn
permission, which a script cannot grant itself, so those are printed under NOT VERIFIED with the
command to run each by hand. A false green on unmeasured layout would be worse than a gap.
The script also exports the two verified layout-audit false-positive classes as
LAYOUT_AUDIT_FILTERS, each with its measured justification, so the 40-of-46 findings they
produced on 2026-08-27 are not re-litigated: stack-overlap fires on grid cells that sit side by
side horizontally (the check only compares vertical rects), and misaligned-top fires on
center-aligned row siblings whose boxes differ in height.
Brand asset kit — npm run site:assets
The kit is authored vector, regenerated from tracked sources (site/assets/src/), not
image-generated. site/assets/MANIFEST.md records every asset's source and method.
npm run site:assets # regenerate mark, app icons, and the 7 per-route OG cards
npm --prefix site run assets:check # verify every generated asset is presentThree things worth knowing before touching it:
- The mark is one definition, correct in both modes.
src/components/BrandMark.tsxis inline SVG reading the role tokens, so light rendersverifiedas#006E50and dark as sulfur#D6E600. The app icon carries its own dark plate so it is legible against light and dark browser chrome without a second export. - Social-card type is glyph OUTLINES, not
<text>. librsvg on macOS resolves families through Pango's CoreText backend, so a repo-local fontconfig never reaches it — verified withfc-match,otool -L, and an inline@font-faceattempt, all three of which said the config was right while the render came out in a generic grotesque.scripts/gen-assets.mjstherefore usesfontkitto instance the variable axes (wght: 850, which ships in no static Archivo instance) and emits paths. Renders identically on a CI runner with no fonts installed. site/assets/fonts/*.ttfis a build input, not a shipped asset — it exists only for that outline extraction and is gitignored. The site serves the woff2 insite/public/fonts/.
Product captures are deliberately NOT in the kit (T697). T672 requires real captures of the
running TUI, chat REPL, and dashboard, never mockups. The two Ink surfaces need a real TTY —
script -q /dev/null npm run tui fails with tcgetattr/ioctl: Operation not supported on socket
from a background session — and the dashboard needs browser permission. A mockup would satisfy the
checklist and violate the requirement, so the gap is named and tracked instead.
Social metadata and the launch gate
Every marketing route emits its own og:image, so sharing /judges does not preview as the
homepage. Absolute URLs are gated on one env var, exactly as the VitePress site proved:
CYBERINE_DOCS_ORIGIN=https://<domain> npm run site:buildUnset — as in every local site:build — Next absolutises og:image and
canonical against http://localhost:3000. That is expected for a locally-reviewed build, but a
crawler cannot fetch a localhost URL, so qa.mjs reports it as unverified and turns it into a
hard HIGH the moment CYBERINE_DOCS_ORIGIN is set and a localhost URL still survives. That is
the T671/T675 launch gate, mechanically enforced rather than remembered.
Production deploy: cyberine.dev (T671)
The site is live at https://cyberine.dev, served by the Worker cyberine (Cloudflare Workers
static assets, the current form of Cloudflare Pages) on the operator's Niafam account. Deploy
config is site/wrangler.jsonc: assets from site/out, auto-trailing-slash HTML handling, the
export's own 404.html for unknown paths, and cyberine.dev as the Worker's custom domain.
npm run site:deploy # build + QA with CYBERINE_DOCS_ORIGIN=https://cyberine.dev, then wrangler deployWrangler needs a login on the Niafam account (npx wrangler login). An exported
CLOUDFLARE_API_TOKEN/CLOUDFLARE_API_KEY without Workers write on that account overrides the
login and fails the deploy; unset it for the command.
Rollback is a version switch, not a rebuild (drilled 2026-10-01: rolled back to the previous version, confirmed the live response changed, rolled forward):
cd site && npx wrangler deployments list # version ids, newest last
cd site && npx wrangler rollback <version-id> --message "<why>" -yRedirects live in the Cloudflare zones, not in the site: www.cyberine.dev, cyberinecore.com
and www.cyberinecore.com answer 301 to the same path on https://cyberine.dev (redirect rules
on proxied placeholder AAAA 100:: records), and both zones have Always Use HTTPS on.
Search-facing behaviour of the build (SEO pass, 2026-10-01):
- USAGE.md is split into section pages on the site.
scripts/sync-docs.mjs(viascripts/splitUsage.mjs) writes one page per##section under/docs/usage/<section>/, and any section over 300,000 characters (today only Run recipes) one page per###under it. Googlebot reads only the first 2 MB of an HTML file and the single page was 7.2 MB. In-page](#anchor)links in USAGE.md are rewritten to the page that now holds the heading; an anchor that matches no heading fails the sync. USAGE.md itself stays one file in the repo. /docs/design/*and/docs/eval/*arenoindex, followand left out of the sitemap (NOINDEX_DOCS_SECTIONSinsite/src/lib/metadata.ts). They stay reachable and linked.- The home page carries
WebSiteJSON-LD (name, url, description) whenCYBERINE_DOCS_ORIGINis set. - Sitemap
<lastmod>is the last commit date of a page's tracked source (thedocs/file, or the marketing route's files); usage section pages carry none, since one USAGE.md date would be wrong for most sections. site/public/_headersmarks the content-hashed/_next/static/*filesimmutablefor a year.
Known limitations and deferred work. There is no llms.txt and no dedicated quickstart page
yet, both of which T671's done-when listed; the site has never had either, so they are new pages,
not launch steps. The layout, frame-timing and reduced-motion checks site:qa skips still need a
browser pass against the live origin. No CI job deploys: a push to main does not change the
live site until site:deploy runs.
Known limitations and deferred work (T666 is the scaffold, not the site). The six routes are
SHELLS: each renders its eyebrow, heading, lede and readout, and the spatial idea it is supposed
to carry is recorded in a comment at the top of its page.tsx. Implementing those skeletons and
the four-move signature suite is T668; the token/type/state system and the self-hosted fonts are
T667; the four-move signature suite is T668. T670 retired VitePress once parity was measured.
Docs content is migrated (T669). All 41 pages from docs/** reach the new site at their
mapped URLs — verified against website-spec.md's own mapping table over the emitted files, plus
0 dead internal links across 52 pages. Search is Fumadocs' STATIC index (staticGET +
staticClient), which is required under output: 'export': the default fetch client asks a
server that does not exist and returns zero results for every query with no error to notice.
Research mode — cyberine research (plan -> search -> verify -> synthesize)
For questions that need current web evidence — comparisons, market scans, "what is the state of X in 2026?", trade-off write-ups — use cyberine research. The orchestrator decomposes the question...
Documentation site: VitePress retired (T670)
The VitePress implementation (docs/.vitepress, npm run docs:legacy:) was deleted on 2026-09-25 after the new site passed its full QA gate: the static site:qa pass (0 HIGH, 0 dead links across 59...