Skip to content

docs(py): build the Python site with great-docs, with versioned deploys - #373

Merged
jat255 merged 26 commits into
mainfrom
jat255/fspr-great-docs-migration
Sep 14, 2026
Merged

jat255 merged 26 commits into
mainfrom
jat255/fspr-great-docs-migration

Conversation

@jat255

@jat255 jat255 commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

This PR moves the Python documentation site from quartodoc to great-docs, which gives it versioned deploys with a version selector, the version in the navbar, and a beta notice on every page. Only the build step of the docs workflow changes; the shared gh-pages layout, the deploy, and the pull request previews are untouched.

kata: fspr. Supersedes most of kata 2f1g.

Merge #372 first. It bumps pkg-py to 0.1.0b1, which this branch declares as the latest documented version while its own pyproject.toml is still 0.1.0.dev1. Nothing else depends on the ordering.

Agent-written detail

Why not build this on Quarto

Quarto has no version selector (quarto-cli#474 is open, milestone "Future") and quartodoc has none, so versioned docs on the old toolchain meant a hand-built manifest, a dropdown, canonical tags and per-page redirects. great-docs added it in v0.8, and builds on quartodoc rather than replacing it. It wraps Quarto, so Quarto still renders the site and quarto-actions/setup stays in the workflow.

What the site does now

0.1.0b1 is served at /py/, the development version at /py/v/dev/, with /py/v/latest/ and /py/v/stable/ aliases and a navbar selector. The versions: list is hand-maintained rather than derived from tags, so prerelease, eol and labels stay editorial and reviewable; adding an entry becomes a step in cutting a release, and great-docs api-snapshot <tag> can backfill a version's API pages from a git tag afterwards.

Versioned narrative pages are re-rendered from the current tree, not archived, so editing a guide page changes what older versions say. Only the API reference is snapshotted.

Things that needed working around

  • API reference names are relative to the package, so the Shiny section uses ui.app, not commons.ui.app. A section-level package: key parses and is then ignored. commons.__all__ does not export ui, so those five functions only appear because they are listed.
  • No config key adds a navbar link, a project _quarto.yml is not merged, and the generated one is rewritten every build, so the R cross-link is injected by a script into great-docs' own widget container. It is selected by class substring because great-docs silently drops any include_in_header entry whose text contains one of its gd-* markers.
  • The beta banner's link is built client-side and used verbatim, so Quarto never rewrites it per page depth and it was broken on 84 of the 90 pages carrying it. It now takes its prefix from Quarto's offset meta, which keeps it correct in previews and in versioned copies.
  • The favicon links were written against site_url, so no icon resolved outside the published site. They are the only absolute URLs in the build and are relativized after rendering.
  • The navbar version badge is removed, not corrected. great-docs derives it from the newest GitHub Release for the whole repository with no tag-prefix filter, so in this monorepo it showed whichever series published last. The selector already names the version, from config.
  • The generated changelog is off for the same reason: it rendered the R package's release notes onto the Python site.
  • setup-github-pages is not used. It targets the Actions Pages source, which owns the whole site and would take /r/ and /r/dev/ offline, and it replaces hosted previews with a local-build comment.

Layout and styling

Design notes moved to pkg-py/notes/, because great-docs renders everything under the directory user_guide: names and the explicit Quarto render list that kept them out is gone. Guide pages carry numeric filename prefixes to set their order, which great-docs strips, so the published URLs are unchanged and the navbar lands on the introduction. The synced docs/assets/ and docs/favicon/ copies stay where scripts/sync-shared.sh puts them.

The stylesheet overrides great-docs' bold reference entries, its orange current-page and hover accents, its outlined page-navigation buttons, and a table rule that sized the feature-parity table to its widest unwrapped line. Two custom properties at the top of the file are the knobs for the accent treatment.

Verification

ruff, pyrefly and the 1499 Python tests pass, as do both .github/scripts suites. The site was checked in headless Chromium rather than by reading the markup: the banner link is followed from the landing page, guide pages, the reference index, a reference detail page and a versioned copy, and the favicons resolve at three depths and inside /v/dev/. A build served from a pr-<n>/py/ prefix resolves its assets, which was the open risk in the design. roborev compact re-verified every finding raised on the branch against the final tree and reported none outstanding.

great-docs takes the navbar version badge from the newest GitHub Release for
the whole repository and cannot filter by tag prefix, so in this monorepo it
shows whichever of the two release series published most recently. The only
Release so far is r-v0.1.0, which rendered as vr-v0.1.0 on the Python site.

The badge is injected during great-docs' own post-render step, so it cannot
be corrected mid-build. This rewrites it afterwards from pkg-py/pyproject.toml,
the same way noindex-preview.py post-processes built HTML.
Quarto has no version selector (quarto-cli#474 is open with milestone
Future) and quartodoc has none, so versioned docs on the old toolchain meant
hand-rolling a manifest, a dropdown, canonical tags and redirects. great-docs
does it from a versions list, and builds on quartodoc rather than replacing
it. It wraps Quarto, so Quarto is still what renders the site.

The design notes move to pkg-py/notes because great-docs renders everything
under the directory user_guide names, and the old explicit Quarto render list
that kept them out is gone.

The generated changelog is off: it reads GitHub Releases for the whole
repository with no tag-prefix filter, so it put the R package's notes on the
Python site.
great-docs has no config key for a custom navbar link, does not merge a
project _quarto.yml, and rewrites the generated one every build, so the R
cross-link is injected with a script next to its own navbar tools. The empty
span is what docs/assets/styles.css already turns into the R logo, so only
the markup had to be reproduced.

The favicon block stays an include rather than becoming the favicon: key,
which takes a single file and would drop the manifest and the other sizes
that the R site serves from the same shared /favicon/.

The custom stylesheet goes under site.css. A top-level css: key is accepted
and silently ignored, which leaves the site unstyled.

Relative links had to move with the pages. The narrative pages now render
under user-guide/, so the homepage's links into them gained that prefix, and
the governance page's hops out to the R site became absolute. Relative
cross-site links cannot survive versioning anyway: from /py/v/<tag>/ a ../r/
hop lands in /py/v/r/ rather than at the site root. The stylesheet's R logo
url() moved for the same reason the hex image's src did, because great-docs
copies docs/ to the user-guide/ prefix and surfaces only the navbar logo at
the root.
… date

With a navbar logo rather than a text title, great-docs puts the version in a
Tippy tooltip on the brand link instead of a badge span, so the wrong version
reached the page by a second route that the first version of this script did
not cover. This repository's config sets a logo, so the tooltip was in fact
the only form appearing.

The release date is now dropped rather than preserved. It comes from the same
GitHub Release as the version, so on the Python site it was the R package's
release date, and pyproject.toml carries no date to put in its place.
Correcting the version while leaving the date would trade one wrong claim for
another.
The version list is hand-maintained so prerelease, eol and label stay
editorial rather than inferred from py-v* tags, which would have had to guess
that 0.1.0b1 is a prerelease. Adding an entry becomes a release step.

0.1.0b1 takes the latest slot, so /py/ serves what `pip install commons`
resolves to, and the development version sits at /py/v/dev/. `latest` is set
explicitly because a prerelease is excluded from latest auto-detection and
every entry here is one.

The announcement banner carries the beta warning, which reaches readers who
land on an API reference page rather than the homepage. It also means the
visible version in the navbar comes from the selector, built from this list,
rather than from the release-derived badge.
Only the build step changes. The copy into docs/py, the shared gh-pages
deploy with clean:false, the noindex pass and the pr-<n> previews are
untouched, because those are what let the two sites and the previews share
one branch. quarto-actions/setup stays: great-docs shells out to Quarto.

The release: published trigger is replaced with the py-v* tag push it was
presumably meant to be. Releases are cut by pushing a tag, so the old
trigger never fired.

Verified by running scripts/preview-docs.sh py and serving the result from a
pr-<n>/py/ subpath: the stylesheet, the R logo, the hex image, a reference
page and a versioned copy all resolve, so the absolute site_url does not
break a preview served from a deeper prefix.
AGENTS.md described the published tree without saying what builds the Python
half or that it now publishes a copy per release. favicon/README.md pointed
at a favicon partial that no longer exists; those tags moved into
great-docs.yml, since great-docs' own favicon key takes a single file and
would drop the manifest and the other sizes.
A fix to docs-version-badge.py would otherwise wait for an unrelated docs
change before reaching the deployed site, because the path filters did not
mention it.

The preview script runs the helper through uv's interpreter rather than the
system python3, matching the line above it that reads the version the same
way.
The R cross-link was injected straight into .quarto-navbar-tools, which is
empty in the static HTML: great-docs collects its widgets at runtime into a
#gd-navbar-widgets flex container inside it. The link sat outside that
container and so missed its alignment and gap, leaving the logo floating
above the row. It now waits for that container and joins it, carrying the
gd-navbar-icon class so it is drawn like the buttons beside it.

The hex sticker becomes the configured logo. It was an inline image in
index.qmd, which great-docs rendered underneath its own logo, so the landing
page showed the python mark above the hex.

Reference entries, navbar links and table links drop to normal weight.
great-docs sets them in bold monospace, and on a page that is mostly a list
of symbol names the weight on top of an underline reads as shouting.

The announcement banner paints a mid-blue background but left its text at the
body colour, which is close to unreadable in light mode, and an inline code
span kept its own pale pill background and disappeared into it.
The author entry gains its ORCID, Posit page and GitHub account. great-docs
renders each as an icon link beside the name, which is how pkgdown presents
the same thing on the R site; it does not link the name text itself.

The hand-written favicon block was broken. Its paths began with a slash,
which the old Quarto project resolved against the Python site root, but
great-docs puts the narrative source under user-guide/, so pkg-py/docs/favicon/
landed at user-guide/favicon/ and all five links resolved to nothing. Rather
than repoint them at that prefix, great-docs now derives the icon set itself
from a favicon: source, so the link tags are generated and there is no
hand-maintained HTML to drift.

That source is the same shared icon the other two sites serve, so a tab still
shows the same image everywhere. It names the PNG rather than the SVG on
purpose: great-docs rasterizes an SVG with cairosvg, which needs a system
cairo library and which warns and skips the raster sizes when absent. Pillow
handles the PNG with nothing extra.

Two things go with the hand-written block: the favicon.svg link that modern
browsers would have preferred, and the site.webmanifest link.
The navbar and the section list both marked the current page with a rule
under it. The navbar's own bold had been lost to the earlier weight
override, which was not scoped to exclude the active item, and the sidebar
item was orange with a thick blue underline, two accent colours the site
does not otherwise use. Both now use weight alone.

Tables were sized to their widest unwrapped line and then scrolled
horizontally. That suits a data table, but the feature-parity table is
prose, so it ran past the page rather than wrapping inside it.

The previous and next page links were drawn as outlined buttons; they are
ordinary links.
The label comes from reference.title, which great-docs only reads when
reference is a mapping. As a bare list of sections it was ignored and the
entry fell back to "Reference", so the sections move under a sections: key.

The current-sidebar-item colour moves behind two custom properties. great-docs
hardcodes it in its own stylesheet with no config key, so a CSS override is
the only lever and this makes it one value to change rather than a rule to
rewrite.
Hovering a navbar link filled it with an orange pill and white text. It is a
literal in great-docs.scss with a dark-mode twin at a higher specificity, so
both selectors are spelled out. A neutral tint matches the hover already used
by the icon buttons beside it.

The orange reaches four places in all. Two are the --gd-active-link theme
variable, now pointed at currentColor so the dark-mode sidebar and
on-this-page active items follow; two are literals needing their selectors
overridden. --gd-syn-operator is deliberately untouched, being the operator
colour inside code blocks rather than site accent.
The light tint was 6% ink, which is what great-docs uses for the resting
state of its navbar icon buttons, not their hover, so on a #f0f0f0 navbar it
was all but invisible. Both themes now sit at 14%, which lands them a similar
distance from their own background: #cecece against light, #3a3a3a against
dark. That is a step past the 10% and 12% great-docs hovers its icon buttons
at, because those have a border to help them read and a bare text link does
not.
The hover label was set to `inherit`, which takes the colour of the parent
list item rather than the link's own, so on the new hover tint it came out
almost white. It now names the navbar text colour, which is already
theme-aware.

The active navbar item still carried a blue underline. There are two
underlines on it: a bottom border from .nav-underline, which was already
suppressed, and a text decoration from .nav-link.active in the accent blue,
which was not.

The override block is also consolidated. An earlier edit in this branch left
a duplicate of the current-page rule, and the variable declarations had
spread across two :root and two body.quarto-dark blocks.
Pointing --gd-active-link at currentColor removed the orange but left the
current item inheriting a colour dimmer than its own siblings, so in dark mode
it read as the least prominent entry in the list rather than the most. Both
the variable and the light-mode override now name --gd-text-primary, which is
itself theme-scoped: #e0e0e0 against the #c8c8c8 of the other items, and the
darkest text in light mode.

The hover pill carries 8px of padding on each side but a -8px left margin,
which pulled it flush against the sidebar's left edge while leaving it inset
on the right. Dropping the negative margin makes the inset even.
The guide was ordered alphabetically, so it opened on the feature-parity
table and the navbar entry pointed there too. Numeric filename prefixes set
the order in auto-discovery mode and great-docs strips them when it copies
the pages, so the published URLs are unchanged and the navbar follows the
first file.

The explicit user_guide list was the other option and was rejected: it only
reads the conventional user_guide/ directory, which would have meant moving
the pages out of docs/ and repointing the asset paths that the stylesheet and
the favicon source depend on, and it requires a section heading the sidebar
does not currently have.
Josh's wording, taken as written.
The banner is built client-side from a meta tag and its url is used verbatim,
so Quarto never rewrites it per page depth. A relative link therefore resolved
against the current page's directory: from user-guide/ the banner asked for
user-guide/user-guide/feature-parity.html. It was broken on 84 of the 90 pages
that carry it, everything but the six at the site root.

Quarto's own offset meta supplies the prefix, which is how great-docs' version
selector and sidebar scripts locate the site root, and it stays correct in a
pull request preview and inside a versioned copy. An absolute or root-relative
url would have sent preview traffic to the published site instead.

The link is found by a class substring rather than by name because great-docs
drops every include_in_header entry whose text contains its own
"gd-announcement" marker, which removed an earlier version of this script
without any warning.
The previous attempt never ran to completion. Assigning to a.href reflects the
resolved absolute URL into the content attribute, so by the time the script
read getAttribute("href") it saw the already-doubled absolute path, which
tripped the guard that skips links needing no prefix. The raw relative path is
now read back from the announcement meta, which is the only place it survives
unresolved.

Verified in Chromium rather than by reasoning about the emitted markup:
the banner link is followed from the landing page, two guide pages, the
reference index, a reference detail page and a versioned copy, and every one
returns 200 on the feature-parity page, with the versioned copy correctly
staying inside its own /v/dev/ tree.
The navbar carried only the hex at 24px, which is too small to read as a
sticker and left the site unnamed. It now shows the Python mark beside
"commons", as the R site shows the R mark beside it. show_title is required:
great-docs suppresses the navbar's text title whenever a logo is set.

The hex moves to hero.logo, so the landing page keeps it at full size. That
key otherwise inherits the navbar logo, which is why the two had to be set
separately rather than one replacing the other.
The favicon links were written against site_url, so every page pointed at the
published site and no icon appeared anywhere else: not locally, not in a pull
request preview, not in a versioned copy. The generated icons sit at the root
of each copy, so Quarto's per-page offset is the right prefix. These were the
only absolute URLs in the built site.

The package name was drawn as a bordered pill with the version beside it. The
border is gone and the badge is now stripped rather than corrected, because
the version selector already names the version and is driven by the list in
great-docs.yml rather than by a repo-wide GitHub Release lookup. With the
navbar title shown, the badge is the only place great-docs puts a version, so
nothing is left showing the wrong one and the correction has nothing left to
do.

The hero logo was a broken image: great-docs emits hero.logo as the img src
verbatim and, unlike the navbar logo, does not copy the file, so the path now
names where the asset actually lands.

docs-version-badge.py becomes docs-postprocess.py, since relativizing the icon
links is the job that remains and the badge is removed in the same pass.
The rewrite replaced the site URL in every quoted string, not just favicon
hrefs. great-docs also injects a canonical-URL script that concatenates the
site URL with a path at runtime, so rewriting it to a page-relative prefix
would have produced a bogus canonical URL on every non-latest version. It
escaped only because that script omits the trailing slash the pattern
required, which is not a property worth relying on. Now only <link> tags are
touched, with regression tests for both spellings of the script's base.

The author entry declared github twice. PyYAML keeps the last of a duplicate
key, so it worked, but stricter parsers reject it.
@jat255 jat255 added py Affects the Python implementation needs-manual-review Agent-created work that needs a human review labels Sep 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Preview root: https://posit-dev.github.io/commons/pr-373/

Python site preview: https://posit-dev.github.io/commons/pr-373/py/

Built from the latest commit on this branch. The R links in it point at the published R site, which no pull request rebuilds.

The offset fix held locally and on the published site but not in a pull
request preview, where the banner link 404ed again. Quarto's external-link
pass runs after it and restores link.dataset.originalHref, which quarto-nav.js
had set to the banner's original un-prefixed url, undoing the correction. That
pass only treats this link as external when the served prefix differs from
site_url, which is true of every preview and of neither localhost nor the
published site, so no local check could have caught it.

The link now carries Quarto's own .no-external opt-out, which excludes it from
that pass, and the stashed original is updated to the corrected path so the
restore is harmless if it runs anyway.
The links are built relative to the package root, which in this monorepo is
pkg-py/ rather than the repository root, so every "source" button pointed at
a path that does not exist.

great-docs has a source.path key for exactly this, and it cannot be used: it
replaces the whole path with the file's basename, which is right for a flat
layout but flattens commons/_ui/_server.py and the three others like it. The
package directory is inserted after rendering instead, which preserves the
real layout.

Verified against the repository rather than by eye: all nine distinct source
paths the reference emits exist under pkg-py, and the generated URL for
Commons resolves on both this branch and main.
…repo

The workflow file was renamed to py-docs.yaml, which left its own paths filter
naming a file that no longer exists, so a change to the workflow stopped
triggering it. The job id and concurrency group still said quartodoc, as did
comments in three sibling workflows and in the preview script. main has no
branch protection, so renaming the job breaks no required check.

The source-link rewrite is now scoped to this repository. It matched any
GitHub blob URL with a src/ path, so a documentation link into another
project's source tree would have gained a pkg-py/ prefix. The repository URL
comes from the same config the site URL does.
@jat255 jat255 removed the needs-manual-review Agent-created work that needs a human review label Sep 14, 2026
@jat255 jat255 added the documentation Improvements or additions to documentation label Sep 14, 2026
@jat255
jat255 marked this pull request as ready for review September 14, 2026 21:14
@jat255
jat255 merged commit 726a2ed into main Sep 14, 2026
12 checks passed
@jat255
jat255 deleted the jat255/fspr-great-docs-migration branch September 14, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation py Affects the Python implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant