Skip to main content
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:
  1. Current content/ is copied to content/v1.0/
  2. kosh.yaml is updated with new version entry:
versions:
  - name: "v1.0"
    path: "v1.0"
    isLatest: false
  - name: "latest"
    path: ""
    isLatest: true
  1. 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:
  1. Try version’s index.md
  2. Try first page in version directory
  3. 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

  1. Develop on latest:
# Edit content/getting-started.md
# Edit content/api.md
kosh serve --dev  # Preview latest docs
  1. Freeze version before release:
kosh version v1.0  # Creates content/v1.0/ snapshot
  1. Update kosh.yaml:
versions:
  - name: "v1.1"      # New latest
    path: ""
    isLatest: true
  - name: "v1.0"      # Frozen
    path: "v1.0"
    isLatest: false
  1. 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

Performance

Build times (100 pages, 3 versions):
Build TypeTimeNotes
Full rebuild (all versions)3.5sCold cache
Latest only1.2sPreserved versions
Single version update1.5sOnly rebuild changed version
Recommendation: Use kosh clean (not --all) to preserve frozen versions.

Limitations

  1. No automatic migration: Kosh doesn’t auto-migrate content between versions. You must manually update versioned copies.
  2. No diff view: Users can’t see what changed between versions (consider external tools like GitHub compare).
  3. 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.

Build docs developers (and LLMs) love