Kosh’s versioning system lets you maintain multiple versions of your documentation simultaneously. Each version is a frozen snapshot, allowing users to view docs for older releases while you work on the latest.
How It Works
From AGENTS.md:
Versioning System:
- Version Configuration: Defined in
kosh.yaml with name, path, and isLatest fields
- Version Selector: Dropdown shows
(Latest) suffix for current version
- Version Landing Pages: Each version has its own
index.md at content/vX.X/index.md
- Version URL Preservation: Switching versions preserves the current page path
- Sparse Versioning: Only changed pages need version-specific content; others fall back to latest
- Outdated Banner: Shows on non-latest versions with link to equivalent page in latest version
Creating a New Version
Use the kosh version command to freeze the current documentation:
# Freeze current docs as v1.0
kosh version v1.0
What happens:
- Current
content/ is copied to content/v1.0/
kosh.yaml is updated with new version entry:
versions:
- name: "v1.0"
path: "v1.0"
isLatest: false
- name: "latest"
path: ""
isLatest: true
- Build output is generated to
public/v1.0/
Configuration
Version Entries
In kosh.yaml:
versions:
- name: "v4.0" # Display name
path: "v4.0" # URL path segment
isLatest: true # Mark as current version
- name: "v3.0"
path: "v3.0"
isLatest: false
- name: "v2.0"
path: "v2.0"
isLatest: false
Fields:
name: Display name in version selector (e.g., “v4.0”, “v3.0 (Beta)”)
path: URL path segment (e.g., v4.0 → /v4.0/getting-started.html)
isLatest: If true, version is also accessible at root (e.g., /getting-started.html)
Content Structure
content/
├── index.md # Latest version landing page
├── getting-started.md # Latest docs
├── api.md
├── v4.0/
│ ├── index.md # v4.0 landing page
│ ├── getting-started.md
│ └── api.md
├── v3.0/
│ ├── index.md
│ ├── quickstart.md # v3.0 had different page names
│ └── api.md
└── v2.0/
└── index.md # Minimal version (only index)
URL Structure
From AGENTS.md:
/ → Hub page (template-only)
/getting-started.html → Latest version (dual access)
/v4.0/ → Latest version landing page
/v4.0/getting-started.html → Latest version (dual access)
/v3.0/ → v3.0 landing page
/v3.0/quickstart.html → v3.0 specific content
/v2.0/ → v2.0 landing page
/v1.0/ → v1.0 landing page
Dual access for latest:
The latest version is accessible at both /page.html and /v4.0/page.html. This ensures:
- Short URLs for latest docs (
/getting-started.html)
- Consistent URLs when switching versions
Version Selector
Themes render a version dropdown using GetVersionsMetadata():
From AGENTS.md:
The GetVersionsMetadata(currentVersion, currentPath string) function in builder/config/config.go handles URL generation with path preservation.
Template example:
<select id="version-selector" onchange="window.location.href = this.value">
{{ range .Versions }}
<option value="{{ .Link }}" {{ if .IsCurrent }}selected{{ end }}>
{{ .Name }}{{ if .IsLatest }} (Latest){{ end }}
</option>
{{ end }}
</select>
Data structure:
type VersionMetadata struct {
Name string // "v4.0"
Link string // "/v4.0/getting-started.html" (preserved path)
IsCurrent bool // true if user is viewing this version
IsLatest bool // true if isLatest: true in config
}
Version URL Preservation
From AGENTS.md:
Version URL Preservation:
When switching versions via the dropdown selector, the current page path is preserved:
- On
/getting-started.html → switch to v4.0 → /v4.0/getting-started.html
- On
/v3.0/quickstart.html → switch to latest → /quickstart.html
- If the target page doesn’t exist in that version, falls back to the first available page
Implementation:
func GetVersionsMetadata(currentVersion, currentPath string) []VersionMetadata {
var versions []VersionMetadata
for _, v := range config.Versions {
link := "/" + v.Path
if currentPath != "" {
link = "/" + v.Path + "/" + currentPath // Preserve page path
}
versions = append(versions, VersionMetadata{
Name: v.Name,
Link: link,
IsCurrent: v.Name == currentVersion,
IsLatest: v.IsLatest,
})
}
return versions
}
Fallback behavior:
If currentPath doesn’t exist in the target version:
- Try version’s
index.md
- Try first page in version directory
- Fall back to site root
Sparse Versioning
From AGENTS.md:
Sparse Versioning: Only changed pages need version-specific content; others fall back to latest
You don’t need to duplicate unchanged pages. Kosh falls back to the latest version:
content/
├── getting-started.md # Latest version
├── api.md # Latest version
└── v3.0/
└── api.md # v3.0 override (different API)
Result:
/v3.0/getting-started.html → Renders from content/getting-started.md (fallback)
/v3.0/api.html → Renders from content/v3.0/api.md (override)
Sparse versioning saves disk space and reduces maintenance. Only create versioned copies of pages that actually changed.
Outdated Banner
From AGENTS.md:
Outdated Banner: Shows on non-latest versions with link to equivalent page in latest version
Template example:
{{ if not .Version.IsLatest }}
<div class="outdated-banner">
⚠️ You're viewing documentation for {{ .Version.Name }}.
<a href="{{ .Version.LatestLink }}">View latest version</a>
</div>
{{ end }}
Data:
type VersionData struct {
Name string // "v3.0"
IsLatest bool // false
LatestLink string // "/getting-started.html" (equivalent page in latest)
}
The banner links to the same page in the latest version.
Clean Command
From AGENTS.md:
Clean Command (Version-Aware):
kosh clean → Removes root files only, preserves version folders
kosh clean --all → Removes entire output directory
- Uses config-based version detection to identify folders to preserve
# Preserve versions (only clean latest)
kosh clean
# Delete everything (including versions)
kosh clean --all
Why preserve versions?
Versioned docs are frozen snapshots that rarely change. Rebuilding them is wasteful:
# Good: only rebuild latest
kosh clean
kosh build # Fast: only builds latest docs
# Bad: rebuild everything
kosh clean --all
kosh build # Slow: rebuilds all versions
Use kosh clean --all only when you need to force-rebuild all versions (e.g., after theme changes).
Version Landing Pages
From AGENTS.md:
Version Landing Pages: Each version has its own index.md at content/vX.X/index.md
Example content/v3.0/index.md:
---
title: Version 3.0 Documentation
---
Welcome to the v3.0 documentation!
## What's New in v3.0
- Feature A
- Feature B
## Guides
- [Quick Start](quickstart.html)
- [API Reference](api.html)
This provides version-specific navigation and highlights.
Hub Page
From AGENTS.md:
Hub Page (/): Template-only landing page (no content/index.md required) with “Go to Latest Docs” CTA
Template example:
<!-- themes/docs/templates/hub.html -->
<div class="version-hub">
<h1>{{ .Site.Title }} Documentation</h1>
<a href="/{{ .LatestVersionPath }}/" class="cta">
Go to Latest Docs ({{ .LatestVersionName }})
</a>
<h2>All Versions</h2>
<ul>
{{ range .Versions }}
<li>
<a href="/{{ .Path }}/">{{ .Name }}</a>
{{ if .IsLatest }}<span class="badge">Current</span>{{ end }}
</li>
{{ end }}
</ul>
</div>
The hub page lists all versions and provides quick access to the latest.
Version Workflow
Release Process
- Develop on latest:
# Edit content/getting-started.md
# Edit content/api.md
kosh serve --dev # Preview latest docs
- Freeze version before release:
kosh version v1.0 # Creates content/v1.0/ snapshot
- Update kosh.yaml:
versions:
- name: "v1.1" # New latest
path: ""
isLatest: true
- name: "v1.0" # Frozen
path: "v1.0"
isLatest: false
- Build and deploy:
kosh build
# Outputs:
# - public/getting-started.html (v1.1 latest)
# - public/v1.0/getting-started.html (v1.0 frozen)
Updating Old Versions
To fix a bug in v1.0 docs:
# Edit content/v1.0/api.md
kosh build # Rebuilds v1.0
Kosh detects the change and rebuilds only the affected version.
Use sparse versioning: only copy files that differ between versions. This minimizes maintenance and disk usage.
Search Integration
From AGENTS.md:
Search: Version-scoped WASM search with snippets and keyboard navigation.
The search engine filters results by version:
if versionFilter != "all" && post.Version != versionFilter {
continue // Skip posts from other versions
}
Version selector:
<select id="version-filter">
<option value="{{ .CurrentVersion }}">Current Version</option>
<option value="all">All Versions</option>
</select>
Users can search within the current version or across all versions.
Theme Support
From AGENTS.md:
Theme Validation
The SSG validates theme presence at startup:
- Theme directory must exist at
themes/<theme-name>/
templates/ directory is required
In theme.yaml:
name: "Docs Theme"
supportsVersioning: true # Enable versioning features
Themes that support versioning provide:
- Version selector dropdown
- Outdated banner
- Version-scoped search
- Hub page template
Build times (100 pages, 3 versions):
| Build Type | Time | Notes |
|---|
| Full rebuild (all versions) | 3.5s | Cold cache |
| Latest only | 1.2s | Preserved versions |
| Single version update | 1.5s | Only rebuild changed version |
Recommendation: Use kosh clean (not --all) to preserve frozen versions.
Limitations
- No automatic migration: Kosh doesn’t auto-migrate content between versions. You must manually update versioned copies.
- No diff view: Users can’t see what changed between versions (consider external tools like GitHub compare).
- Static paths: Version paths are hardcoded in config. Renaming a version requires manual URL updates.
Versioning is best for documentation sites with clear release cycles. Blogs and marketing sites rarely need versioning.