Documentation Style Guide¶
Canonical reference for all Cloud-Native DCS documentation. Applies to human and AI authors.
What the Docs Gates Read¶
Every gate described here reads the files git can see: tracked files, plus
untracked files no ignore rule matches. A new doc is therefore linted before
it is staged, and a gitignored path under docs/ is exempt.
That exemption is the point. docs/ holds several gitignored trees: the
derivative notes on the purchased standards (docs/isa-88/, docs/mtp/, and
their siblings), and any *.local.md override. Those are scratch, local to
one machine, and CI cannot reproduce a finding in one. When the gates walked
the working tree instead, a scratch note without front matter failed
make lint-docs-style on every commit in the repo, in every session, and no
git stash could clear it (#1391).
Enumeration lives in one helper per language: scripts/lib/docs-files.sh,
scripts/docs-shots/lib/git-visible.mjs, hack/gitfiles.py. A new docs gate
uses one of them. find, grep -r, readdirSync, and rglob all walk the
working tree. That is the #1391 bug restated.
Front Matter¶
Every doc starts with YAML front matter:
---
title: Human-Readable Title
category: tutorial | how-to | reference | explanation
related:
- docs/other-doc.md
- docs/another.md
---
Categories follow the Divio documentation framework:
| Category | Purpose | Example |
|---|---|---|
| tutorial | Learning-oriented walkthrough | E2E LED Test |
| how-to | Task-oriented guide | Alarm Management |
| reference | Information-oriented lookup | REST API Reference |
| explanation | Understanding-oriented discussion | Architecture |
There is no other valid category: value (runbooks and incident
reports are how-to, and compliance traceability docs are reference).
make lint-docs-style enforces the enum, the presence of title:,
and that the H1 matches title.
Hardcoded Counts¶
Never hardcode counts that change as the project grows. "18 CRDs" and
"5 binaries" go stale on the next scaffold, so write "all CRDs" and
"all binaries". If an exact count is genuinely useful, generate it from
source (e.g., ls config/crd/bases/ | wc -l).
Terminology¶
Follow docs/terminology.md for user-facing terms. Key rules:
- "control system" not "cluster" in user-facing docs
- "edge device" not "Raspberry Pi" when speaking generically
- "unit runtime" not "sidecar"
- "process cell" not "cell"
Prose¶
Everything else in this guide is structural and mostly lint-backed. The
writing itself is governed by the org-wide prose core
(cndcs-claude-harness/portable/docs/writing-style.md, injected at
SessionStart by the harness-writing-style plugin): complete sentences,
the em-dash zero, colon and semicolon discipline, contrast and negation
rules, and the reference-scaffolding carve-out.
make lint-docs-prose enforces the mechanical share (#1518, #1519), and
it is strict: every non-exempt page must be clean outright. The #1518
sweep (the founder-directed exception to the core's no-bulk-rewrites
default) brought the whole site into conformance, and #1528 deleted the
migration baseline that carried the transition.
Two registered deltas narrow the core for this surface, both founder rulings recorded in #1518 and in the core's exception table:
- Dated records are exempt. An accepted ADR, a dated compliance
drill evidence record, or a banner-marked historical design doc that
predates the gate is a signed-in-time record and stays as written
(the ADR ruling in #1518, widened to the other two classes by the
#1528 ruling). The frozen list is
scripts/.docs-prose-adr-exempt, nothing is ever added to it, and a new ADR or drill record is linted from its first commit. Amending an old record for another reason brings its prose with it. - The em dash survives as a scaffolding separator. A heading, a
table cell, or a list item may carry the
**Term** — descriptionglyph. Flowing prose keeps the flat ban.
One mechanical exemption follows from the core's quoted-and-generated rule: a whole-line HTML comment is exempt from every prose rule, since a comment never reaches a reader (the canonical doc-video embed comment carries a semicolon by design). An inline comment beside prose exempts nothing.
The audience is engineers evaluating or operating the product, and prose
quality is a credibility surface for them. Documentation that reads like
unrevised machine output undercuts trust in the product it describes.
The buyer-facing treatment of these ideas lives in
cndcs-marketing/STYLE.md.
Docs-specific voice and scaffolding¶
- "We" is acceptable only in explanation docs when stating a design decision and its rationale. Everywhere else, name the product or the component as the actor.
- A single scope-setting sentence at the top of a doc is fine. Running narration ("in this section", "as mentioned above") is not.
- Reference scaffolding here additionally includes code-adjacent
instruction lines ("Apply with
dcs apply -f <file>.yaml"), one-line admonitions, UI-gap placeholder notes, front matter, tab labels, and image alt text. Fragments are the correct register in all of them.
Captions¶
A caption identifies the figure. It does not restate the argument the body already makes.
- One phrase or short sentence. Two short sentences only when the figure genuinely has two beats.
- Name the real view and the objects on screen ("The batch detail view with the granulation phase held"). The drawing notation and the previous caption's text are both the wrong source. Write from the rendered image and the section it supports.
- Punctuation is tighter than body prose: no em dashes, no semicolons, and a colon only for a named-count enumeration ("three views: ...").
- Alt text is not a caption copy. The
altdescribes what the image shows for assistive tech (the view, the key objects, the state on screen), and the caption orients the reader in the argument. Write both. Never paste one into the other.
Code Examples¶
Use pharma-realistic values in examples (e.g., granulator-1, site-newark-plant,
reactor-temp-high). Avoid generic names like foo, bar, test-1.
For docs covering functionality inside the application (the Batch Operations and Explanation sections), follow the Tabbed Alternatives rules below. UI is the primary interface for this audience.
For docs covering platform / SRE / integrator concerns (Platform Administration, Integration, Reference, Compliance), YAML / CLI / API are the correct primary form. No UI tab is required. Keep examples in the most appropriate single transport for the task.
Tabbed Alternatives¶
In the Batch Operations and Explanation sections of the site, every user-facing action step should offer the reader a choice of interface. Most users of these docs are UI-first operators and engineers who will not be comfortable with YAML until they are much deeper into the system. The tabs must meet them where they are.
Scope¶
In scope (the Batch Operations and Explanation nav sections of
mkdocs.yml):
docs/batch-operations/index.md,docs/equipment-operation.md,docs/batch-execution.md,docs/alarm-management.md,docs/io.md,docs/control-modules.md,docs/phases.md,docs/recipes.md,docs/data-interface.md,docs/historian.mddocs/library/mental-models.md,docs/library/adaptation-guide.md,docs/library/parameter-binding.md,docs/library/alarms-and-interlocks.md,docs/library/debugging-running-batches.md
Out of scope: Platform Administration, Integration, Reference,
Compliance, Getting Started, Project, and the Libraries section
of shipped templates. Those audiences are platform-savvy and a
CLI-first or YAML-first presentation is correct.
Which tabs to show¶
Pick tabs by the shape of the action. The CLI / API / YAML tabs collapse into "three transports for the same manifest" for most CRUD steps, and showing all three is redundant padding that rots.
| Action type | Tabs | Example |
|---|---|---|
| Create / edit a resource | UI | YAML | Apply a PhaseTemplate, ControlModule, MasterRecipe |
| Imperative command | UI | CLI | Approve a recipe, abort a batch, issue an ISA-88 command |
| Observe / read | UI | CLI | Watch live batch state, list units |
| Workflow (CLI does more than apply) | UI | CLI | YAML | dcs simulate (which creates a Batch, waits, and cleans up) vs. a raw Batch manifest |
| API has semantics CLI / YAML can't express | UI | CLI | API | Streaming / SSE / WebSocket endpoints |
The API tab is rare. Reserve it for endpoints the CLI cannot express. For ordinary CRUD, a single once-per-page footnote like "Integrators: see API Reference for the REST equivalents" is enough, and per-step API tabs are almost always duplication.
Authoring the tabs¶
Tabs use the pymdownx.tabbed syntax already enabled in mkdocs.yml:
=== "UI"
`/system` → Equipment Library → Phases → pick site
`riverbend` → **+ New Phase Template**. Paste the manifest
and **Save**.
=== "YAML"
<!-- lint-examples:allow: reference snippet showing the structure with placeholders, not an applyable manifest -->
```yaml
apiVersion: procedural.dcs.io/v1alpha1
kind: PhaseTemplate
...
```
Apply with `dcs apply -f <file>.yaml`.
Tabs in the same group must sit at column 0 with their content indented by four spaces. A non-indented, non-blank line ends the group.
Placeholder for UI paths that aren't implemented yet¶
When the tab-set rules call for a UI tab but the UI path genuinely does not exist yet, still show the UI tab with a placeholder admonition. A missing tab silently tells UI-first readers that they are not the target audience. A placeholder tells them honestly that the gap is known and tracked.
=== "UI"
!!! info "Not yet in the UI"
No UI path for this step yet — tracked in
[#NNN](https://github.com/cloud-native-dcs/cloud-native-dcs/issues/NNN).
Use the CLI / YAML tab below.
=== "CLI"
```bash
dcs --site riverbend masterrecipe approve my-recipe
```
Conventions:
- Use
!!! info: the placeholder marks a roadmap gap, and!!! warningwould dress it as an error state. - Always link a tracking issue. Un-linked placeholders become permanent technical debt that nobody reviews.
- Point the reader at the working tab(s).
For steps where the UI path exists but hasn't been documented yet, use the same admonition form with wording like "UI path exists but not yet documented — see [#NNN]". This distinguishes a missing feature from a missing doc so future contributors fix the right thing.
Lint enforcement¶
Three checks run in make verify and the pre-commit hook for staged
docs/*.md files:
make lint-docs-ui-tabs— every tab group in an in-scope file containing=== "CLI",=== "API", or=== "YAML"must also contain=== "UI".make lint-docs-untabbed-actions— covers every doc except the Platform Administration section (platform / Helm plumbing is exempt). Rules:```yamlblocks must live inside a tab group (e.g.=== "UI"/=== "YAML") everywhere they appear. An application CR the reader might copy should show both the UI and YAML path.```bash/```shell/```sh/```consoleblocks are flagged only inside H2 sections that already carry at least one tab group, so pure reference prose without any tabs is unaffected.
make lint-docs-ui-tab-shots— every=== "UI"tab must carry a screenshot reference (or a placeholder admonition).
For pre-existing tab groups that predate the UI-tabs check, place an
HTML comment marker within four lines above the group's first ===
line to opt out:
<!-- lint-docs-ui-tabs:allow (UI gap acknowledged in prose above; tracked in #NNN) -->
=== "CLI"
...
The lint-docs-untabbed-actions:allow marker has a deliberately
high bar: use it only when the YAML has no UI counterpart and we
don't plan to add one. Anti-patterns, CRD-shape snippets, and spec
fragments that describe a field the UI exposes are all actions. Put
them in UI | YAML tabs.
Qualifying cases for the marker:
- Platform / Helm values (
mqtt:,changeControl:, cluster-scopedvalues.yamlsnippets): a platform operator authors these during install, outside the DCS UI. Run-section docs are already out of lint scope. Helm values that appear elsewhere still need the marker. - API / schema reference (
docs/api-reference.mdrequest and response bodies, MkDocs front-matter schema): by definition the reference is the raw shape. - Third-party language syntax (
docs/structured-text.mdST embedded inside a YAML transition) where the YAML is the vehicle for a language example.
Side-by-side comparisons are NOT a qualifying case. When two
variants of a CR need contrasting (sim vs real, before vs after,
small vs large), use two consecutive tab groups that share a single
screenshot in their UI tabs and put the YAML differences in their
YAML tabs. A summary differences table between the two blocks gives
the reader the at-a-glance comparison without forcing them into raw
YAML. See docs/io.md "Authoring an IOModule" + "Sim vs real — what
changes" for the canonical pattern.
Placement: within the four lines immediately above the fenced block:
<!-- lint-docs-untabbed-actions:allow: Helm values, not an application CR -->
```yaml
# ...
Each marker must state the reason. A marker is not a blanket waiver.
If you add one, be prepared to defend it under this checklist.
### Content-view tabs (FBD / ST)
The UI / CLI / API / YAML tab axis covers *which interface* the reader
uses. A second, orthogonal tab axis covers *which content view* a
control-module template's logic is rendered in: **FBD** (Function
Block Diagram) or **ST** (Structured Text). These are equivalent
authoring forms, and both produce the same `ControlModuleTemplate` /
`ControlProgram` resource. Wherever a template's logic appears in
prose or as a screenshot, the reader should be able to flip between
the two views the same way they do in the gateway UI.
Use a separate top-level `=== "FBD"` / `=== "ST"` group at the same
level as the interface tabs. Path placeholders below use `<stem>` to
mean the template's filename root (e.g. `discrete-valve`, `pid-loop`).
The actual references in the library use the real stems with a `.png`
extension.
```markdown
=== "FBD"
{ .fbd }
=== "ST"
{ .st }
Conventions:
- Order: FBD first, ST second. Matches the gateway's default view on the template detail page and the section header toggle.
- Don't nest FBD/ST inside a UI/YAML group. Two flat groups read more cleanly and the screenshots stay full-width.
- YAML examples that materially differ between the two forms
(
blocks+connectionsvs.structuredText) belong in their own FBD/ST group, separate from the UI/YAML tab group used for the CRD-shape walkthrough above. Don't try to show all four combinations. Pick one form for the walkthrough and link to the other. - The FBD/ST labels are not in the UI-tabs lint's enforcement set, so
using them does not require a
=== "UI"companion tab.
Skip the FBD/ST group when only one form is meaningful, for example the FB Network Editor section (the visual editor is FBD-only) or the ST language reference (no FBD counterpart for the syntax tables).
Headings¶
- H1: Title only (one per doc, matching front matter
titleexactly, enforced bymake lint-docs-style) - H2: Major sections
- H3: Subsections
- H4: Rare, and only for deeply nested content
- Never skip heading levels (e.g., H2 directly to H4)
Cross-References¶
- Use relative links between docs:
[Architecture](architecture.md). Absolute URLs are for external sites only - A "Related Documentation" footer is optional, but when present it must use that exact heading (see Related Documentation Footer)
- Link to compliance traceability docs when documenting standard-related features:
[ISA-88 traceability](compliance/isa88.md)
Anchors¶
A #fragment is slugged the way the published site slugs it. The site is
the artefact a reader follows the link on, so an anchor that works only in a
GitHub blob view is broken where the documentation is actually read.
make lint-docs-links resolves every fragment against that slug and names the
closest heading when one misses.
The two slugifiers agree on almost everything. They part company on a heading whose punctuation sits between two spaces, because the site collapses the run into one separator and GitHub keeps one per removed character. Copying an anchor out of a GitHub heading link is how four broken ones reached the tree. Derive it from the heading text instead.
#### `activeRetentionDays` — etcd / kine sizing
[sizing](capacity-planning.md#activeretentiondays-etcd-kine-sizing)
Two other forms resolve. A heading may carry an explicit id with attr_list
(## A heading {#chosen-anchor}), which is worth using when the wording is
likely to change under an anchor other pages already point at. Two headings that
slug alike get _1, _2 and so on in document order, and a link to the second
one names it that way.
Disabled Features¶
Some features are intentionally disabled in the product (removed from UI, CLI, and API) even though the underlying CRDs or code may remain. Docs must not mention them, because a mention misleads users into trying features that don't work.
The docs/.disabled-terms.tsv file is the authoritative list, and each row
is <pattern>\t<reason>. scripts/lint-disabled-features.sh greps the docs
git can see (see § "What the Docs Gates Read") against it on every pre-commit
and in CI. A snapshot at
scripts/.disabled-terms-snapshot grandfathers pre-existing mentions so the
lint only fails on new violations. The baseline only ratchets down.
Workflow:
- You want to remove a mention: edit the doc, then
scripts/lint-disabled-features.sh --updateto shrink the snapshot. - A feature was newly disabled: add a rule to
docs/.disabled-terms.tsv, then--updateto record the current state. Clean up mentions over time. - A feature was re-enabled: delete the rule, then
--updateto prune stale snapshot entries.
Icons¶
Unicode symbols only (per CLAUDE.md). Never use emoji.
Good: ⚙ ◆ ☑ ✎ ◉ Bad: (no emoji characters)
Compliance References¶
When documenting a feature related to a standard (ISA-88, IEC 61131-3, 21 CFR Part 11, IEC 62443), include a note linking to the traceability doc:
> **Compliance**: This feature implements ISA-18.2 alarm state model.
> See [ISA-88 traceability](compliance/isa88.md) for requirements mapping.
Tables¶
- Use Markdown tables for structured data
- Align columns for readability in source
- Include a header row
Diagrams¶
- Use Mermaid fenced code blocks (
```mermaid). The site renders them via thepymdownx.superfencescustom fence configured inmkdocs.yml. - Default to
flowchart TDfor topologies and pipelines,stateDiagram-v2for state machines, andsequenceDiagramfor interactions. - Quote node labels (
["..."]) and use<br/>for line breaks. Put edge explanations on the edges themselves with-->|text|. - Keep nodes to a short title plus one or two descriptive lines. Push per-layer bullets into a bulleted list under the diagram.
- Do not use ASCII box-drawing characters (
┌,│,─,▶, …) to build diagrams. They render as fixed-width blobs, break screen readers, and diverge from the Mermaid-rendered look of the rest of the site. Reserve ASCII for short inline fragments where a real diagram would be overkill. scripts/lint-docs-diagrams.shgreps the docs git can see (see § "What the Docs Gates Read") for box-drawing characters and fails on any match. The pre-commit hook and CI run it automatically when markdown underdocs/is staged.
Diagram accuracy¶
Diagrams make factual claims and are held to the same accuracy bar as code examples. Every name in a diagram must be real:
- CRD kinds, API groups, and field names exactly as generated in
config/crd/bases/. Never an invented illustrative schema or a placeholder kind. - Component and binary names as they exist under
cmd/. - ISA-88 state and command names exactly as defined in
pkg/statemachine/. - Depicted relationships (who creates, watches, owns, or approves what) must match the controller code.
A diagram may simplify by omitting. It may not invent. When a diagram
depicts a contract the code implements, wrap the region in a
contract marker so make check-docs-reverse flags
drift.
Diagram theme¶
The default Mermaid theme is the standard, and meaning is carried by node labels and edge labels alone:
- No
classDef,style, or inline color directives to encode semantics. A reader must get the full meaning from the text alone. - A diagram color grammar (reserved colors and strokes with registered meanings, like the marketing site's symbol registry) is a deliberate design effort with its own review tooling. Do not introduce one ad hoc. If a need arises, file an issue and design it as a system.
Video Clips¶
Short UI walkthroughs are embedded as raw HTML <video>, with no
MkDocs plugin involved. MkDocs has no video extension we depend on, and a
<video> element is small, well-understood, and styles cleanly against the
theme. A plugin (mkdocs-video or similar) would buy nothing over the
snippet below and grows the build's dependency surface. Keep it raw HTML.
The clip binaries are not committed. They are recorded from
docs/videos/*.clip.yaml, published to Cloudflare R2, and served from
media.cndcs.io (see scripts/docs-shots/README.md and the recorder
pipeline, #658). Animated conceptual diagrams ship through the same
pipeline and embed identically. A diagram: clip spec renders a committed
HTML/SVG source deterministically, with no live surface to film
(ADR 0030, authoring contract in scripts/docs-shots/README.md
§ "Animated diagrams"). Embeds reference those stable URLs by convention:
https://media.cndcs.io/<tier>/videos/<stem>.<ext>, where <tier> is
docs (COI-gated, the default) or marketing (public, for marketing-tagged
clips). The recorder emits four artifacts per clip: <stem>.mp4 (the source),
<stem>.gif (animated fallback), <stem>.jpg (a still poster frame), and an
intermediate <stem>.webm.
Take the tier from docs/videos/manifest.json. The publish step uploads
each artifact to exactly one key and records the resulting URL in that
committed manifest. A doc may embed a marketing-tier clip, and several do,
but it has to name the tier the clip was published to, because no copy exists
at the other one. Copying a neighbouring snippet without checking the manifest
is how #1304 happened. The failure is total: the .jpg poster and the .gif
fallback 404 alongside the .mp4, so the reader gets an empty region with no
still to fall back on. make lint-docs-clips fails any tier that disagrees
with the manifest.
Canonical embed¶
<figure class="doc-video">
<video controls muted playsinline preload="none"
poster="https://media.cndcs.io/<tier>/videos/<stem>.jpg">
<source src="https://media.cndcs.io/<tier>/videos/<stem>.mp4" type="video/mp4">
<img src="https://media.cndcs.io/<tier>/videos/<stem>.gif"
alt="<describe what the clip shows, same detail as a screenshot alt>">
</video>
<figcaption>One line on what the reader is watching and where.</figcaption>
</figure>
Conventions:
-
All three URLs carry the same
<tier>, the one the manifest records for that stem. -
poster(the.jpg) is required. It keeps the gated/remote clip from showing a blank box before play. Withpreload="none"the heavy MP4 isn't fetched until the reader hits play, so the poster is all they see at rest. <source>points at the.mp4, and the<img>GIF is the in-<video>fallback. To prove the clip is referenced,lint-docs-clipsmatches the.mp4or.gifURL. Always keep one of them present.controls muted playsinline—muted+playsinlineare required for mobile inline playback. Never addloop. The site script (docs/javascripts/doc-video-autoplay.js) starts each clip when its figure scrolls into view, plays it ONCE, and lets a click replay it. A looping video never firesended, which breaks that play-once contract. The docs clips are narratives with endings, so a restarting loop reads as a glitch. Readers withprefers-reduced-motionkeep click-to-play..doc-videois the only class — it's styled indocs/stylesheets/extra.css. Do not invent per-page video classes.- Write a real
altandfigcaption, at the same level of detail you'd give a screenshot. Thealtdescribes the clip for assistive tech, and the caption orients the reader. - Inside a
=== "UI"tab, indent the whole block four spaces like any other tab content. A clip does not replace the static screenshots a UI tab needs (make lint-docs-ui-tab-shots). It complements them.
make lint-docs-clips warns when a docs/videos/*.clip.yaml spec records a
clip that no doc embeds. Referencing the stem's .mp4/.gif URL as above
satisfies it.
Eliding waits¶
A clip whose honest wall time is dominated by waiting (image pulls, rollout
waits, a scheduler tick) may elide that waiting with a cut: step in its
spec (#1275). The cut wraps exactly the wait it removes:
- term.run: kubectl get pods -w
- cut:
term.waitFor: { pattern: 'Running', timeoutMs: 600000 }
The camera keeps rolling while the wrapped wait runs. At encode time the recorder drops the captured frames between the cut's start and the moment the wait resolved, then concatenates the surrounding footage in capture order. Every frame in the published clip is real, unedited capture of the real system. A cut removes time and nothing else: no reordering, no synthesis.
Eliding is appropriate only when the removed time is pure waiting whose
completion is verifiable on screen: the wrapped wait's condition (the pod
turning Running, the status chip appearing) is visible in the frames on
either side of the splice, and the spec records exactly what was skipped.
Never use a cut to tighten the pacing of interactions, and never to imply a
speed the product does not have. The mechanism enforces the first half of
that rule by construction. A cut may wrap only wait-class steps (settle,
wait, pause, the waitFor* family, a terminal waitFor), and
make lint-docs-clips rejects anything else.
Every elision must be signalled, and the recorder does this automatically: after the wait resolves it holds a small "… 4m 32s of waiting elided" notice on camera for about a second, so the cut never reads as "this happened instantly" and the removed wall time is on record in the footage itself. Do not crop or obscure the notice. When the waiting is itself the claim (a failover time, a rollout duration), state the real duration in the caption or the surrounding prose. The shortened clip cannot carry that claim.
Contract Markers¶
Some documentation describes a contract the code implements: a state transition table, an API surface, an audit-trail field list. When that section changes, the corresponding code must be re-checked. Mark such regions with paired HTML comments referencing a contract ID:
<!-- contract:isa88-state-machine -->
| From State | Start | Stop | ... |
| ... |
<!-- /contract:isa88-state-machine -->
Each contract ID is opted into reverse-detection by listing it in a rule
in docs/doc-map.json:
{
"category": "statemachine",
"patterns": ["pkg/statemachine/**/*.go"],
"docs": ["docs/compliance/isa88.md"],
"contracts": ["isa88-state-machine"]
}
make check-docs-reverse (hack/check-docs-reverse.py) runs git-diff
against the doc. When a changed line falls inside a contract region whose
ID is referenced by a rule, the rule's patterns are reported as code to
review. If any changed file matches one of the patterns, the rule is
considered satisfied. The assumption is that you already inspected the
implementation while editing both sides.
Guidelines for contract markers:
- Wrap only the section that maps to code. Editorial tweaks to the surrounding introductory text should not trip the check.
- One contract ID per rule is the common case. A doc may host multiple distinct regions sharing the same ID (the check unions their ranges).
- Pick a short, kebab-case ID that describes what is contracted:
isa88-state-machinenames the contract, whilecompliance-isa88-section-7names a doc location that will move.
File Naming¶
- Lowercase with hyphens:
alarm-management.md - Match the primary topic:
mqtt-telemetry.mdnotmqtt-client-architecture.md
Related Documentation Footer¶
A related-links footer is optional. Don't pad pages that have nothing useful to link. When a doc does carry one, the section must be titled exactly:
## Related Documentation
- [Doc Title](relative-link.md) -- one-line description
Variant headings ("See also", "Related", "Where to go next", "Next
steps", "What next?", "Further reading", ...) are not allowed.
scripts/lint-docs-style.sh (make lint-docs-style) rejects them so
the footer reads identically across the site. A short prose lead-in
under the heading is fine. The heading text is what's standardized.