The docs site¶
This page documents the documentation site itself: how it's configured, how to preview and build it locally, how it deploys, and how to add a page.
What it's built with¶
The site is built with Zensical, the static site
generator from the creators of Material for MkDocs. It's declared as an
optional dependency group in pyproject.toml:
mike powers the versioned deploys — see Versioning below
for why the mike entry above isn't the whole story (the PyPI release
listed here gets overridden with a Zensical-compatible fork before it's
actually used).
Install it (and everything else) with:
Layout¶
zensical.toml # site configuration (root of the repo)
docs/ # docs_dir — every page lives under here
├── index.md # landing page
├── getting-started.md
├── tutorial/
│ ├── filtering.md
│ └── sorting-pagination.md
├── reference/
│ └── operators.md
├── design/ # the product design docs (docs/design/*.md) — do not edit
│ ├── index.md # section landing page, added for the nav
│ └── 00-overview.md ... 05-roadmap-and-release.md
├── contributing/
│ └── docs-site.md # this page
└── changelog.md
docs_dir defaults to docs/ in Zensical, so it isn't set explicitly in
zensical.toml — see
Basics for the full list of
defaults.
zensical.toml¶
The config lives at the repo root (/zensical.toml), not inside docs/.
The notable choices, each linked to the Zensical doc page that explains it:
navis explicit rather than derived from the directory tree, so ordering (Tutorial before Reference, Design as its own section) and page titles are deliberate. See Navigation.repo_url/edit_uripoint at this repository. Zensical's defaultedit_uriassumes amasterbranch; since this repo's default branch ismain,edit_uri = "edit/main/docs/"is set explicitly. See Repository.navigation.indexesis on so thedesign/index.mdsection-landing page attaches directly to the "Design" nav section instead of needing a separate top-level entry. See Section index pages.- Markdown extensions mirror the set Zensical ships by default (via
zensical new), spelled out explicitly inzensical.tomlso it's visible what's enabled without digging into Zensical's defaults. See Extensions.
Local preview¶
Serves the site at http://localhost:8000 with live rebuild on file
changes. Pass -o/--open to open a browser automatically, or
-a/--dev-addr to bind a different address/port.
Building¶
Builds the static site into site/ (Zensical's default site_dir). This is
exactly what CI runs — see Deployment below. Pass -c/--clean
to force a clean rebuild, or -s/--strict to fail the build on warnings
(e.g. broken internal links).
Adding a page¶
- Add a Markdown file under
docs/(or one of its subdirectories). - Add it to the
navarray inzensical.toml, as a{ "Title" = "path.md" }entry (or nested inside a section's array — see the existingnavinzensical.tomlfor examples). - Run
uv run zensical build(orserve) locally to confirm it renders and there are no broken-link warnings.
Adding a new top-level section works the same way: add a new
{ "Section Name" = [ ... ] } entry to nav with its pages listed inside.
Versioning¶
The docs are versioned, the same way
py-ds-academy's docs are:
every release gets its own URL, latest always points at the newest
release, and a rolling dev version tracks main. This is implemented with
mike, the same tool
mkdocs-material
projects use, deploying to the standard gh-pages branch layout.
Why a fork of mike is required¶
Plain mike (the package on PyPI) only knows how to build docs by calling
mkdocs directly — it imports mkdocs.config, injects an mkdocs plugin,
and shells out to mkdocs build. It has no idea zensical.toml or the
zensical CLI exist, so it can't drive this site's build.
Zensical's own team maintains a compatible fork —
squidfunk/mike — that replaces the
mkdocs-specific internals with calls to zensical build instead. Per its
README, this fork is deliberately not published to PyPI and must be
installed from git:
This is an explicitly temporary arrangement: Zensical's roadmap lists native versioning support as coming "in the coming months," at which point this fork (and the workaround below) should be retired in favor of whatever Zensical ships natively. See zensical.org/docs/setup/versioning/ for the upstream setup docs this project follows.
Because the fork can't be resolved as a normal PyPI dependency, mike is
declared in the docs dependency group in pyproject.toml — pinned to a
released mike version for the sake of having some resolvable metadata —
but every place that actually runs mike (CI and local use) overrides it
immediately afterward with the git-installed fork:
uv sync --group docs
uv pip install --python .venv \
"mike @ git+https://github.com/squidfunk/mike.git@<pinned-commit>"
The pinned commit lives in MIKE_ZENSICAL_REF in both docs.yml and
release.yml. Bump it deliberately (and re-test) rather than tracking a
branch, so CI doesn't silently pick up upstream changes.
zensical.toml version config¶
This is what turns on the version-selector dropdown in the header (Zensical
renders it via the same client-side versions.json-fetching mechanism
mkdocs-material uses) and tells the theme that latest is the
non-"outdated" alias, so older versions get an "you're viewing an outdated
version" banner and latest doesn't.
URL scheme¶
https://fast-pager.eytanohana.com/— redirects to whatevermike set-defaultlast pointed at (latest).https://fast-pager.eytanohana.com/latest/— alias for the newest tagged release.https://fast-pager.eytanohana.com/X.Y.Z/— one directory per released version (e.g./0.1.0/).https://fast-pager.eytanohana.com/dev/— tracksmain, rebuilt on every push. Useful for previewing unreleased docs changes, but not linked from the version selector's "latest" alias and not the default redirect target.
How a version gets published¶
- Push to
main(.github/workflows/docs.yml,deploy-devjob) — runsuv run mike deploy --push --branch gh-pages dev, updating thedevversion in place. This does not touchlatestor the root redirect. - Tag push matching
v*.*.*(.github/workflows/release.yml,deploy-docsjob) — after the tag-vs-pyproject.tomlversion check and CI both pass, runs:
VERSION="${GITHUB_REF_NAME#v}" # v0.5.4 -> 0.5.4
uv run mike deploy --push --branch gh-pages --update-aliases "$VERSION" latest
uv run mike set-default --push --branch gh-pages latest
This deploys the new version, re-points the latest alias at it, and
updates the root redirect to latest. The first versioned docs deploy
happens automatically the next time a v*.*.* tag is pushed — nothing
else needs to happen for X.Y.Z/latest to appear for the first time.
Both jobs authenticate as github-actions[bot] (configured via git config
before the mike deploy/set-default calls) and push using the workflow's
own GITHUB_TOKEN, so no extra secrets are needed.
Local preview of the versioned site¶
uv sync --group docs
uv pip install --python .venv \
"mike @ git+https://github.com/squidfunk/mike.git@<commit from docs.yml's MIKE_ZENSICAL_REF>"
uv run mike deploy 0.0.1-local # or any version name — doesn't push anywhere
uv run mike serve
mike serve serves the full versioned gh-pages layout (with the selector)
from your local gh-pages branch at http://localhost:8000. This writes
real commits to a local gh-pages branch — delete it afterward
(git branch -D gh-pages) if you don't want it lying around, and never push
it. For everyday single-version editing, uv run zensical serve (see
Local preview above) is faster and doesn't touch git at
all.
Version selector: current limitation¶
The version selector dropdown is supported by Zensical 0.0.51 via the
provider = "mike" config above — it is not a documented limitation. What
is still rough, and worth flagging for future maintainers:
- The
mikefork required to make any of this work is unreleased-to-PyPI and pinned by commit SHA rather than version tag (squidfunk/mike doesn't cut version-tagged releases the way jimporter/mike does). Track Zensical's versioning roadmap item for when native support lands and this workaround can be dropped in favor of whatever config Zensical ships directly.
Deployment¶
.github/workflows/docs.yml builds the site:
- On pull requests —
uv run zensical build --clean --strictruns as a CI check (no deploy). This catches broken pages/config before merge. - On push to
main— the same build runs, then (see Versioning above) thedeploy-devjob publishes thedevversion to thegh-pagesbranch. - On a
v*.*.*tag push —.github/workflows/release.yml'sdeploy-docsjob publishes the tagged version and re-pointslatestand the root redirect at it.
The deployed site is served at
https://fast-pager.eytanohana.com/, which matches the site_url
configured in zensical.toml.
One manual repo setting required
Versioned docs deploy via commits to the gh-pages branch (the
standard mike layout), not via the actions/deploy-pages artifact
flow this repo used previously. For GitHub Pages to actually serve that
branch, a maintainer with repo admin access must flip Settings →
Pages → Build and deployment → Source from "GitHub Actions" to
"Deploy from a branch", with branch gh-pages / folder / (root).
This is a one-time change; nothing in this repo's workflows can do it
for you, and until it's flipped, pushes to gh-pages will update the
branch but the live site won't reflect them.
Design docs are read-only here¶
docs/design/*.md are copied into the nav as-is and must not be edited from
this site's tooling or content changes — they're the canonical design
documents for the whole project (see DEVELOPMENT_PLAN.md). If a design doc
needs to change, change it directly and treat the docs-site content
(Tutorial, Reference, Getting Started) as downstream of it, updating those
pages to match.