diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 77b3423..ad3a756 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -5,7 +5,7 @@ on:
types: [published]
permissions:
- contents: read
+ contents: write
pages: write
id-token: write
@@ -16,9 +16,14 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- - uses: actions/configure-pages@v5
-
- uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Configure git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Install uv
uses: astral-sh/setup-uv@v7
@@ -26,9 +31,22 @@ jobs:
- name: Set up Python
run: uv python install 3.13
- - run: uv sync --group dev
+ - run: uv sync --group docs
- - run: uv run zensical build --clean
+ - name: Install mkdocs shim
+ run: cp scripts/mkdocs .venv/bin/mkdocs && chmod +x .venv/bin/mkdocs
+
+ - name: Deploy docs version
+ run: |
+ VERSION="${GITHUB_REF_NAME#v}"
+ MINOR_VERSION="${VERSION%.*}"
+ uv run mike deploy --push --update-aliases "$MINOR_VERSION" latest
+ uv run mike set-default --push latest
+
+ - name: Prepare site artifact
+ run: git worktree add site gh-pages
+
+ - uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v4
with:
diff --git a/docs/overrides/partials/header.html b/docs/overrides/partials/header.html
new file mode 100644
index 0000000..a957872
--- /dev/null
+++ b/docs/overrides/partials/header.html
@@ -0,0 +1,69 @@
+{#-
+ Override of partials/header.html — adds version selector slot missing in Zensical.
+-#}
+{% set class = "md-header" %}
+{% if "navigation.tabs.sticky" in features %}
+ {% set class = class ~ " md-header--shadow md-header--lifted" %}
+{% elif "navigation.tabs" not in features %}
+ {% set class = class ~ " md-header--shadow" %}
+{% endif %}
+
+
+ {% if "navigation.tabs.sticky" in features %}
+ {% if "navigation.tabs" in features %}
+ {% include "partials/tabs.html" %}
+ {% endif %}
+ {% endif %}
+
diff --git a/docs/stylesheets/version.css b/docs/stylesheets/version.css
new file mode 100644
index 0000000..b24b379
--- /dev/null
+++ b/docs/stylesheets/version.css
@@ -0,0 +1,100 @@
+/* Version selector styles for Zensical modern theme (backported from classic theme).
+ The JS appends .md-version into .md-header__topic, so we make that flex. */
+.md-header__topic:has(.md-version) {
+ display: flex;
+ align-items: center;
+}
+
+
+:root {
+ --md-version-icon: url('data:image/svg+xml;charset=utf-8,');
+}
+
+.md-version {
+ flex-shrink: 0;
+ font-size: .8rem;
+ height: 2.4rem;
+}
+
+[dir=ltr] .md-version__current { margin-left: 1.4rem; margin-right: .4rem; }
+[dir=rtl] .md-version__current { margin-left: .4rem; margin-right: 1.4rem; }
+
+.md-version__current {
+ color: inherit;
+ cursor: pointer;
+ outline: none;
+ position: relative;
+ top: .05rem;
+}
+
+[dir=ltr] .md-version__current:after { margin-left: .4rem; }
+[dir=rtl] .md-version__current:after { margin-right: .4rem; }
+
+.md-version__current:after {
+ background-color: currentcolor;
+ content: "";
+ display: inline-block;
+ height: .6rem;
+ -webkit-mask-image: var(--md-version-icon);
+ mask-image: var(--md-version-icon);
+ -webkit-mask-position: center;
+ mask-position: center;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+ -webkit-mask-size: contain;
+ mask-size: contain;
+ width: .4rem;
+}
+
+.md-version__alias {
+ margin-left: .3rem;
+ opacity: .7;
+}
+
+.md-version__list {
+ background-color: var(--md-default-bg-color);
+ border-radius: .1rem;
+ box-shadow: var(--md-shadow-z2);
+ color: var(--md-default-fg-color);
+ list-style-type: none;
+ margin: .2rem .8rem;
+ max-height: 0;
+ opacity: 0;
+ overflow: auto;
+ padding: 0;
+ position: absolute;
+ scroll-snap-type: y mandatory;
+ top: .15rem;
+ transition: max-height 0ms .5s, opacity .25s .25s;
+ z-index: 3;
+}
+
+.md-version:focus-within .md-version__list,
+.md-version:hover .md-version__list {
+ max-height: 10rem;
+ opacity: 1;
+ transition: max-height 0ms, opacity .25s;
+}
+
+.md-version:hover .md-version__list { animation: hoverfix .25s forwards; }
+.md-version:focus-within .md-version__list { animation: none; }
+
+.md-version__item { line-height: 1.8rem; }
+
+[dir=ltr] .md-version__link { padding-left: .6rem; padding-right: 1.2rem; }
+[dir=rtl] .md-version__link { padding-left: 1.2rem; padding-right: .6rem; }
+
+.md-version__link {
+ cursor: pointer;
+ display: block;
+ outline: none;
+ scroll-snap-align: start;
+ transition: color .25s, background-color .25s;
+ white-space: nowrap;
+ width: 100%;
+}
+
+.md-version__link:focus,
+.md-version__link:hover { color: var(--md-accent-fg-color); }
+
+.md-version__link:focus { background-color: var(--md-default-fg-color--lightest); }
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..209c700
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,5 @@
+# Minimal stub for mike compatibility.
+# The actual build is handled by zensical (see scripts/mkdocs shim).
+site_name: FastAPI Toolsets
+docs_dir: docs
+site_dir: site
diff --git a/scripts/mkdocs b/scripts/mkdocs
new file mode 100755
index 0000000..c439e2c
--- /dev/null
+++ b/scripts/mkdocs
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+"""mkdocs shim for mike compatibility.
+
+mike parses mkdocs.yml (valid YAML stub) for its Python internals, then calls
+`mkdocs build --config-file ` as a subprocess.
+
+This shim intercepts that subprocess call, ignores the temp config, and
+delegates the actual build to `zensical build -f zensical.toml` instead.
+"""
+
+from __future__ import annotations
+
+import os
+import subprocess
+import sys
+
+
+def main() -> None:
+ args = sys.argv[1:]
+
+ # mike calls `mkdocs --version` to embed in the commit message
+ if args and args[0] == "--version":
+ print("mkdocs, version 1.0.0 (zensical shim)")
+ return
+
+ if not args or args[0] != "build":
+ result = subprocess.run(["python3", "-m", "mkdocs"] + args)
+ sys.exit(result.returncode)
+
+ config_file = "mkdocs.yml"
+ clean = False
+
+ i = 1
+ while i < len(args):
+ if args[i] in ("-f", "--config-file") and i + 1 < len(args):
+ config_file = args[i + 1]
+ i += 2
+ elif args[i] in ("-c", "--clean"):
+ clean = True
+ i += 1
+ elif args[i] == "--dirty":
+ i += 1
+ else:
+ i += 1
+
+ # mike creates a temp file prefixed with "mike-mkdocs"; always delegate
+ # the actual build to zensical regardless of which config was passed.
+ del config_file # unused — zensical auto-discovers zensical.toml
+
+ cmd = ["zensical", "build"]
+ if clean:
+ cmd.append("--clean")
+
+ env = os.environ.copy()
+ result = subprocess.run(cmd, env=env)
+ sys.exit(result.returncode)
+
+
+if __name__ == "__main__":
+ main()