Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Tutorial: add versioned docs to a fresh repo

This walks you, start to finish, through giving a MyST docs site a pydata-style version switcher and a versioned GitHub Pages deployment. By the end you will have:

It assumes a repo that already builds docs with myst build --html from a docs/ directory. Replace ORG/REPO throughout. The snippets below are pinned to the latest release (v0.27.0); bump that pin to any version from this project’s releases when you need a different one.

1. Add the plugin to your MyST project

In docs/myst.yml, load the plugin from its release asset and route a navbar part:

# docs/myst.yml
project:
  plugins:
    - https://github.com/DiamondLightSource/myst-version-switcher-plugin/releases/download/v0.27.0/version-switcher.mjs
site:
  template: book-theme
  parts:
    navbar_end: navbar_end.md

Then place the directive (see the directive reference for all options):

<!-- docs/navbar_end.md -->
:::{version-switcher}
:json-url: https://ORG.github.io/REPO/switcher.json
:::

The json-url points at a switcher.json that does not exist yet — publish.yml will generate it on your first deploy.

2. Set the Pages source to “GitHub Actions” and allow deploys from any ref

In Settings → Pages, set Source to GitHub Actions (not “Deploy from a branch”). deploy-pages refuses to publish otherwise.

Because internal PRs deploy from their own ref, the github-pages environment’s deployment policy must allow those refs. In Settings → Environments → github-pages, it is recommended to set Deployment branches and tags to No restriction.

3. Add the workflow files

Your project will use this project’s two reusable workflows to build a single version of the docs, then publish all available versions of the docs into a single GitHub Pages site.

It is recommended that you use the structure below.

ci.yml

This is the entry point, it defines two jobs:

It does not publish. publish.yml (below) picks this run up once it completes, which keeps the site’s reconstruction cost off your PRs and stops a Pages failure showing as a red check against an author who cannot act on it.

# .github/workflows/ci.yml
name: CI
on:
  pull_request:
  push: 
    branches: [main]
    tags: ['*']

jobs:
  docs:   # Call the docs building workflow directly
    uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/docs.yml@v0.27.0
    with:
      # Whatever turns your sources into docs/_build/html at $BASE_URL. uv and Node
      # are preinstalled, so: make docs · tox -e docs · npm ci && npm run docs
      build-command: myst build --html

  release: 
    needs: [docs]
    if: github.ref_type == 'tag'            # tag pushes only
    uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/release.yml@v0.27.0
    permissions:
      contents: write                       # create the Release + attach assets

ci.yml has no publish job at all — publishing is a separate workflow that picks this run up once it finishes.

publish.yml

The one file you carry, and the one place you pin @v0.27.0. It calls the engine (publish-gh-pages.yml) when CI completes. Copy it verbatim:

# .github/workflows/publish.yml
name: Publish
on:
  # Matches your CI workflow by its `name:`, NOT its filename. Rename that and you must
  # change it here too, or publishing silently stops.
  workflow_run:
    workflows: [CI]
    types: [completed]
  # Fork-PR previews and manual re-deploys.
  workflow_dispatch:
    inputs:
      pr:
        description: "Fork PR to approve + preview (empty = plain re-deploy)"
        required: false
        default: ""

jobs:
  publish:
    # Both guards matter. `conclusion` because workflow_run fires on failed runs too, and
    # a failed run has no usable docs artifact. `head_repository` because workflow_run
    # gets a WRITE token even when a fork's PR triggered it — without this, untrusted
    # code reaches your live site. Both fail OPEN if removed.
    if: >-
      github.event_name == 'workflow_dispatch' ||
      (github.event.workflow_run.conclusion == 'success' &&
       github.event.workflow_run.head_repository.full_name == github.repository)
    uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/publish-gh-pages.yml@v0.27.0
    with:
      pr: ${{ inputs.pr }}
      # The whole site deploys as ONE Pages artifact, and Pages rejects it over 1 GB.
      # Every release and every open PR is a full copy of the docs, so these cap what
      # gets published (0 = no limit). LITERALS, so the policy holds on every path into
      # the engine — including a manual dispatch. If a deploy ever warns that the
      # uploaded artifact exceeds 1 GB, lower these and re-run it: nothing is deleted,
      # and raising them again brings the old versions back.
      max-releases: "30"
      max-prs: "20"
    permissions:
      contents: read
      actions: read
      pages: write
      id-token: write
      statuses: write

That is the whole publish setup: no job in ci.yml, no shim, nothing to thread through. A maintainer publishes a fork preview by running this workflow from the Actions tab with the pr number — which serves that fork’s HTML and JS from your Pages origin, shared with the rest of the org’s sites, so read what you are approving before you do.

4. Push your branch and open a PR

Make the changes from steps 1 and 3 in a branch and open a PR. On the PR you’ll see:

5. Merge to main — your first deploy

Merging pushes to main, which builds main; the Publish run that follows assembles a single-entry switcher.json and an index.html redirecting to main/, and deploys. Visit https://ORG.github.io/REPO/ — the redirect lands you on main/ with the switcher showing one entry. (The single-entry first deploy is graceful by design; no release required.) From here on, every push to main redeploys and every internal PR gets its own /pr-<n>/ preview — each one a separate Publish run in the Actions tab.

6. Cut your first release

Tag the merged commit on origin/main and push the tag (tagging origin/main rather than your local HEAD means you can release straight from a feature branch):

git fetch origin
git tag v1.0.0 origin/main
git push origin --tags   # or: git push origin v1.0.0 to push just this tag

The tag build runs and the release job creates the GitHub Release with that build’s docs.zip attached. This works on any repo, including ones with immutable releases enabled (it attaches the asset as the release is created, before it’s sealed).

Either way, the next deploy’s publish gathers that release, flags it preferred (★), creates the stable/ alias pointing at it, and points the root redirect at the constant stable/ URL. Your switcher now lists main and 1.0.0, and https://ORG.github.io/REPO/stable/ always resolves to the latest release — a stable URL for cross-project objects.inv references.

Where next