The website was made using the following:
- MkDocs
- Material for MkDocs
- Material for MkDocs Extensions
- PyMdown Extensions
- Awesome Nav for MkDocs
- mkdocstrings
- mkdocs-autorefs
- mkdocs-section-index
- MkDocs Site URLs
- mkdocs-git-revision-date-localized-plugin
- mike (documentation versioning)
Automatic code documentations assumes google-style docstrings. For examples on how to format google-style docstrings, see here: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html
Note for Mac Users: On a Mac with Apple Silicon it may be necessary to first install some relevant libraries that are needed for OpenDSM:
brew install openblas
brew install lapack
Each of the above will also prompt you to set some environment variables with commands like export LDFLAGS="/some/appropriate/location/", so run these commands one at a time and look at the tail end of the output for instruction.
To install the necessary packages, run the following from the opendsm-website directory
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install .
python3 -m mkdocs serveFrom opendsm-website directory run:
. .venv/bin/activate
python3 -m mkdocs serveWhen editing opendsm docstrings (which feed the API reference), also watch the package
source so the API pages live-reload: mkdocs serve -a localhost:8001 -w ..
The published site is versioned with mike. Each
documentation version is published at /vX.Y/ (shown as X.Y in the version selector) and
mirrors an opendsm release; patch releases
(vX.Y.Z) refresh the existing vX.Y version rather than creating a new one. main builds
the dev version; the newest stable release carries the stable alias (shown as a badge in
the version selector), and the site root redirects to stable. mike orders the version selector itself (dev first, then releases
newest→oldest).
Deployment runs from .github/workflows/website_deployment.yaml:
- push to
main→ builds/refreshes thedevversion (opendsm@master) - push a
vX.Y.Ztag → builds versionvX.Y, movesstable, sets it as default - Actions → Run workflow (
workflow_dispatch) → builds an arbitrary version (for backfilling)
The API reference is generated from the installed opendsm, so a version must be built with
pyproject.toml pinning opendsm to that release — otherwise it silently documents master
(which can differ substantially). To make this reliable, every published stable version lives
on a long-lived version/X.Y branch: it is main's content with the opendsm dependency
re-pinned to the release commit/tag. dev (built from main) keeps @master.
main, gh-pages, and version/** are protected from deletion by a repository ruleset.
Force-pushes are still allowed, so the refresh below works.
Publishing or refreshing a stable version (e.g. 1.2):
- Rebuild the branch from current
mainand re-pin opendsm:git checkout main && git pull git checkout -B version/1.2 # in pyproject.toml: opendsm @ git+https://github.com/opendsm/opendsm@<release-commit-or-tag> git commit -am "Pin opendsm to the 1.2 release" git push -f origin version/1.2
- Actions → Run workflow on
version/1.2withversion=v1.2,alias=stable,set_default=true.
Backfilling an older version is the same flow on its own version/X.Y branch, dispatched with
version=vX.Y (leave alias/set_default unset unless it should become the newest stable).
The install page and the version/Python badges pin themselves at build time. The
hooks/version_pins.py hook reads the installed opendsm version and its supported
Python versions, then checks PyPI: if a newer release line exists, the version is treated
as superseded and its install page renders the pinned command
(uv pip install "opendsm==X.Y.Z" --exclude-newer <date>), links the release tag, switches
the badges to static values, and adds a deprecation note. The freeze date is the successor
line's release date, read from PyPI.
So deprecating a version needs no page edits — only the pyproject.toml opendsm pin you
already set on its version/X.Y branch. Two overrides exist:
- When an intermediate line was skipped, add it to
SUPERSEDED_BYinhooks/version_pins.py(e.g."1.0": "1.2"freezes 1.0 to 1.2's release date rather than the abandoned 1.1). - To force a date for a one-off build, set the
OPENDSM_FREEZE_DATEenv var (thefreeze_dateworkflow_dispatch input wires it through).
Content and code render with plain mkdocs serve. The version-selector dropdown only appears
when a versions.json is reachable, so to preview it either run mike serve (after a local
mike deploy), or drop a throwaway gitignored src/versions.json:
[{"version": "dev", "title": "dev", "aliases": []}, {"version": "v1.2", "title": "v1.2", "aliases": ["stable"]}]- Internal links and assets must use
site:-prefixed paths (e.g.site:assets/...,site:documentation/...), never root-absolute/assets/.... Onlysite:links are rewritten to the active version's base; absolute links break once served under a version path. CNAMEandrobots.txtlive at thegh-pagesroot, not undersrc/(which would land in a version directory). A manualgh-pagescleanup must preserve both.
navigation.instant is deliberately off and should stay off as long as the Osano cookie-consent
script (src/overrides/main.html) is loaded. This was investigated end-to-end; the conflict is
architectural and cannot be cleanly fixed. Don't repeat the investigation.
Instant loading turns the site into an SPA — on every internal navigation it removes the old
[data-md-component=container] subtree and inserts the new page's, rather than doing a full reload.
Osano's bundle runs a MutationObserver on document.documentElement (the whole tree), and on every
childList change it (a) re-scans all added nodes to enforce script/cookie/iframe blocking, and (b)
re-renders its consent widget when one of its required nodes is removed. Instant nav fires both on
every navigation: the widget re-render is a visible consent-banner flash, and the full re-scan of
the incoming page is a multi-second cursor stall on large pages (e.g. the CalTRACK technical
appendix). Confirmed by reading both Material's instant-nav code and the Osano bundle; Osano injects no
<style>/<link>, so this is not a CSS-preservation problem — the trigger is the DOM mutation itself.
Why it can't be fixed: Osano's observer scope and behavior are hardcoded in its bundle and are its
consent-enforcement — suppressing or narrowing the observer defeats the compliance function it exists
for. Self-hosting a patched osano.js would freeze a compliance-critical, frequently-updated,
obfuscated third-party script and require perpetual re-patching, with real compliance-drift and
security risk. Neither is an acceptable trade for snappier navigation on a docs site.
Conclusion: instant nav is incompatible with any document-wide consent/observer script like Osano.
If the goal was avoiding the version selector's first-load late-paint, solve that without instant nav
(see the version-box-static approach), or accept it.