# Docs Contribution Guide: [Product or docs site name]

> Contributing guide for documentation that lives in a Git repository and ships through the same review and CI flow as code (docs as code). It covers repository layout, branch and pull request flow, local preview, the front matter schema, prose linting with Vale (v3 configuration, checked against Vale v3.25.1, October 2026), Markdown linting with markdownlint-cli2 (v0.24.0, markdownlint 0.42.0), link checking, images and alt text, code samples, review and merge, and how documentation is released. Content types follow the four Diátaxis forms (diataxis.fr); accessibility rules target WCAG 2.2 (W3C Recommendation, 12 December 2024). Replace every `[bracketed]` value and every row marked *Example*. Worked examples use Corvid Commerce, a fictional SaaS retailer, and its developer docs site.

<!--
Guidance: How to use this template
- This guide is for people who change documentation. Code contribution rules
  (build, tests, commit style for source code) belong in Contributing
  (`dev/contributing`); link to it instead of repeating it.
- Write it for a first-time contributor who has never seen the repository.
  Every command in it must run as written on a clean checkout.
- Keep the configuration files (Vale, markdownlint, link checker) in the
  repository and quote them here, so the guide and CI never disagree.
- Review this guide whenever a linter version, the docs toolchain or the
  release process changes.
-->

## Document control

<!-- Guidance: Name the docs owner who keeps this guide true, and record the tool versions the guide was written against. -->

| Field | Value |
|---|---|
| Document ID | [DOCS-CONTRIB-001] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Superseded] |
| Owner | [@docs-lead] |
| Approvers / reviewers | [@docs-platform-engineer], [@developer-relations-lead], [@engineering-manager] |
| Last updated | YYYY-MM-DD |
| Classification | [Public / Internal] |
| Docs repository | [*Example:* `corvid/developer-docs`] |
| Site generator | [*Example:* static site generator with Markdown sources] |
| Tool versions | [*Example:* Vale 3.25.1, markdownlint-cli2 0.24.0, lychee 0.24.2] |
| Next review | YYYY-MM-DD (and after any toolchain upgrade) |
| Related | Contributing (`dev/contributing`), Technical Writing Style Guide (`docs/style-guide-technical`), Termbase (`docs/termbase`) |

### Approvals

| Role | Name | Decision | Date |
|---|---|---|---|
| Documentation lead | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Docs platform engineer | [@name] | [Decision] | YYYY-MM-DD |
| Engineering representative | [@name] | [Decision] | YYYY-MM-DD |

## Revision history

<!-- Guidance: Record changes to rules, tool versions and the review flow, not typo fixes. -->

| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft |
| 0.2 | YYYY-MM-DD | [@name] | *Example:* Vale vocabularies moved to `config/vocabularies` for Vale v3 |
| 1.0 | YYYY-MM-DD | [@name] | *Example:* Approved; CI checks made required on `main` |

## Contents

<!-- Guidance: Keep aligned with the headings. -->

1. [Purpose and scope](#1-purpose-and-scope)
2. [Roles and ownership](#2-roles-and-ownership)
3. [Repository layout](#3-repository-layout)
4. [Branch and pull request flow](#4-branch-and-pull-request-flow)
5. [Local setup and preview](#5-local-setup-and-preview)
6. [Front matter schema](#6-front-matter-schema)
7. [Content types and writing rules](#7-content-types-and-writing-rules)
8. [Prose linting with Vale](#8-prose-linting-with-vale)
9. [Markdown linting with markdownlint-cli2](#9-markdown-linting-with-markdownlint-cli2)
10. [Link checking](#10-link-checking)
11. [Images, diagrams and alt text](#11-images-diagrams-and-alt-text)
12. [Code samples](#12-code-samples)
13. [Review and merge](#13-review-and-merge)
14. [Docs definition of done](#14-docs-definition-of-done)
15. [Releasing and versioning the docs](#15-releasing-and-versioning-the-docs)
16. [Glossary and references](#16-glossary-and-references)

## 1. Purpose and scope

<!-- Guidance: Say who may contribute, what kinds of change are welcome and what is out of scope. Pitfall: a guide that only addresses staff writers and scares away engineers who fix one sentence. -->

This guide explains how to propose, check, review and publish a change to the [Product name] documentation. It applies to every page under `docs/`, to shared snippets, images and diagrams, and to the configuration that builds the site.

Contributions of every size are welcome: a typo fix, a corrected command, a missing parameter in a reference page, a new how-to guide or a full tutorial. Small fixes need only a pull request. New pages and structural changes (new sections, moved pages, changed navigation) need a short issue first so the docs owner can confirm where the page belongs in the information architecture (`docs/information-architecture`).

Out of scope for this guide: changes to product source code (see Contributing (`dev/contributing`)), API description files that are generated from code, marketing pages, and legal texts, which have their own owners and approval paths.

| Change type | Issue first? | Typical reviewer | Expected review time |
|---|---|---|---|
| Typo, broken link, formatting | No | Any docs maintainer | [*Example:* 2 working days] |
| Correction of technical content | No | Subject matter expert plus docs maintainer | [*Example:* 3 working days] |
| New page or major rewrite | Yes | Docs lead plus subject matter expert | [*Example:* 5 working days] |
| Navigation, URL, redirect or toolchain change | Yes | Docs lead plus docs platform engineer | [*Example:* 5 working days] |

## 2. Roles and ownership

<!-- Guidance: Name roles, not only people, and say how ownership is enforced (CODEOWNERS). Pitfall: no owner for a section, so pull requests wait forever. -->

| Role | Responsibility | Who |
|---|---|---|
| Contributor | Proposes changes, runs local checks, answers review comments | Anyone |
| Docs maintainer | Reviews style, structure and Markdown; merges approved changes | [@docs-maintainers] |
| Subject matter expert (SME) | Confirms technical accuracy for a product area | [Per area, see CODEOWNERS] |
| Docs lead | Owns the information architecture, style guide and this guide | [@docs-lead] |
| Docs platform engineer | Owns the site build, CI checks, preview deployments and redirects | [@docs-platform] |
| Localisation coordinator | Owns translation handoff for changed pages | [@l10n-coordinator] |

Ownership is enforced with a `CODEOWNERS` file (see Code Ownership Policy (`dev/code-ownership-policy`)). Each product area maps to an SME team and to the docs maintainers, so both are requested automatically.

```text
# .github/CODEOWNERS (Example)
/docs/                         @corvid/docs-maintainers
/docs/checkout/                @corvid/docs-maintainers @corvid/checkout-team
/docs/orders-api/              @corvid/docs-maintainers @corvid/orders-team
/.vale.ini                     @corvid/docs-platform
/.markdownlint-cli2.jsonc      @corvid/docs-platform
/.github/styles/               @corvid/docs-platform @corvid/docs-lead
```

If a page has no clear SME, the contributor names one in the pull request and the docs lead confirms it. A page without an accountable owner is flagged in the next content audit (`docs/content-audit`).

## 3. Repository layout

<!-- Guidance: Show the real tree and explain every top-level folder. Pitfall: a tree that is two reorganisations old. -->

The documentation sources, the site configuration and the quality checks live in one repository so that a single pull request can change all three.

```text
developer-docs/                      (Example)
├── docs/
│   ├── index.md                     landing page
│   ├── tutorials/                   learning-oriented lessons
│   ├── how-to/                      task-oriented guides
│   ├── reference/                   API, CLI and configuration reference
│   ├── explanation/                 background and concepts
│   └── _snippets/                   reusable fragments, never published alone
├── static/
│   └── images/<section>/            screenshots and diagrams (SVG preferred)
├── .github/
│   ├── CODEOWNERS
│   ├── workflows/docs.yml           CI checks for docs
│   └── styles/                      Vale StylesPath
│       ├── Corvid/                  house rules (YAML)
│       └── config/vocabularies/Corvid/
│           ├── accept.txt
│           └── reject.txt
├── .vale.ini
├── .markdownlint-cli2.jsonc
├── lychee.toml                      link checker configuration
├── redirects.yaml                   old path to new path
└── site.config.[ext]                site generator configuration
```

Rules for placing files:

1. Put each page in the folder that matches its content type (section 7). A page that mixes types is split before merge.
2. Name files in lowercase kebab case, matching the last URL segment: `docs/how-to/rotate-api-keys.md` is served at `/how-to/rotate-api-keys`.
3. Store images next to their section under `static/images/` and never hotlink images from other sites.
4. Put text that appears on more than one page in `_snippets/` and include it; do not copy it.
5. Never rename or move a published page without adding a redirect in `redirects.yaml` in the same pull request.

## 4. Branch and pull request flow

<!-- Guidance: Describe the exact flow, including forks for external contributors. Align with the Branching Strategy (`dev/branching-strategy`). Pitfall: docs changes that bypass the checks because they "are only text". -->

Docs follow the same trunk-based flow as code: short-lived branches from `main`, a pull request per change, required checks, and squash merge.

```mermaid
flowchart LR
    A[Issue or idea] --> B[Branch from main]
    B --> C[Edit and preview locally]
    C --> D[Run Vale, markdownlint, link check]
    D --> E[Open pull request]
    E --> F[CI checks and preview deployment]
    F --> G{Reviews}
    G -- changes requested --> C
    G -- SME and docs approval --> H[Squash merge to main]
    H --> I[Site publishes from main]
```

1. Create a branch named `docs/<short-topic>` (for example `docs/refund-webhook-retry`). External contributors fork the repository and open the pull request from their fork.
2. Keep one topic per pull request. A typo sweep across many pages is one topic; a new tutorial plus a navigation change is two.
3. Write commit messages in the imperative mood with a `docs:` prefix, for example `docs: add retry table to refund webhook reference`.
4. Fill in the pull request template (Pull Request (`dev/pull-request`)): what changed, why, which product version it describes, how it was verified, and screenshots of the rendered preview for layout changes.
5. Link the issue, the product change (pull request or ticket) that the page documents, and any related localisation request.
6. Mark the pull request as draft until the local checks pass.

Docs that describe an unreleased feature are merged behind the release process in section 15, not before the feature ships.

## 5. Local setup and preview

<!-- Guidance: Give copy-paste commands for a clean machine and the expected output. Pitfall: steps that only work on the docs lead's laptop. -->

Install the tools once, then preview every change before you push. The versions below are the ones CI uses; using the same versions avoids results that differ between your machine and the pipeline.

| Tool | Version used by CI | Purpose | Install source |
|---|---|---|---|
| [Runtime for the site generator] | [*Example:* Node.js 24 LTS] | Builds and serves the site | Official installer or version manager |
| Vale | 3.25.1 | Prose linting | vale.sh install page or package manager |
| markdownlint-cli2 | 0.24.0 | Markdown linting | npm package `markdownlint-cli2` |
| lychee | 0.24.2 | Link checking | GitHub releases or package manager |

```bash
# Example: first-time setup on a clean checkout
git clone https://github.com/corvid/developer-docs.git
cd developer-docs
npm ci                      # installs the site generator and markdownlint-cli2
vale sync                   # downloads the Vale packages named in .vale.ini
npm run docs:dev            # serves the site at http://localhost:3000
```

Expected result: the local server prints the address and rebuilds on save. Open the page you changed and check headings, tables, code blocks, admonitions and images in both light and dark themes.

Before pushing, run the same checks as CI:

```bash
npm run docs:lint           # markdownlint-cli2 "docs/**/*.md"
vale docs/                  # prose rules and vocabulary
lychee --offline --config lychee.toml docs/   # internal links only, as in CI
npm run docs:build          # fails on broken internal references
```

Every pull request also gets a preview deployment from CI. Reviewers read the preview, not the raw diff, for layout and navigation changes.

## 6. Front matter schema

<!-- Guidance: Define every field, its type and whether it is required, and enforce it in CI. Pitfall: optional fields that nobody fills, so freshness reports are empty. -->

Every page starts with a YAML front matter block. The build validates it against the schema below and fails on unknown or missing required fields.

| Field | Type | Required | Rule |
|---|---|---|---|
| `title` | string | Yes | Sentence case, 70 characters or fewer, unique in the site |
| `description` | string | Yes | One sentence, 160 characters or fewer, used for search results and link previews |
| `content_type` | enum | Yes | One of `tutorial`, `how-to`, `reference`, `explanation`, `landing` |
| `owner` | string | Yes | Team handle from CODEOWNERS, not a person |
| `product_area` | string | Yes | Value from the area list in `site.config` |
| `applies_to` | string | Yes | Product or API version range, for example `orders-api >= 2026-06` |
| `last_verified` | date | Yes | ISO 8601 date when someone last ran or checked the content |
| `status` | enum | No | `current` (default), `beta`, `deprecated` |
| `tags` | list | No | Values from the controlled tag list only |
| `translate` | boolean | No | `false` keeps the page out of localisation handoff |

```yaml
---
title: Retry failed refund webhooks
description: Configure retry intervals and inspect delivery attempts for refund webhooks.
content_type: how-to
owner: corvid/checkout-team
product_area: checkout
applies_to: orders-api >= 2026-06
last_verified: 2026-10-20
status: current
tags: [webhooks, refunds]
---
```

Update `last_verified` only when you have actually run the steps or checked the reference against the product. A content audit uses this field to find stale pages, so a date bumped without checking hides real problems.

## 7. Content types and writing rules

<!-- Guidance: Point to the style guide and termbase rather than restating them; list only the rules that reviewers block on. Pitfall: duplicating the style guide here so the two drift apart. -->

Each page serves one reader need, using the four Diátaxis forms:

| Content type | Reader need | Folder | Must contain |
|---|---|---|---|
| Tutorial | Learn by doing a guided lesson | `tutorials/` | A working result at the end, tested steps, no choices for the reader |
| How-to guide | Complete a specific task | `how-to/` | A goal in the title, prerequisites, numbered steps, a verification step |
| Reference | Look up exact facts | `reference/` | Complete, consistent entries generated or checked against the product |
| Explanation | Understand why and how it works | `explanation/` | Context, design reasons and trade-offs, links to the task pages |

Writing rules are defined in the Technical Writing Style Guide (`docs/style-guide-technical`) and approved terms in the Termbase (`docs/termbase`). Reviewers block a pull request on these points only:

1. The page mixes content types (for example, a reference page with a tutorial in the middle).
2. A term contradicts the termbase, or a deprecated term is used.
3. Headings skip levels, or the page has more than one H1.
4. Steps cannot be followed in order, or a step hides two actions.
5. Link text does not describe the destination (for example, "click here").
6. The page includes personal data, real customer names, secrets or internal hostnames in examples.

Everything else (tone, word choice, sentence length) is a suggestion that the contributor may accept or discuss.

## 8. Prose linting with Vale

<!-- Guidance: Quote the real .vale.ini and explain how to add terms. Vale v3 reads vocabularies from <StylesPath>/config/vocabularies/<name>/; older guides that mention a Vocab folder directly under StylesPath describe Vale 2. Pitfall: suppressing a rule globally to silence one false positive. -->

Vale checks prose against the house style and the approved vocabulary. The configuration lives in `.vale.ini` at the repository root. The structure below follows the Vale v3 configuration format: global keys first, then a section per file glob.

```ini
# .vale.ini (Example, Vale v3)
StylesPath = .github/styles
MinAlertLevel = suggestion
Packages = Google
Vocab = Corvid

[docs/**/*.{md,mdx}]
BasedOnStyles = Vale, Google, Corvid
Google.Headings = NO
Google.Will = suggestion
```

What each key does in this setup:

| Key | Meaning in this repository |
|---|---|
| `StylesPath` | Folder that holds style packages, the house `Corvid` style and the `config/` folder |
| `MinAlertLevel` | Lowest level reported; CI fails only on `error` (see the workflow) |
| `Packages` | Third-party style packages that `vale sync` downloads into `StylesPath` |
| `Vocab` | Vocabulary loaded from `config/vocabularies/Corvid/` |
| `BasedOnStyles` | Styles applied to matching files; `Vale` enables the built-in spelling and term checks |
| `Rule.Name = NO` | Turns one rule off for that file glob |
| `Rule.Name = suggestion` | Changes the level of one rule for that file glob |

### 8.1 Vocabulary: accept and reject lists

Vale v3 stores each vocabulary in its own folder, `<StylesPath>/config/vocabularies/<name>/`, with two plain-text files:

- `accept.txt`: one approved term per line. Vale adds these to the spelling dictionary and flags variants with different capitalisation.
- `reject.txt`: one forbidden term per line. Vale reports every occurrence as an error.

Entries are case-sensitive and may be regular expressions. Use `(?i)` at the start of an entry to accept any capitalisation, or a character class such as `[Ww]ebhook` to accept two specific forms.

```text
# .github/styles/config/vocabularies/Corvid/accept.txt (Example)
Corvid
Corvid Commerce
[Ww]ebhook
idempotency
SKU
(?i)lakehouse
```

```text
# .github/styles/config/vocabularies/Corvid/reject.txt (Example)
whitelist
blacklist
master branch
check-out session
```

Generate both files from the termbase: preferred and admitted terms go to `accept.txt`, deprecated terms go to `reject.txt`. Do not hand-edit them when the termbase export is the source.

### 8.2 House rules

House rules are YAML files in `.github/styles/Corvid/`. Each rule has an `extends` type (such as `existence` or `substitution`), a message and a level. Add a rule when reviewers have made the same comment on three or more pull requests.

```yaml
# .github/styles/Corvid/ProductNames.yml (Example)
extends: substitution
message: "Use '%s' instead of '%s'."
level: error
ignorecase: false
swap:
  Corvid commerce: Corvid Commerce
  orders API: Orders API
```

### 8.3 Handling false positives

1. Prefer fixing the text. Most alerts point to a real ambiguity.
2. For a product name or a term missing from the vocabulary, request a termbase entry; do not add it only to `accept.txt`.
3. For a single legitimate exception, wrap the passage in Vale comments and explain why in the pull request:

```markdown
<!-- vale Google.Exclamation = NO -->
Press **Ctrl+S** to save!
<!-- vale Google.Exclamation = YES -->
```

## 9. Markdown linting with markdownlint-cli2

<!-- Guidance: Quote the real config file and say which rules are relaxed and why. Pitfall: a long list of disabled rules with no reason, so nobody dares to re-enable them. -->

markdownlint-cli2 checks Markdown structure: heading levels, list and code block style, trailing spaces and similar issues that break rendering or accessibility. It reads `.markdownlint-cli2.jsonc` at the repository root. Other accepted names are `.markdownlint-cli2.yaml`, `.markdownlint-cli2.cjs` and `.markdownlint-cli2.mjs`; rule-only files named `.markdownlint.jsonc`, `.markdownlint.json`, `.markdownlint.yaml` or `.markdownlint.yml` also work. Keep rules in one file only, preferably the `config` key of `.markdownlint-cli2.jsonc`; markdownlint-cli2 reads both kinds of file and gives the cli2 file precedence for `config`.

```jsonc
// .markdownlint-cli2.jsonc (Example)
{
  "config": {
    "default": true,
    "MD013": false,                                   // line length: prose is not wrapped
    "MD024": { "siblings_only": true },               // repeated headings allowed in different sections
    "MD033": { "allowed_elements": ["kbd", "details", "summary"] },
    "MD040": true,                                    // every fenced block names its language
    "MD046": { "style": "fenced" }
  },
  "globs": ["docs/**/*.md"],
  "ignores": ["docs/_snippets/generated/**"],
  "gitignore": true
}
```

| Rule | Setting | Reason |
|---|---|---|
| MD013 line length | Off | Sources use one sentence per line or no wrapping; line length does not affect output |
| MD024 duplicate headings | Siblings only | Reference pages repeat "Parameters" and "Response" under each operation |
| MD033 inline HTML | Allow `kbd`, `details`, `summary` | Needed for keyboard keys and collapsible sections; other HTML is blocked |
| MD040 code block language | On | Enables syntax highlighting and lets CI pick blocks for testing |
| MD046 code block style | Fenced | Indented code blocks are hard to review and cannot carry a language |

Front matter is recognised automatically and is not linted as Markdown. Run `npx markdownlint-cli2 --fix "docs/**/*.md"` to repair the rules that support automatic fixes, then review the diff before committing.

## 10. Link checking

<!-- Guidance: Separate internal links (must never break) from external links (may break for reasons outside your control). Pitfall: a nightly external check that fails every pull request and gets ignored. -->

Broken links are the most common defect readers report. The repository checks them at two moments:

| Check | When | Scope | Blocks merge? |
|---|---|---|---|
| Internal links and anchors | Every pull request | Links between pages, heading anchors, image paths | Yes |
| External links | Nightly on `main` | Links to other sites | No, opens an issue for the docs maintainers |
| Redirect coverage | Every pull request that moves or deletes a page | Old paths listed in `redirects.yaml` | Yes |

```toml
# lychee.toml (Example)
exclude_path = ["docs/_snippets/generated"]
exclude = ["^https://localhost", "^https://example\\.com"]
max_retries = 2
accept = ["200", "429"]
include_fragments = "full"   # check heading anchors; without it, broken #anchors pass
# root_dir = "/abs/path/to/docs"   # needed for root-relative links such as /how-to/x in offline mode
```

lychee checks anchors (the part after `#`) only when `include_fragments` is set; the value `"full"` is the one shown in the lychee v0.24.2 example file, so check the example file of your version. In offline mode, links that start with `/` resolve only when `root_dir` is set.

Rules for links:

1. Link to the canonical page, not to a search result or a preview URL.
2. Use relative links between docs pages so previews and versions work.
3. Link to a specific heading only when the heading text is stable; if you rename a heading that other pages link to, update those links.
4. Prefer official, stable sources for external links and record the date you checked them when the target changes often.
5. Never link to private repositories, internal dashboards or tickets from public docs.

## 11. Images, diagrams and alt text

<!-- Guidance: Give concrete alt text rules with examples; WCAG 2.2 success criterion 1.1.1 (Non-text Content) applies. Pitfall: screenshots that contain real customer data or that go stale after every UI change. -->

Use an image only when it shows something text cannot, such as a location on screen or the shape of an architecture. Prefer diagrams as code (Mermaid) because they are reviewable, translatable and theme-aware.

| Rule | Detail |
|---|---|
| Format | SVG for diagrams, PNG for screenshots, no animated GIFs for instructions |
| Size | [*Example:* 200 KB maximum per image; crop to the relevant area] |
| Data | Use the demo tenant and fictional data only; blur nothing, recapture instead |
| Text in images | Avoid it; text in images cannot be searched or translated |
| Theme | Check the image in light and dark themes |
| Source files | Keep editable sources next to exported diagrams |

Alt text rules:

1. Describe the purpose of the image in context, not its appearance. For a screenshot of a settings page, say what the reader should notice.
2. Keep it short, usually one sentence. Put long explanations in the body text, where every reader benefits from them.
3. Use empty alt text (`![](image.png)` with no description) only for purely decorative images.
4. Do not start with "Image of" or "Screenshot of"; assistive technology already announces the element type.
5. For a diagram, summarise the relationship it shows, and make sure the same information is available in the text or a table.

| Image *(Example)* | Weak alt text | Better alt text |
|---|---|---|
| Webhook settings screen | "Screenshot of settings" | "Webhook settings with the Retry policy field set to exponential" |
| Checkout data flow diagram | "Diagram" | "Checkout events flow from the storefront to the Orders data product through the event bus" |

## 12. Code samples

<!-- Guidance: Make samples runnable and tested in CI where possible. Pitfall: samples with real keys, or samples that drift from the API. -->

Code samples are product surface: readers copy them into production.

1. Every fenced block names its language (`bash`, `json`, `python`, `text` for output).
2. Show the command and its expected output in separate blocks, so readers can copy the command without the output.
3. Use placeholders in angle brackets or capitals (`<ORDER_ID>`, `$CORVID_API_KEY`) and explain each one directly after the block.
4. Never include real secrets, tokens, internal hostnames or customer data. Use the documented test keys and the `example.com` domain family.
5. Keep samples short enough to read on one screen; link to a full sample repository for longer programs.
6. Mark samples that are tested in CI with the test harness tag your site uses, and add new samples to the test list in the same pull request.
7. Pin the SDK or API version a sample targets and match `applies_to` in the front matter.

```bash
# Example: request with placeholder values
curl -X POST "https://api.example.com/v1/refunds" \
  -H "Authorization: Bearer $CORVID_API_KEY" \
  -H "Idempotency-Key: <UNIQUE_KEY>" \
  -d '{"order_id": "<ORDER_ID>", "amount": 1200, "currency": "EUR"}'
```

## 13. Review and merge

<!-- Guidance: Define who must approve what and the turnaround targets. Use the Docs Review Checklist (`docs/docs-review-checklist`) as the reviewer's tool. Pitfall: SME review skipped because the docs reviewer "knows the product". -->

Two kinds of review protect readers: technical accuracy and documentation quality. Both are required for content changes.

| Review | Reviewer | Checks |
|---|---|---|
| Technical accuracy | SME from CODEOWNERS | Facts, defaults, limits, version ranges, samples run as written |
| Documentation quality | Docs maintainer | Content type, structure, style, terms, links, accessibility, front matter |
| Task test | Contributor or a second person | Every step followed on a clean environment, result matches the page |
| Localisation impact | Localisation coordinator (when `translate` is true) | Strings and screenshots that need translation, do-not-translate terms |

Reviewers use the Docs Review Checklist (`docs/docs-review-checklist`) and label each comment as **blocking** or **suggestion**. Contributors resolve or answer every blocking comment; suggestions may be declined with a short reason.

Merge rules:

1. All required CI checks pass: build, markdownlint-cli2, Vale errors, internal links, redirect coverage.
2. One SME approval and one docs maintainer approval (one approval for typo-only changes).
3. Squash merge with a `docs:` commit message that summarises the change.
4. The author deletes the branch after merge.

Turnaround targets: first review within [*Example:* 2] working days; if a pull request waits longer, mention the docs lead in the pull request.

## 14. Docs definition of done

<!-- Guidance: A short list that product teams can paste into their own definition of done. Pitfall: "docs updated" as a single checkbox with no criteria. -->

A docs change is done when all items below are true:

| ID | Criterion | Evidence |
|---|---|---|
| DOD-01 | The page has the right content type and lives in the matching folder | Folder and `content_type` agree |
| DOD-02 | Front matter is complete and `last_verified` reflects a real check | Build passes schema validation |
| DOD-03 | Steps and samples were run against the version in `applies_to` | Note in the pull request |
| DOD-04 | Vale reports no errors; warnings were reviewed | CI log |
| DOD-05 | markdownlint-cli2 passes | CI log |
| DOD-06 | Internal links and anchors resolve; moved pages have redirects | CI log |
| DOD-07 | Images have purposeful alt text and contain no real personal data | Review comment |
| DOD-08 | Terms match the termbase; new terms were requested | Review comment |
| DOD-09 | SME and docs maintainer approved | Pull request approvals |
| DOD-10 | Localisation and release notes were informed when needed | Linked issue |

## 15. Releasing and versioning the docs

<!-- Guidance: Explain how docs for unreleased features wait for the release, and how old versions are kept. Pitfall: docs published before the feature, or docs for removed features left live without a notice. -->

The site publishes automatically from `main`. Content that must wait for a product release follows one of two patterns:

| Pattern | Use when | How |
|---|---|---|
| Release branch | A large feature with many pages | Branch `docs/release-<version>`, merged into `main` on release day by the docs platform engineer |
| Feature flag in front matter | A small change tied to a flag | `status: beta` plus a site build flag that hides the page until the flag is public |

Versioned documentation:

1. The site keeps the current version and [*Example:* the two previous] major versions. Older versions are archived as static snapshots with a banner that links to the current page.
2. A fix to a supported older version is cherry-picked into its version folder in the same pull request when possible.
3. When a feature is deprecated, the page gets `status: deprecated`, a notice at the top and a link to the replacement. Customer-facing announcements follow the Deprecation Notice (`docs/deprecation-notice`).
4. Every product release lists its documentation changes in the release notes (Release Notes (`dev/release-notes`)) and, for APIs, in the API changelog (`docs/api-changelog`).
5. Machine-readable outputs (for example an `llms.txt` file, see `dev/llms-txt`) are rebuilt with the site; check them after navigation changes.

```yaml
# .github/workflows/docs.yml (Example, actions pinned to full commit SHAs)
name: docs
on:
  pull_request:
    paths: ["docs/**", ".vale.ini", ".markdownlint-cli2.jsonc", ".github/styles/**", "lychee.toml", "redirects.yaml", "package*.json"]
permissions:
  contents: read
  checks: write
  pull-requests: write
jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<pinned-sha>
      - uses: DavidAnson/markdownlint-cli2-action@<pinned-sha>
        with:
          globs: "docs/**/*.md"
      - uses: errata-ai/vale-action@<pinned-sha>
        with:
          version: 3.25.1
          files: docs
          fail_on_error: true
      - uses: lycheeverse/lychee-action@<pinned-sha>
        with:
          args: --offline --config lychee.toml docs
      - run: npm ci && npm run docs:build
```

vale-action reports only the lines a pull request changed (its default `filter_mode: added`), so old problems in untouched lines do not block a change; set `filter_mode: nofilter` for a full sweep. It runs `vale sync` for `Packages` itself, and the `version` input pins Vale, because otherwise the action installs the latest release and CI would differ from the versions table in section 5. The external-link check from section 10 runs on a schedule (`on: schedule: - cron: "0 5 * * 1"`) in a separate workflow so that a dead third-party site does not block a pull request.

Pin each action to a full commit SHA and record the release it corresponds to; check each action's repository for its current inputs before copying this example.

## 16. Glossary and references

<!-- Guidance: Define the terms a new contributor meets in this guide and list the tools and standards with versions. -->

| Term | Meaning in this guide |
|---|---|
| Docs as code | Writing documentation in plain-text files kept in version control and checked, reviewed and published with the same tools as source code |
| Front matter | The YAML block at the top of a Markdown file that holds page metadata |
| Linter | A tool that checks files against rules and reports violations |
| Preview deployment | A temporary build of the site for one pull request |
| SME | Subject matter expert who confirms technical accuracy |
| Vocabulary (Vale) | A pair of accept and reject lists that Vale applies to prose |

| Reference | Use |
|---|---|
| Vale, v3.25.1 (released 2026-10-22), vale.sh and github.com/errata-ai/vale, MIT licence | Configuration keys, vocabulary layout (section 8) |
| markdownlint-cli2, v0.24.0, and markdownlint, v0.42.0, github.com/DavidAnson/markdownlint-cli2, MIT licence | Configuration file names and keys, rule identifiers (section 9) |
| lychee link checker, v0.24.2, github.com/lycheeverse/lychee | Link checking (section 10) |
| CommonMark Spec, version 0.31.2 (2024-01-28), spec.commonmark.org | Markdown syntax baseline |
| Diátaxis, Daniele Procida, diataxis.fr, CC BY-SA 4.0 | Names of the four content types (section 7) |
| WCAG 2.2, W3C Recommendation, 12 December 2024, w3.org/TR/WCAG22 | Alt text and accessible links (sections 7, 11) |
| Contributing (`dev/contributing`) | Code contribution rules |
| Technical Writing Style Guide (`docs/style-guide-technical`) | Style rules that Vale enforces |
| Termbase (`docs/termbase`) | Source of the Vale vocabularies |
| Docs Review Checklist (`docs/docs-review-checklist`) | Reviewer checklist |
| Documentation Plan (`docs/documentation-plan`) | Release-level documentation scope |

## Standard and licence note

Based on the public documentation of Vale v3 (MIT licence), markdownlint-cli2 and markdownlint (MIT licence) and lychee, cited for configuration key names, file names and rule identifiers only, with original explanations and fictional examples; on the Diátaxis framework by Daniele Procida (diataxis.fr, CC BY-SA 4.0), whose four content type names are cited with attribution (if you adapt Diátaxis text itself into this guide, that adapted material must be shared under CC BY-SA 4.0); and on WCAG 2.2 (W3C Recommendation, 12 December 2024). Tool versions and configuration syntax change often: check the current releases and documentation at vale.sh, github.com/DavidAnson/markdownlint-cli2 and github.com/lycheeverse/lychee before adopting this guide. Corvid Commerce is fictional. Original text written for this collection. Template licence: MIT.
