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