git:20260926.41cba2d to git:20261001.57bf898

9 added, 0 removed. Audit A to A.

# docs — the Docusaurus site (bestax.io)
Docusaurus 3. Dev server: `pnpm docs` (from repo root) → http://localhost:3000.
Production build: `pnpm exec turbo run build --filter=@allxsmith/bestax-docs` (builds bulma-ui
first). Deploys to Cloudflare Pages from `docs/build/` via `.github/workflows/deploy.yml`.
## Layout
- `docs/api/` — **API reference, one page per component**, in folders mirroring
`bulma-ui/src/` (`elements/`, `components/`, `form/`, `layout/`, `columns/`, `grid/`,
`helpers/`). A new component gets its page here — it's step 4 of the anatomy rule in
`bulma-ui/CLAUDE.md`.
- `docs/guides/` — task-oriented guides (`getting-started/`, `features/`, `library/`,
`helpers/`, `llms/`).
- `docs/components/`, `blog/`, `src/` — Docusaurus scaffolding, landing page, posts.
- `sidebars.js` — navigation; new pages must be reachable from it (or via autogenerated dirs).
- `plugins/devto-preprocessor.js` — local plugin; blog posts are syndicated, keep frontmatter
compatible.
## The LLM docs pipeline (this site is machine-read)
The build runs `docusaurus-plugin-llms` (configured in `docusaurus.config.js`), regenerating:
- `/llms.txt` (curated index) and `/llms-full.txt` (full concatenation)
- a per-page `.md` twin for every doc page (so `llms.txt` links resolve)
The plugin reads **source** markdown, and since 0.5.0 it strips PascalCase JSX tags while
keeping their **inner text**. That single rule decides how components must be authored if their
content is to reach an agent:
- **Content in children survives.** A fenced code block wrapped in a component comes out the
other side as exactly that fence, wrapper gone. This is why `<PackageManagerTabs>` takes a
pnpm fence as its children rather than a `command` prop — the artifact is then correct by
construction, with no build step to keep in sync.
- **Content in props does not.** It goes with the tag, silently. A self-closing
`<PackageManagerTabs command="…" />` left an empty section in every artifact while the
rendered site looked fine. Before giving a docs component a prop that carries prose or a
command, check what the generated `.md` looks like.
The known casualty is `<TabItem label="…">`: the label is Docusaurus's own prop and can't move
to children, so `<Tabs>` collapses to its bodies with nothing marking which option is which.
`skills/theming.mdx` and `skills/custom-component.mdx` are affected. Tracked upstream at
rachfop/docusaurus-plugin-llms#64, which asks for a preserve-list or a pre-clean hook.
`build` then chains `scripts/strip-generated-markers.mjs`, which removes the
`<!-- bestax:generated -->` markers from the built `.md`/`.txt` only — the plugin does not
strip HTML comments, and the markers are a source-control device no reader of the site needs.
It is a build step rather than a Docusaurus plugin because `postBuild` hooks run under
`Promise.all`, so a plugin declared after `docusaurus-plugin-llms` still races it.
Consequences: moving/renaming/deleting a doc page changes the published LLM index that AI
agents consume — treat URL changes like API changes. The canonical AI entrypoint is the LLMs
guide (`docs/guides/llms/` → https://bestax.io/docs/guides/llms) — don't add new competing
entrypoints. (Converging every discovery surface on that URL was evaluated and declined in
#203: discovery already resolves in 1–2 hops from every entrypoint and no peer library does
it, so a novel non-standard `package.json` key and extra release churn weren't worth it.)
## Conventions
- API pages mirror `docs/api/components/avatar.md` (the house exemplar), not older siblings;
`check:conformance` enforces the required sections. Frontmatter `title:` = the exported
name, and the first `## Overview` sentence becomes its catalog one-liner (`gen:catalog`).
- **Parts of an API page are generated** (`pnpm gen`, `scripts/gen-api-docs.mjs`). Regions
delimited by `<!-- bestax:generated <id> -->` / `<!-- /bestax:generated <id> -->` are
machine-owned and hand-edits are reverted: `overview` (the summary sentence, from the
component's TSDoc), `import`, `props` (from the `<X>Props` interfaces) and `cssvars` (parsed
from the SCSS). To change a props table, change the TSDoc on the interface member. Prose
outside the markers — including admonitions right below the Overview sentence — is yours.
Deleting a marker pair opts that region out; `docs-section-order` makes that visible rather
than silent. One exception to that opt-out: `cssvars` presence is data-driven — the section
renders only when the component's `SCSS_SOURCES` entry (in `scripts/lib/api-sources.mjs`)
is non-empty, so a wrong `[]` there suppresses it with every gate green (#464; the orphan
rule in `check:conformance` now catches the unclaimed-partial half of that).
Managed categories are listed in `scripts/lib/api-sources.mjs` — the set starts
empty and grows one category per PR, so a page is only generated once its category lands.
- Section order on a managed page: Overview, Import, Usage, _(page-specific extras)_,
Accessibility, Related Components, Additional Resources, Props, CSS & Sass Variables.
Every category except `helpers/` is managed; the hook pages there have no `## Props`
at all, and `config.md`/`theme.md` follow the order but keep their hand-written tables.
- The `props` region owns the tables and nothing else. Prose inside `## Props` is preserved
above the opening marker or below the closing one — but prose sitting _between_ two tables
has nowhere to go, so move it out first. A `###` heading that is not one of the component's
sub-components (`### TaginputTag`) ends the region rather than being swallowed by it.
- No inline `style={{}}` in examples — use `Block`/helper props; there is no `gap` helper,
space children with `m*`/`p*`.
- Code examples must compile against the current library API; when a component changes, its
docs page changes in the same PR (CONTRIBUTING requires docs before approval).
+ - **`jsx` and `tsx` fences are linted** with the ESLint plugin's `recommended` rules, in
+ `pnpm test` (`scripts/eslint-plugin-docs.test.mjs`, whose header says how a fence is read).
+ That catches a helper value the library drops without a word (`textAlign="center"`), a
+ deprecated prop, and a flex prop that emits nothing. A fence needs no import for it. A fence
+ that is wrong on purpose, such as a rule's ✗ example, takes `nolint` in its info string
+ (` ```jsx nolint `), which changes nothing on the rendered page. The marker is per fence so
+ the rest of the page is still checked, and it fails once nothing in the fence is reported,
+ so it goes when the mistake does. A fence the test cannot parse fails too, marked or not,
+ since no rule can read it. An example that is not code at all belongs in a `text` fence.
- **Install and run commands go in `<PackageManagerTabs>`**, wrapping the pnpm fence you would
have written anyway, so npm/yarn/bun readers don't translate by hand:
````md
<PackageManagerTabs>
```bash
pnpm create bestax@latest my-app
cd my-app
pnpm install
```
</PackageManagerTabs>
````
Registered globally in `src/theme/MDXComponents.js` — no import. Write the **pnpm** form
exactly, one command per line; npm, yarn and bun are derived from it, and a line that isn't a
pnpm verb (`cd my-app`, a `#` comment) passes through unchanged. Blank lines around the fence
are required — without them MDX treats it as literal text, not a code block. The component
derives the command back out of the fence and throws during the prerender if the round trip
isn't exact, so a non-canonical fence (`npm install foo`, odd spacing) fails the build rather
than rendering tabs derived from something the page never showed. Docs-tree pages
only — a syndicated blog post must not use it (JSX reaches dev.to as raw text); see the
Syndication section of `blog/CLAUDE.md`.
- **`.md` files here render JSX**, because `markdown.format` defaults to `mdx` and
`docusaurus.config.js` does not override it. That is load-bearing but easy to miss: setting
`format: 'detect'` would make every `.md` page render its tags as literal text.
- Markdown is prettier-formatted (`pnpm format:check` covers `md`/`mdx`).
- **Guides are canonical reference, not lab notebooks.** No run statistics, run ids, dated
observations or "as of" tallies in `docs/docs/**`: the LLM index serves it as current fact,
and a count is stale the day after it is written. Dated snapshots belong in `blog/`, which
is excluded from the index (see `blog/CLAUDE.md`). Evidence goes on the issue or PR and the
guide links it. A claim that needs a number to be true belongs in a test or a generator, not
a sentence. A guide section says what the operator can do and what each switch is compared
against; it does not re-derive a third party's internals. Claim the case in front of you:
"every" and "only" are one counterexample from false. Never cite a line number; cite a
heading, a step id, or a flag. `check:conformance --only=fragile-prose` enforces the count,
run-id, and line-reference parts over `docs/docs/guides/**`; the migration guides are exempt
because their counts describe a frozen upstream, and the API pages are generated. The rule
still holds everywhere in `docs/docs/**` — the check is a net, not the contract. The
library comparison page is the one guide that shows dated data, and only through its
component, which the index strips; its prose follows this rule (see "The State of React" in
`blog/CLAUDE.md`).
- Scripts in `docs/scripts/` are covered by the root `pnpm lint` — run it before pushing even
a docs-only PR (#471 broke CI on exactly this). Playwright `page.evaluate` callbacks
execute in the browser, so declare the browser globals each callback actually uses
(`/* global document */` in bulma-ui's `capture-stories.mjs`;
`/* global document, getComputedStyle */` in `rasterize-cover.mjs`) — and when a script already
has the page open, validate through the parsed DOM and computed styles, not regexes over
the source (quoting, `data-*` attributes, and transparent paint all fool regexes; two
review rounds proved it).