Working on the docs
This page documents the documentation pipeline itself — site structure, hand-written vs auto-generated pages, generator internals, build + deploy, and how to add new content.
Two surfaces, one project
| Surface | Lives at | Purpose |
|---|---|---|
docs/spec/ | Repo root | Internal design / RFC territory. Authoritative for why. Plain Markdown, read on GitHub. |
website/ | Repo root | User-facing documentation site (this site). Authoritative for how. Docusaurus 3 + MDX. |
Rule of thumb. If a change affects user behavior, update the site. If it changes design rationale, update the spec. Big features often touch both — do it in the same PR.
Site structure
website/
├── docusaurus.config.ts Site metadata, plugins, nav, footer, prism/mermaid config
├── sidebars.ts The sidebar tree — every page referenced here
├── package.json Node deps (Docusaurus 3.10, local search, mermaid)
├── src/
│ ├── pages/index.tsx Custom landing page (hero + features + install)
│ ├── components/ React components used by pages
│ └── css/custom.css Infima overrides, brand palette, dark-mode tuning
├── static/ Images, diagrams, favicon
└── docs/
├── introduction/ 4 pages — what is versionx, comparisons, status, principles
├── get-started/ 7 pages — install, quickstart, first release, migrations
├── guides/ 7 task-oriented guides
├── reference/ Auto-generated reference pages
│ └── cli/ One page per versionx subcommand (auto-gen)
├── integrations/ MCP (per agent), JSON-RPC, HTTP API, Actions
├── sdk/ Rust SDK embedding + cookbook + docs.rs pointer
├── contributing/ Dev env, workspace tour, this page, etc.
└── roadmap.md User-facing version-sliced roadmap
Hand-written vs auto-generated
Hand-written (most pages)
Plain Markdown with YAML frontmatter:
---
title: My new page
description: One-sentence summary used in search + link previews.
sidebar_position: 3
---
# My new page
Content goes here in GitHub-flavored Markdown + MDX.
Rules:
- Frontmatter is required.
titleanddescriptionat minimum.sidebar_positionwhen ordering matters. - Use
.md, not.mdx, unless you need React components or front-matter imports. Docusaurus 3 processes both through MDX, but.mdreads as more human-friendly source. - MDX escapes:
{,},<, and>outside code blocks get parsed as JSX. Wrap literal braces in backticks (`{path}`) or escape with\{/\}. Same with</>— use</>or wrap in backticks. - Admonitions use the
:::note/:::tip/:::warning/:::dangerfenced blocks. - Code blocks always carry a language tag so Prism highlights correctly (
bash,toml,rust,json,console, etc.).
Auto-generated (Reference)
Reference pages come from the source of truth. Hand edits get clobbered on the next cargo xtask docs run, and CI fails when committed content drifts from what generators produce.
| Page / directory | Source of truth | Generator |
|---|---|---|
reference/cli/*.md | clap derive in crates/versionx-cli/src/main.rs (via versionx --help-json) | cargo xtask docs --only cli |
reference/versionx-toml.md | crates/versionx-config/src/schema.rs | cargo xtask docs --only config |
reference/events.md | Every .info("kind") / .warn("kind") / Event::new("kind") site across crates | cargo xtask docs --only events |
reference/exit-codes.md | CoreError enum in crates/versionx-core/src/error.rs | cargo xtask docs --only exit-codes |
integrations/mcp/tool-catalog.md | Tool descriptor() fns in crates/versionx-mcp/src/tools/*.rs | cargo xtask docs --only mcp |
integrations/json-rpc-methods.md | methods {} and notifications {} const blocks in crates/versionx-daemon/src/protocol.rs | cargo xtask docs --only rpc |
Every generated file starts with:
{/* GENERATED by `cargo xtask docs --only <name>`. Do not edit by hand. */}
Run all generators at once:
cargo xtask docs
Generator internals
Implementation in xtask/src/docs/. Shared helpers in syn_util.rs.
docs-cli
Invokes versionx --help-json (a hidden root flag that emits the complete command tree as JSON) and walks the resulting tree. Produces a root page plus one file per top-level subcommand. Sub-subcommands render inline on the parent page.
docs-config
Parses schema.rs via syn, walks every pub struct and pub enum, extracts field names, types, serde attributes (default, flatten, rename, skip_serializing_if), and doc comments. The root VersionxConfig renders at ##, everything else at ###.
docs-events
Event kinds aren't enumerated as Rust variants — they're dot-separated strings passed to EventSender::emit / .info / .warn / Event::new(...). The generator scans every .rs file under crates/ with a regex and collects the string-literal first argument. Tests get filtered out by cutting at the \n#[cfg(test)]\nmod tests marker. Results group by top-level category.
docs-exit-codes
Parses error.rs via syn, walks CoreError variants, and reads each variant's #[error(...)] template attribute.
docs-mcp
Reads tools/mod.rs to discover tool module names (via regex), then parses each <name>.rs with syn to find the descriptor() function. Walks its body for a ToolDescriptor { ... } struct literal and extracts name, title, description, mutating fields.
docs-rpc
Parses protocol.rs, locates the methods and notifications inline modules, and collects every pub const NAME: &str = "value" inside them. Doc comments on each const become the entry description.
table_escape
All generators go through xtask::docs::syn_util::table_escape before writing into Markdown tables. It replaces | with \|, {/} with \{/\}, </> with </>, and collapses newlines. Without this, MDX tries to parse literal {path} as JSX and literal <kind> as an opening tag.
Build + deploy
Local
# Run generators
cargo xtask docs
# Serve with hot-reload
cd website
npm install # first time
npm start # http://localhost:3000/versionx/
# Production build
npm run build # static output in website/build/
npm run serve # serve the production build locally
CI
.github/workflows/docs.yml has four jobs:
- regenerate — runs
cargo xtask docs, thengit diff --exit-code -- website/. Any drift between committed and generated content fails the build. This is what keeps the auto-gen contract honest. - build —
npm ci+npm run build+ upload thebuild/output as a Pages artifact. - lychee — broken-link check over the built HTML, advisory (non-blocking).
- deploy — on pushes to
main, publishes the artifact to GitHub Pages.
Triggered by changes under website/**, crates/**/*.rs, xtask/**, or the workflow / lychee.toml themselves.
Pages configuration
- Source: GitHub Actions (not the classic
/docsbranch-based deploy). - URL:
https://kodydennon.github.io/versionx/. - Enabled via the GitHub API (
gh api -X POST repos/.../pages -f build_type=workflow). Branch policy permitsmain. - Custom domain deferred — move to
versionx.devor similar later with a one-linedocusaurus.config.tsedit + awebsite/static/CNAMEfile + DNS.
Adding a new page
- Decide which section it belongs to.
- Create
website/docs/<section>/<slug>.mdwith frontmatter. - Register it in
website/sidebars.ts. Sidebar order is explicit — add your entry in the right place in the array. - Link to it from neighboring pages' "See also" sections if relevant.
- Local build:
cd website && npm run build. Fix any MDX errors. - Commit.
For Reference pages, edit the source of truth instead (e.g., a new clap flag, a new config field, a new error variant, a new MCP tool) and run cargo xtask docs — the generator handles the rest.
Adding a new section to the sidebar
Edit website/sidebars.ts. Sections are category objects:
{
type: 'category',
label: 'My new section',
link: { type: 'doc', id: 'my-section/overview' },
items: [
'my-section/overview',
'my-section/page-two',
// ...
],
},
Every id must correspond to a real doc file. Docusaurus fails loudly if it can't resolve one.
Updating the landing page
The landing page is a custom React component at website/src/pages/index.tsx — not one of the doc pages. It pulls in:
website/src/components/HomepageFeatures/— the feature grid.website/src/pages/index.module.css— hero layout styles.
Edit freely, but keep the install snippet and the versionx demo block in sync with actual CLI output.
Updating the theme / brand
website/src/css/custom.css owns the Infima overrides — colors, fonts, headings, dark-mode tuning. The brand palette is a deep slate base with a burnt-amber accent. Light and dark variants are both defined.
Style guide (brief)
- Plain voice, no marketing puff in Reference.
- No headings deeper than
###outside Reference pages. - Code blocks always have a language tag.
- Imperative command examples (
versionx plan, not "you would run versionx plan"). - Avoid "awesome", "powerful", "blazing fast".
- Every Guide opens with "You'll learn" (3 bullets) and "Prerequisites".
- Every Reference item includes a "Since" version where applicable.
- Every page ends with "See also" — 2–4 internal links.
Troubleshooting
MDX compilation failed: "Could not parse expression with acorn"
Literal { or } outside a code span. Wrap in backticks or escape with \{ / \}.
MDX compilation failed: "Expected a closing tag for <foo>"
Literal <foo> in text. Wrap in backticks, or escape as <foo>.
Drift-check failed in CI
A source file changed but the generated page wasn't regenerated. Run cargo xtask docs locally, commit the result.
npm run build warns about onBrokenMarkdownLinks
Docusaurus v4 migration notice. Safe to ignore until v4; the option will move to markdown.hooks.onBrokenMarkdownLinks.
Local dev server doesn't show new content
Clear the cache: npm run clear then npm start.
Search returns nothing
The local search plugin builds its index at npm run build time, not in dev mode. Run npm run build && npm run serve to test search.
See also
- Release engineering — how Versionx releases itself, including
cargo xtask docsin the pipeline. - RFCs & design docs — spec vs site territory and how they relate.
- Dev environment setup — broader local setup.