Skip to content

Move docs off MkDocs onto ProperDocs, upgrade Material to 9.7, fix broken links - #341

Merged
ewels merged 5 commits into
mainfrom
claude/docs-framework-migration-5w6uhx
Aug 10, 2026
Merged

ewels merged 5 commits into
mainfrom
claude/docs-framework-migration-5w6uhx

Conversation

@ewels

@ewels ewels commented Aug 10, 2026 •

Copy link
Copy Markdown
Owner

MkDocs has been unmaintained since August 2024 and its planned v2 drops support for existing themes, plugins and config files — and our mkdocs pin had no upper bound, so a v2 release to PyPI would have broken the docs build outright. Material for MkDocs is also EOL on 5 November 2026.

This swaps the build engine for ProperDocs (a drop-in fork of MkDocs 1.x), bumps Material 9.5 → 9.7, fixes some long-standing broken links, and turns on strict so links can't rot again. The site looks and works exactly as before — verified page by page. The eventual move to Zensical is deliberately not attempted here.

Two things worth a second opinion, both covered below: the mike floor bump, and whether strict is too sharp an edge.

Details

Changes

Build engine → ProperDocs

ProperDocs is a fork of MkDocs 1.x committed to no breaking changes. Its output is byte-identical to MkDocs apart from the <meta name="generator"> tag, and mike detects and drives it automatically, so docs/_deploy.py and .github/workflows/build-docs.yml need no changes at all.

One gotcha worth flagging for review: mike only gained ProperDocs detection in 2.2. The old mike>=2.1.3 floor would have let a resolve land on 2.1.3 and silently keep building through MkDocs, so the floor is bumped. mkdocs is still pulled in transitively by mkdocs-material and mike, so it is capped below v2.

Material for MkDocs 9.5 → 9.7

The final feature release, which folds in the features that used to be exclusive to Insiders. Builds clean with no config changes. Rendered output is unchanged apart from asset hashes and a FontAwesome 6 → 7 bump.

Fixed broken links

e608f63 split documentation/panels.md into panels/{overview,tips,advanced}.md and left two references behind:

  • The comparison page linked to panels/panels.md, a dead link.
  • The groups_and_sorting.md redirect pointed at documentation/panels.md. Because the target did not exist, mkdocs-redirects skipped the page entirely, so the old v1.8 URL /documentation/groups_and_sorting/ was returning a hard 404 rather than redirecting. It resolves again now.

Also fixed the panels overview image, which had one ../ too many and so never rendered, and marked the under-construction documentation/structure.md as not_in_nav (it is still published, exactly as before).

strict: true

Nothing stopped those links from rotting again. strict turns any warning into a build failure, including plugin warnings — which matters, because the broken redirect target was reported by mkdocs-redirects rather than by the link checker, so no validation: setting would have caught it.

validation.links.anchors and unrecognized_links default to info, meaning broken anchors pass silently; both are raised to warn so strict acts on them. Neither surfaces any existing problem.

Trade-off worth a second opinion: strict is indiscriminate. If the social-cards, rss or glightbox plugins ever emit a warning, main stops publishing docs until someone fixes it. Happy to drop the strict commit if that is too sharp an edge.

Testing

Built origin/main with its own toolchain (MkDocs 1.6.1, Material 9.5.50) and this branch with the new one, then text-diffed every page:

  • No page present on main is missing here. One page is added: the restored groups_and_sorting redirect.
  • Across all 30 shared pages, the only text difference is the new changelog entries this PR adds.
  • mike deploy --update-aliases 1.9 latest in a scratch clone: builds via ProperDocs, writes to gh-pages, mike list reports 1.9 [latest], versions.json correct, version-selector markup unchanged.
  • Reintroduced each of the three link bugs in turn, plus a link to a non-existent anchor: each now aborts the build, including through the mike deploy path that CI uses.
  • prek run -a clean locally (minus actionlint, which needs a Go download, and mypy, which needs project deps — both pass in CI).

Not verified: social card generation. The sandbox blocks fonts.google.com, which the plugin fetches Kode Mono from, so the comparison builds ran without it. The plugin loads fine under ProperDocs — it gets as far as the font fetch — but the first CI run is what will confirm the cards still render. Worth watching on merge, particularly now that strict would turn a warning from it into a failed deploy.

A correction, for the record

An earlier revision of this branch dropped mkdocs-github-admonitions-plugin as unused. That was wrong, and 70de252 puts it back. docs/changelog.md and docs/contributing.md are thin wrappers that pull in the root CHANGELOG.md and CONTRIBUTING.md via include-markdown, and those two files contain five GitHub-style alerts that rendered as plain blockquotes with a literal [!WARNING] in the body text without the plugin. The net change to that plugin's config in this PR is now just a comment explaining that it has to run after include-markdown.

Why not Zensical

Zensical is the official successor and where this should eventually land, but at 0.0.53 it is not ready for this project specifically. Building these docs with it: the editor.html override fails outright (minijinja has no .items() on maps), the blog plugin is unsupported so every existing post URL changes and archive/category pages vanish, RSS and redirects produce no output, and include-markdown markers pass through raw (the changelog page drops from 74 KB to 21 KB). Worth revisiting once blog support lands.

claude added 5 commits August 10, 2026 11:47
MkDocs has been unmaintained upstream since August 2024, and its planned
v2 explicitly drops support for existing themes, plugins and config
files. The docs group had no upper bound on `mkdocs`, so a v2 release to
PyPI would have broken the docs build outright.

Switch the build engine to ProperDocs, a drop-in fork of MkDocs 1.x that
is committed to no breaking changes. Its output is byte-identical to
MkDocs apart from the `<meta name="generator">` tag, and `mike` detects
and drives it automatically from 2.2 onwards, so `docs/_deploy.py` and
the build-docs workflow need no changes. `mkdocs` is still pulled in
transitively by mkdocs-material and mike, so it is capped below v2.

Also bump Material for MkDocs from 9.5 to 9.7. That is the final feature
release before it goes to end of life in November 2026, and it folds in
the features that used to be exclusive to Insiders. The rendered output
is unchanged apart from asset hashes and a FontAwesome 6 to 7 bump.

Finally, drop mkdocs-github-admonitions-plugin and its `gh-admonitions`
entry. Nothing in docs/ uses GitHub-style `> [!NOTE]` alerts, so it was
dead configuration.

Verified by building the site with both the old and new toolchains and
diffing: the same 30 pages are produced, and the visible text of every
page is identical except for the two pages whose source changed on main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBo3PGTF2T7gT2X1wut1Ft
Commit e608f63 split `documentation/panels.md` into `panels/overview.md`,
`panels/tips.md` and `panels/advanced.md`, but two references to the old
path were missed:

- The comparison page linked to `panels/panels.md`, which rendered as a
  dead link.
- The `groups_and_sorting.md` redirect pointed at `documentation/panels.md`,
  so the redirect page was skipped entirely and the old v1.8 URL 404'd.
  It now resolves to the overview page again.

Also fix the panels overview image, which had one `../` too many and so
never rendered, and mark `documentation/structure.md` as `not_in_nav`.
That page is a deliberate under-construction stub whose nav entry is
commented out, so the build no longer reports it as an oversight. It is
still published, exactly as before.

The docs now build with no warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBo3PGTF2T7gT2X1wut1Ft
Nothing stopped the links fixed in the previous commit from rotting
again: a rename that orphans a link only produced a warning, and the
build carried on and published the dead link.

`strict: true` turns any warning into a build failure, which covers
plugin warnings too. That matters here, because the broken
`mkdocs-redirects` target was reported by the plugin rather than by the
link checker, so no amount of `validation:` config would have caught it.

The two `validation.links` options default to `info`, meaning broken
anchors and malformed internal links are silently ignored. Raised to
`warn` so `strict` acts on them. Neither surfaces any existing problem;
the docs build clean today.

Verified by reintroducing each of the three bugs from the previous
commit in turn, plus a link to a non-existent anchor: each one now
aborts the build. Also verified through `mike deploy`, which is the path
CI actually uses, since `strict` has to survive mike's config handling
to be worth anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBo3PGTF2T7gT2X1wut1Ft
Dropping this in 003d852 was wrong. The check behind it grepped only the
docs directory, but `docs/changelog.md` and `docs/contributing.md` are
thin wrappers that pull in the root CHANGELOG.md and CONTRIBUTING.md via
include-markdown, and those files contain five GitHub-style alerts: four
in the changelog, one in the contributing guide.

Without the plugin they rendered as plain blockquotes with a literal
"[!WARNING]" or "[!NOTE]" as the first line of body text.

The plugin has to run after include-markdown for this to work, since the
alerts arrive with the included content, so it goes back in the same
position it was in before, now with a comment saying why.

Re-verified against origin/main properly this time, by text-diffing every
page rather than sampling: the only difference across all 30 shared pages
is the new changelog entries themselves, plus the groups_and_sorting
redirect page this branch restores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBo3PGTF2T7gT2X1wut1Ft
pyproject-fmt aligns trailing comments to the longest entry in the list,
so adding `mkdocs-github-admonitions-plugin` back in 70de252 shifted the
column and left the `mike` comment two spaces short. I ran the formatter
after the earlier edits to this list but not after that one, which is
what broke the prek job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBo3PGTF2T7gT2X1wut1Ft
@ewels
ewels merged commit 54fea4e into main Aug 10, 2026
302 checks passed
@ewels
ewels deleted the claude/docs-framework-migration-5w6uhx branch August 10, 2026 15:07
@dwreeves

Copy link
Copy Markdown
Collaborator

Ah, TIL about ProperDocs. I did know MkDocs was unmaintained but I didn't know there was a replacement. Cool!

@ewels

ewels commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Yes - it's a bit of a mess but it's something. I don't see it as much more than a stopgap though to be honest, as mkdocs-material is still unmaintained now.

Zensical is being touted as the successor and seems very nice to use, but it's still early days and it doesn't have much in the way of customisability yet. My favourite is still Astro (see other proof-of-concept PR #342).

@dwreeves

dwreeves commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator

Probably not worth too much experimentation unless you want to have fun with it. The docs are fine, even if the framework is not receiving updates.

Well, actually, there is one issue with the docs... the prerelease tag was never removed in 1.9.x.

image

I have a fix to _deploy.py written, but I also see you have a much larger PR migrating to a different framework.

# fmt: off
# /// script
# requires-python = ">=3.13"
# dependencies = [
#     "packaging"
# ]
# ///
from typing import Sequence, Optional, Tuple
import os
from importlib.metadata import version
import subprocess
import json

import packaging.version


def print_section(v: str) -> None:
    print("=" * 79)
    print(v)
    print("=" * 79)


def print_subsection(v: str) -> None:
    print("-" * 79)
    print(v)
    print("-" * 79)


def cmd(args: Sequence[str]) -> subprocess.CompletedProcess[str]:
    print_section(f"Command: {' '.join(args)}")
    res = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, text=True)
    if res.returncode:
        if res.stdout:
            print_subsection("Stdout:")
            print(res.stdout)
        if res.stderr:
            print_subsection("Stderr:")
            print(res.stderr)
        res.check_returncode()
    return res


def get_series(identifier: str) -> Optional[Tuple[int, int]]:
    """(major, minor) of the version an alias/version points at, or None if it does not exist."""
    try:
        res = cmd(["mike", "list", identifier, "-j"])
    except subprocess.SubprocessError:
        return None
    if not res.stdout.strip():
        return None
    data = json.loads(res.stdout)
    major, minor, *_ = data["version"].split(".")
    return int(major), int(minor)


def deploy() -> None:
    cmd(["git", "fetch", "origin", "gh-pages:gh-pages"])
    v = packaging.version.parse(version(os.environ["PACKAGE_NAME"]))

    head = (v.major, v.minor)
    latest = get_series("latest")
    prerelease = get_series("prerelease")

    aliases = []
    if v.is_prerelease:
        if latest is None or head > latest:
            aliases.append("prerelease")
    elif latest is None or head >= latest:
        aliases.append("latest")

    if not v.is_prerelease and prerelease == head:
        cmd(["mike", "delete", "--push", "prerelease"])

    cmd(["mike", "deploy", "--push", "--update-aliases", f"{v.major}.{v.minor}", *aliases])

    if latest is not None or "latest" in aliases:
        cmd(["mike", "set-default", "--push", "latest"])


if __name__ == "__main__":
    deploy()

@dwreeves

Copy link
Copy Markdown
Collaborator

Oh! Looked more carefully. I see the mike versioning stays in tact. So, I should fix it then! No overlap!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants