Versions & i18n

Ship multiple documentation versions or languages from one project — same URL-prefix mechanism, a switcher in the header, fully static.

Markline serves multiple versions (v2, v1, …) or locales (en, es, …) from a single project. Both use the same idea: the first entry is the default and lives at the root; every other entry is served under a URL prefix with its own content folder — and the header shows a switcher.

How it works

There's one rule for the whole folder layout: the default version lives at the content root; every other version lives in a folder named after its id. A version folder is just a smaller mirror of the root — same docs/ and api/openapi.json inside.

content/
├── markline.json          # config + the versions list
├── docs/                  # DEFAULT version docs   → /quickstart
│   └── quickstart.mdx
├── api/
│   └── openapi.json       # DEFAULT version spec    → /api-reference
└── v1/                    # one folder per non-default version id
    ├── docs/              # v1 docs                 → /v1/quickstart
    │   └── quickstart.mdx
    └── api/
        └── openapi.json   # v1 spec                 → /api-reference/v1

So a version id is both a folder name and a URL prefix. The one wrinkle to remember — docs and the API reference prefix the id in different spots:

Content fileDefault URLVersion v1 URL
docs/quickstart.mdx/quickstart/v1/quickstart
api/openapi.json/api-reference/api-reference/v1

(Docs put the version first; the API reference keeps everything under /api-reference and puts the version after it — so all API routes stay in one place.)

A project uses versions or locales, not both — they share the prefix mechanism (if both are set, versions win). The default entry is always unprefixed, so existing URLs don't change when you add a second version. A version folder only needs the parts that differ — drop api/ and it's docs-only; drop docs/ and it's reference-only.

Versions

List versions in markline.json. The first is the default; each entry is { id, label } and may carry its own navigation.

"versions": [
  { "id": "v2", "label": "v2 (latest)" },   // default — content in docs/, served at /
  { "id": "v1", "label": "v1" }             // content in v1/docs/, served at /v1/…
]
1

Add the version content

Create v1/docs/ and author its pages — they're independent from the default, so v1 can lag or differ freely.

2

(Optional) give it its own navigation

A version entry can set navigation: { tabs: [...] } to override the sidebar for that version; otherwise it inherits the default navigation.

3

Ship

markline build emits every version statically. The header shows a version switcher; picking one navigates under its prefix.

API reference per version

Each version can ship its own OpenAPI spec. Drop it at <id>/api/openapi.json and Markline serves that version's reference at /api-reference/<id> — generated exactly like the default, with its own resource pages, code samples, and interactive explorer.

content/
├── api/openapi.json        # default → /api-reference
└── v1/
    └── api/openapi.json     # v1      → /api-reference/v1
  • The version selector in the API-reference header lists every version and switches the spec in place (the default is marked Default).
  • Within a version, every link — resource pages, search results, the explorer's endpoint switcher — stays under that version's /api-reference/<id> prefix, so you never fall back to the default spec mid-navigation.
  • Per-resource and per-operation MDX overlays are version-scoped too: <id>/api/sections/<tag>.mdx and <id>/api/operations/<operationId>.mdx.
  • The pill label follows the spec's info.version — a semver like 1.4.2 shows as v1 · 1.4.2; a date like 2025-06-01 shows once, verbatim.
A version that has no api/openapi.json simply has no API reference (its selector entry won't resolve a spec) — versions can be docs-only, reference-only, or both.

Localization

Locales work identically — i18n.locales, first is the default (unprefixed), each other locale under its id prefix with content in <id>/docs/.

"i18n": {
  "locales": [
    { "id": "en", "label": "English" },   // default — docs/, served at /
    { "id": "es", "label": "Español" }     // es/docs/, served at /es/…
  ]
}
content/
├── docs/        # English (default) → /quickstart
└── es/
    └── docs/    # Spanish → /es/quickstart

The header shows a language switcher that jumps to the same page under the chosen locale prefix.

Notes

  • The default entry (first in the list) is never prefixed — keep your canonical content in docs/.
  • Each variant is fully static and independently navigable; search and the API reference work within each.
  • Configure these in Configuration; for hosting the built output see Deployment.