# 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. The strip
leaves a marker shown inside a code fence, so the step fails the build when a built file keeps
more such markers than the source pages show. A code fence left open in one page of
`llms-full.txt` would otherwise hide every later page's markers.

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.
- **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).
