--- name: documentation description: How Kopiur writes and maintains user-facing docs — the MkDocs Material site under docs/ and the example manifests under deploy/examples/. Use when adding or editing any doc page, writing or changing an example manifest, or whenever you change operator behavior that affects how someone deploys, uses, or configures Kopiur (CRD fields, defaults, Helm values, RBAC, reconciler UX, kopia/mover invocation). Encodes the painfully-clear-for-three-audiences rule, the examples-live-in-deploy/examples-and-are-snippet-included convention, the simple→complex example ladder, and the docs-change-with-the-code discipline. --- # Kopiur documentation norms Kopiur's docs are an **MkDocs Material** site whose source IS the `docs/` tree (`mkdocs.yml` sets `docs_dir: docs`). Page order and grouping live in the `nav:` block of `mkdocs.yml` — there is no SUMMARY.md; a `docs/*.md` left out of `nav:` still builds but is unlinked (and `--strict` warns). Example manifests live in `deploy/examples/` and are pulled into the prose at build time (pymdownx snippets); they are never copy-pasted into `.md` files. `mise run docs` builds the site with `mkdocs build --strict` — a broken cross-link or a missing nav file fails the build, so it doubles as our doc lint. The MkDocs + Material + pymdown-extensions toolchain is pinned in `pyproject.toml` / `uv.lock` and run via uv (mise installs uv). Material bundles mermaid.js and the admonition styling, so there are no preprocessor binaries or committed assets. ## The bar: painfully clear for three audiences Every user-facing page must let a reader who has never seen Kopiur succeed at **all three** of: 1. **Deploy** — how to install / apply this (Helm value, `kubectl apply`, scope, prerequisites). No "obviously you'd…" steps left implicit. 2. **Use** — the mental model first. Kopiur separates the **recipe** (`SnapshotPolicy`) from the **invocation** (`Snapshot`) from the **schedule** (`SnapshotSchedule`); say which one does what before showing fields. 3. **Modify common values** — call out the handful of fields a real user actually changes (bucket/prefix/endpoint, secret names, cron + jitter, retention, identity, deletionPolicy) and what each does. Don't make readers reverse-engineer the CRD to find the knobs. When in doubt, over-explain the "why", and prefer a worked example over a prose description. Use Material **admonitions** for the gotchas — alpha API, webhook-enforced constraints, version prerequisites. This repo uses the pymdownx **blocks** form (`/// note | Title` … `///`), NOT the classic `!!! note` form: the classic `admonition` extension is not even registered (`mkdocs.yml` wires up `pymdownx.blocks.admonition`), because the pre-commit `oxfmt` pass strips the 4-space body indentation `!!!` requires and silently breaks every such block. The blocks body stays at **column 0** and the block is closed with a `///` line: ```markdown /// warning | Lose the password, lose the backups The `KOPIA_PASSWORD` encrypts the repository. If you lose it, the backups are unrecoverable — kopia cannot decrypt without it. /// ``` Keep a blank line before the closing `///` (so oxfmt doesn't fold it into a preceding list item). The title is bare text after `| ` — not quoted. Registered types are the canonical Material set: `note`, `abstract`, `info`, `tip`, `success`, `question`, `warning`, `failure`, `danger`, `bug`, `example`, `quote` — `important` is **not** one; use `warning` for a webhook-enforced constraint. ## Examples are the backbone (simple → complex) Reach for an example for any non-trivial capability. The example set must form a ladder: the lowest-numbered examples are the canonical first run with the fewest moving parts; later ones layer in complexity (cluster scope, selectors, GitOps deploy-or-restore, discovered snapshots, fine-grained maintenance). Add new examples at the complexity tier they belong to, not just at the end. ### The hard rule: example YAML lives in `deploy/examples/`, never inline Docs include manifests with a pymdownx **snippet** marker so prose and apply-ready files cannot drift. The snippet path is repo-root-relative (snippets `base_path` is the repo root): ````markdown ## Example 09 — <1–3 sentences: what this shows and when you'd reach for it.> ```yaml --8<-- "deploy/examples/09-.yaml" ``` ```` ```` **Never** paste a literal multi-line manifest into a `.md` file. If you're about to, stop and make it a file under `deploy/examples/` instead. (Short inline `console`/`kubectl` snippets that aren't manifests are fine.) ### Example manifest conventions (match the existing files) - Filename `NN-kebab-name.yaml`, `NN` zero-padded and ordered by complexity. - A top-of-file comment block: what it demonstrates, the CRD type/field it maps to (verified against `crates/api`), and any caveats on field shapes. - `REPLACE_ME` for secrets the user must supply; a real-looking but obviously- placeholder value otherwise. - Inline comments on every non-obvious field — especially the "common values" above and any default being made explicit for teaching. - Backends are **externally tagged** (`backend.s3`, not `backend: { kind: S3 }`) — see [[kopiur-design]]. Apply-ready and self-contained (Secret + CRs in one file) so `kubectl apply -f` just works after filling in `REPLACE_ME`. ### Adding an example: which of the two homes it belongs to There are **two** conventions, and the number tells you which: **01–20 — the `docs/examples.md` ladder.** A three-touch change: 1. Create `deploy/examples/NN-name.yaml`. 2. Add a `## Example NN — …` section in `docs/examples.md` with the `--8<-- "deploy/examples/NN-name.yaml"` snippet inside a `yaml` fence. 3. Add a row to the table at the top of `docs/examples.md`. **21+ — capability-page inclusion.** The ladder is a *tutorial* sequence and stops at 20 deliberately; everything after it is a per-capability example that belongs beside the prose explaining that capability, not at the end of a tutorial. So a two-touch change: 1. Create `deploy/examples/NN-name.yaml`, usually with `# --8<-- [start:…]` / `[end:…]` section markers so the page can pull just the CR that teaches the point. 2. Snippet-include it from the capability page that documents the field (`docs/replication.md` for 19's replication CR, `docs/repositories.md` for the `spec.seed` examples, `docs/backends/s3.md` for 38, …). **Do not** add a `docs/examples.md` row/section for a 21+ example — that is how the same manifest ends up documented twice and the two copies drift. Either way the file must be reachable from *some* page: an example nothing includes is an orphan, and nothing in the build catches it. **Scenario bundles** are a third home: `deploy/examples/scenarios/NN-name.yaml`, one per `docs/scenarios/*.md` page, pulled by that page (whole-file or by section) and listed in the scenarios index table. They are typed-checked by `crates/api/tests/examples_match_crd_shapes.rs` like everything else. A new top-level **page** (not an example) also needs an entry in the `nav:` of `mkdocs.yml`, or it builds unlinked (and `--strict` warns). A new **scenario** page additionally needs its row in `docs/scenarios/index.md`. ## Docs change in the same PR as the behavior **Whenever you change logic that affects how someone deploys, uses, or configures Kopiur, update the docs in the same change.** This is not a follow-up task. Triggers that REQUIRE a docs/examples update: | You changed… | Update… | |---|---| | A CRD field / shape / default in `crates/api` | the relevant `docs/*.md` prose **and** any example manifest that uses it | | A Helm value or install scope (`deploy/helm`) | `docs/install.md` | | RBAC / SA / mover behavior | `docs/movers.md` | | Maintenance defaults or projection | `docs/maintenance.md` | | A webhook-enforced constraint | the affected page (state it as a `/// warning` admonition) + the example that would otherwise violate it | | A reconciler UX change (status, print columns, phases) | wherever that surface is described | Field shapes shown in docs/examples must be the real ones — the manifests are apply-ready, so a wrong field is a user-facing bug, not a typo. ## Verify before claiming done ```bash mise run docs # mkdocs build --strict (broken link / missing nav file = build fail) ```` The build also expands every snippet — a renamed/missing example file fails here (snippets `check_paths` is on). `--strict` additionally validates heading anchors, so a deep-link to a `#section` that no longer exists fails too. Don't claim a docs change is done without a green `mise run docs`. For a fast inner loop without the rustdoc nesting, use `mise run docs-serve` (live-reload `mkdocs serve`). If you added/changed a manifest, the field shapes should be the same ones the tests in `crates/api` accept (see [[kopiur-design]] for the `from_yaml` parse path); a manifest that wouldn't survive admission is wrong even if the site builds. ## Common mistakes - Pasting YAML inline in a `.md` instead of snippet-including a file → drift. - Adding a **01–20** ladder example but forgetting the `docs/examples.md` table row or the section (or vice-versa) → orphaned file or dead reference. - Adding a **21+** example to the `docs/examples.md` ladder as well as its capability page → the same manifest documented twice, and the copies drift. - New page not added to the `nav:` in `mkdocs.yml` → builds unlinked. - Writing an admonition in the classic `!!! note` form (4-space-indented body) instead of the `/// note | Title` … `///` blocks form → the title renders as literal text and the indented body becomes a code block (it does not error — eyeball the rendered page or grep the built HTML for `class="admonition`). - Documenting the field list without the mental model or the "which values do I actually change" guidance → technically complete, practically useless. - Shipping a behavior change with "docs later" → the docs now lie. Same PR. - Internally-tagged backend in an example → won't admit; contradicts the CRDs.