Skip to content

Docs authoring conventions

Every markdown file under docs/ ships through the same VitePress pipeline and ends up in front of operators, integrators, and contributors who do not have access to the internal task tracker. To keep the corpus readable and to keep the rendered HTML free of internal task identifiers, three rules are non-negotiable:

  1. Headings are sentence case.
  2. Each file carries a complete frontmatter block.
  3. PX-XXXX, REQ-YYY, S0NN never appear in the rendered output — neither in headings nor in body prose. When the trace is still useful for grep or audit, keep it as an HTML comment.

These rules are enforced at go test time by the gates in tests/docs/ (heading_convention_test.go, frontmatter_test.go, anchor_links_test.go). A failing test is the authoritative signal — when in doubt, run make test against tests/docs/ and let the failure name the file.

Heading style

  • One H1 per file, on the first content line below the frontmatter. H1 is the page title in the rendered sidebar / breadcrumb.
  • All headings (H1–H6) are sentence case: only the first word and proper nouns are capitalised.
  • No trailing parentheticals carrying internal IDs. The forbidden shapes are PX-XXXX, REQ-YYY, REQ-PX-XXXX-YYY, S0NN, and the review-item codes W-NNN, I-NNN, C-NNN, B-NNN — none of these may appear in any heading anywhere in the corpus.
  • Inline code is allowed (# The `state/` sub-package) but unquoted angle-bracketed placeholders are not — the VitePress build parses them as Vue/HTML elements and fails. Wrap placeholders in backticks: write `<Domain>`, not <Domain>.

Examples

Do:

markdown
# Storage topology

## Row-to-package map
## SpiceDB and Postgres wiring
### Per-Domain residency

Don't:

markdown
# Storage Topology — plexsphere platform packages

## Row-to-Package Map
## SpiceDB↔Postgres wiring (PX-0011)
### Per-Domain Residency (REQ-013)

Frontmatter contract

Every file under docs/ opens with a YAML frontmatter block delimited by --- lines. The contract:

FieldTypeRequiredNotes
titlestringyesSentence case, matches the H1. Used by VitePress for the <title> tag and breadcrumbs.
descriptionstringyesOne-sentence summary of the page. Used by VitePress for the meta description and search snippets.
statusenumyesdraft or stable. draft pages render with a banner. For how-to guides the choice follows a deliberate criterion — see the draft and stable status policy under the how-to skeleton below, not a blanket default.
audienceenumyesoperator, contributor, or developer. Drives later filter / role-based navigation.
quadrantenumyesOne of tutorial, how-to, reference, explanation, contributing. The Diátaxis quadrant the page belongs to — must match the section the file lives under. The explanation quadrant spans three directories (architecture/, contexts/, explanation/); the other quadrants map one-to-one to their directory, with one exception: a cross-cutting operational runbook under docs/operations/ carries quadrant: how-to (its voice) even though it lives outside docs/how-to/, because it spans more than one bounded context. contributing is a deliberately non-Diátaxis fifth value for the contributor handbook. See how this documentation is organised for the placement rule.
sidebarbooloptionalDefaults to true. Set to false only for files intentionally unreachable from the sidebar (e.g. archived roadmaps). The drift gate in tests/docs/vitepress_config_test.go honours this opt-out.

The previously-permitted feature: field is no longer allowed — frontmatter values render into the <title> tag, the <meta> description, the sidebar tooltip, and the search index snippet, and must therefore not carry an internal task identifier. See the traceability identifier conventions for the full surface map and the regression gate.

Example

yaml
---
title: Manage domain settings
description: Operator how-to for renaming a Domain, rotating its IdP binding, and rolling the per-Domain signing key.
status: stable
audience: operator
quadrant: how-to
---

(Earlier revisions of this convention added a feature: PX-0024 key. That allowance has been retired — see the section above and the traceability identifier conventions.)

Traceability without leaking PX

When the link to an internal feature task carries real value (a ground-truth pointer, a follow-up gate, an audit trail), keep it — but as an HTML comment, immediately under the frontmatter:

markdown
---
title: 

---

<!-- traceability: PX-0017, REQ-005 -->

# …

HTML comments survive in the markdown source (so grep PX-0017 still finds the page) but VitePress strips them from the rendered HTML. The reader never sees an opaque task ID.

The same rule applies inline. When a paragraph today reads:

The relay assignment loop fans out signed envelopes onto the per-node JetStream subject.

rewrite it so the sentence stands on its own and move the trace to a comment if it is still useful:

markdown
<!-- traceability: PX-0017, REQ-009 -->

The relay assignment loop fans out signed envelopes onto the per-node
JetStream subject.

The body sentence is now a self-contained statement; the comment preserves the PR/audit link for future contributors who need it.

Cross-doc anchor links (./foo.md#some-section) resolve through the GitHub-style slug of the target heading. When you rename a heading, every anchor that pointed at the old slug must be updated in the same change. The anchor_links_test.go gate walks the whole corpus and fails on any stale fragment, so the rule is enforced — but it is faster to keep the references coherent in the editor than to chase the test failure later.

A common gotcha: legacy anchors that ended in -px-XXXX (because the heading carried the parenthesised PX). Once the heading is rewritten in sentence case, the slug shortens — update every link in the corpus in the same commit.

Section landing pages

Every top-level section under docs/ (architecture/, contexts/, contributing/, explanation/, how-to/, reference/, tutorials/) ships an index.md answering three questions in 15–30 lines:

  1. What is in this section?
  2. When do I need it? (i.e. which Diátaxis quadrant, which audience)
  3. Where is the entry point?

The home page at docs/index.md links into each section index; the section index links into its individual pages. Adding a new top-level section directory therefore requires adding an index.md to it; the sidebar drift gate in tests/docs/vitepress_config_test.go fails until it is wired up.

How-to skeleton

Every how-to under docs/how-to/ follows the same spine. The strong guides already share it; new and revised guides match it so a reader meets a predictable shape on every page.

Section order

A how-to walks these stages in order. The first three plus See also are mandatory; the middle two are optional and appear only when the task needs them.

StageHeadingRequiredPurpose
Introthe H1 plus one or two framing sentencesyesName the surface and the single goal.
Prerequisites## PrerequisitesyesWhat the reader must have in place — session, IDs, tools, environment variables. Bind every placeholder the Steps reference.
Steps## StepsyesThe ordered procedure. Each primary command shows its sample output (see below).
Verification## VerificationyesThe success signal — a command and the output that proves the task worked.
Rollback / cleanup## Rollback or ## CleanupoptionalHow to undo the change, or how to remove a transient artefact (a scratch secret, a port-forward).
Troubleshooting## TroubleshootingoptionalThe handful of failures an operator hits most, each with its fix.
See also## See alsoyesLinks to the reference page, the bounded-context explanation, and sibling how-tos.

The casual labels "Verify", "Next steps", and "Troubleshooting" map onto the canonical headings ## Verification, ## See also, and an optional ## Troubleshooting. The heading text is fixed — use the canonical form, not the casual synonym, so the slugs stay stable across the corpus.

Show the output

Every primary command in Steps and the Verification command shows what it prints, inline, as #-prefixed comment lines inside the same fenced ```shell block. Mask secrets, tokens, and UUIDs with so the sample stays copy-safe. getting-started/log-in.md (the whoami grid), platform/capture-a-support-bundle.md (the archive listing), and platform/run-behind-a-reverse-proxy.md (the Set-Cookie line) are the exemplars.

shell
plexctl domain list
# ID   SLUG       DISPLAY_NAME   CREATED_AT
# …    acme-corp  Acme Corp      …

A bare command with no shown output, or a jq pipe whose result is left to the imagination, is the gap this rule closes — the reader cannot tell success from a silent no-op without it.

Surfaces plexctl does not wrap yet

Some procedures drive a raw /v1 path, SQL, or a third-party CLI because plexctl has no subcommand for them yet. State that once, in the intro, with the canonical one-liner:

There is no plexctl <noun> command yet, so this guide drives <surface> with <tool>.

Fill <surface> with the concrete path (/v1/clouds) and <tool> with what the guide actually uses (curl, psql, the nats CLI). Keep the note in the framing prose; do not repeat it at each step.

Draft and stable status

The status frontmatter field (see the frontmatter contract above) carries a deliberate signal for how-tos, not a blanket default:

  • stable — the guide documents a shipped surface, follows this skeleton end to end, and shows sample output. Most how-tos are stable.
  • draft — the guide's own intro flags the surface as not yet wrapped ("…yet", "does not wrap"), so the procedure is expected to change shape once the plexctl subcommand lands.

A guide is draft because its subject is still moving, never because the page was left unfinished. When the wrapping command ships, the guide is rewritten plexctl-first and flips to stable in the same change.

Training deck chapters

A tutorial lesson on the demo path carries a second deliverable: the training deck chapter that condenses it for a live presentation. The two ship together, in the same commit, for the same reason tests do — a deck that lags its lessons fails in front of an audience, and nothing in an ordinary review compares two files nobody diffs against each other.

A chapter lives at docs/training/NN-<slug>.md and satisfies the frontmatter contract above like any other page (quadrant: tutorial). On top of that it carries four markers, all of them HTML comments so the chapter file stays a readable page:

MarkerWhereWhat it does
<!-- source: docs/tutorials/<lesson>.md -->under the frontmatter, one per lesson the chapter condensesbinds the chapter to the lesson its commands must match
<!-- slide -->between slidesthe slide separator make demo-deck splits on
<!-- hat: <id> -->under the source declarations, and again wherever the acting identity changessays who is acting, which both renderings draw as a badge
<!-- skip: <hash> — <reason> -->in a <!-- coverage --> block at the footrecords a lesson command the chapter deliberately does not show

The hat marker is the one exception to "invisible in the rendered page": it renders. On a slide it becomes a coloured bar across the top edge and a pill naming the identity, repeated on every slide until the next declaration, because a presenter needs it on every slide. On the VitePress page it becomes a badge at the switch point only, because a page is read top to bottom. The vocabulary — the ids, their colours, icons, and the plexctl profile each one signs in under — lives in docs/.vitepress/training-hats.mjs, which both renderers import, so a hat is defined once.

A slide that genuinely acts as two identities at once declares both: <!-- hat: admin, operator -->. Its bar is split down the middle.

The rules the gate enforces:

  1. Every lesson on the demo path is claimed by exactly one chapter.
  2. Every ```bash block on a slide appears byte-identical in one of that chapter's declared sources. A slide may not paraphrase a command. When a slide genuinely needs something no lesson contains, mark the fence with <!-- deck-only --> on the line above it.
  3. Every ```bash block in a claimed lesson is either on a slide or carries a skip declaration with a stated reason.
  4. Every chapter declares the identity it opens in, and every plexctl command on a slide matches the hat that slide is wearing. A --profile operator under the admin badge fails, and so does a profile-less command under a hat that only ever runs with one.

Rule 3 is why the deck stays honest: adding a step to a tutorial turns the build red until somebody decides whether the demo shows it. The failure message prints the exact skip line to paste, so the fix is a copy-paste plus a sentence.

Rule 4 exists because neither renderer reads the commands — both draw whatever the marker says. A chapter that changes profile without moving its marker ships a badge that states the wrong identity, which is worse than no badge at all: the room is now being told, in colour, that the --profile operator command on screen runs as the Domain admin.

Blocks are addressed by a twelve-character content hash rather than by position, so inserting a step into a lesson does not invalidate the declarations below it. A skip whose hash no longer matches anything is itself a failure — a stale exemption is an exemption nobody reviewed.

Reproduce locally with:

bash
go test ./tests/docs/ -run TestTrainingDeck
make demo-deck

Cross-references