CLAUDE.md@docs · diff
git:20261015.f18eed3 to git:20261015.b7338d9
2 added, 2 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.
Docusaurus's `<TabItem label="…">` is handled by the plugin itself: it writes the label as a
bold line ahead of the tab's body, so a `<Tabs>` block keeps which option is which.
The plugin also drops HTML comments outside code, and with them the `<!-- bestax:generated -->`
markers, a source-control device no reader of the site needs. The rule: the keyword
`bestax:generated` must never reach the built markdown or the llms files, so a docs page or blog
post that wants to show marker syntax shows it without the keyword. `build` chains
`scripts/check-generated-markers.mjs` to check that, and that script's header says exactly what
the step checks and fails on.
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.
- 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.
A `jsx live` or `tsx live` fence is also type-checked against the library's build, in
`pnpm test` (`scripts/docs-fence-types.test.mjs`, whose header says how a live fence is
read), so a prop the component does not take fails even though the page renders. One that
is wrong on purpose takes `notypecheck` in its info string (` ```tsx live notypecheck `),
which also changes nothing on the page. It holds only while the fence still has a type
error, so it goes when the mistake does, the way `nolint` does.
- **Live examples render in a shadow root**, and the preview
(`src/theme/CodeBlock/ShadowLivePreview.js`) wraps each one in a `ConfigProvider` whose
`portalContainer` is an element inside that root, so what an example portals keeps the
preview's styles. A `ConfigProvider` the example nests replaces that setting rather than
adding to it, and its portaled content lands unstyled in `document.body` with every gate
green. In an example that portals, don't nest one, or give it a `portalContainer`: an element
the example renders, or the preview's own, read with `useConfig()`.
- **A live example opens in StackBlitz and CodeSandbox** from the two buttons beside its copy
button (`src/theme/CodeBlock/SandboxButtons`, builders in `src/theme/CodeBlock/sandbox/`).
The project is built in the reader's browser when they click, from the code in the editor:
the fence becomes an `App.tsx` with exactly the imports it uses, inside the create-bestax
`vite-ts` template (StackBlitz; CodeSandbox gets a Create React App layout), installing the library release this build read or a later compatible one.
Nothing is published ahead of time and a fence needs no marker. A component that exists only in this site goes in
`docsOnlyScope` in `src/theme/CodeBlock/scope.js` (and in the scope header of
`scripts/docs-fence-types.test.mjs`): an example using one shows no sandbox buttons, since no
- sandbox can install it. That test also runs the converter over every live fence that is not
- marked `notypecheck`, and type-checks what it writes, so a fence it cannot convert fails `pnpm test` instead of opening
+ sandbox can install it. That test also runs the converter over every live fence and
+ type-checks what it writes, so a fence it cannot convert fails `pnpm test` instead of opening
broken; a failure there is the converter's to fix, not the fence's.
- **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).