Using GitHub Pages to Host Documentation

Adrian
5 min readFeb 6, 2021

Use GitHub Pages to host docs with versioning, PR previews and more!

GitHub Pages (GHP) allows you to host static websites for GitHub projects. In this article, I’ll show you how to use GHP to host your projects documentation.

Features

  • Builds doc previews for PRs.
  • Allows versioning docs, including designating a “default” version (latest, stable, etc.).
  • Permalinks to named versions.
  • Zero-config deployment: no webservers, no load balancing, etc.
  • Docs are hosted as a branch in your repo, you have full freedom to edit the history or do any other maintenance.
  • Works with public or private repos, and can build docs for PRs from forks.
  • Any documentation tool that generates static sites will work.
  • Handles deployment race conditions (eg. two PRs building at the same time).

Samples

A simple Sphinx documentation site with versioning:

A PR with a documentation preview:

Requirements

  • A GitHub repository.
  • A docs branch (you can name it whatever you want).
  • GHP set up for this docs branch, with the path set to / . See GitHub’s docs for more details.
  • A doc tool that generates static HTML. We use Sphinx, but MkDocs and others should also work.
  • Basic familiarity with GitHub Actions.

There are plenty of guides out there to get started with GitHub Pages, or to set up your language/doc tool to generate HTML. I will not be covering these topics since I want to focus on the deployment for PRs, versioning, etc.

Why not ReadTheDocs.org?

Read The Docs (RTD) is a popular platform for hosting open source documentation, especially Sphinx documentation. While RTD can be an excellent platform, it is not without it’s issues:

  • Free version cannot host private repos / enterprise.
  • Their runners are inflexible and slow.
  • Designed around Sphinx, compatibility with other tools is flakey.

The second concern became particularly important when I wanted to host tutorials for SciKeras as Jupyter Notebooks via Jupytext. Since these notebooks train neural nets, RTD’s runners could not handle building them. This was not a problem for GitHub’s runners.

Overview

Within your docs branch, a subfolder gets created for:

  • Each PR
  • Each tag
  • Your default branch (main/master) and any other branches you want to host, for example your develop branch.

Whenever a PR, a tag or a push event to one of the designated branches occurs, GitHub Workflows kick off to build and deploy the docs. There are two Workflows:

The first Workflow is expected to build the static HTML and save it as a workflow artifact. This can be run (and edited) by forks, but to prevent forks from pushing arbitrary changes to your repo, this Workflow does not have permissions to deploy the docs. Since the only requirement for this workflow is that an artifact with the static HTML be generated, most documentation tools will work.

The second action picks up the artifact from the first action and deploys it to your docs branch (i.e. unzips it and pushes the changes). Because this action has the ability to push to your repo, it always run from the Workflow file on your default (main/master) branch. This means that PRs cannot edit and run this file; they need to be merged into your default branch first.

Implementation

For a fully implemented example, see https://github.com/adriangb/gh-pages-docs. The corresponding doc site is at https://www.adriangb.com/gh-pages-docs/.

In the root of your repository, create a .github/workflows folder if you do not already have it.

For the first workflow, create a file called build_docs.yml in .github/workflows and copy the following into it:

This workflow has two parts: build the docs and save the artifact.

There are some parts of this workflow that are required to interface with the second workflow:

  • The artifact must be named docs
  • The workflow must be named Build Docs
  • The ref for the source branch must be saved to a file called ref.txt at the root of the artifact (this is done in the Save ref step).

You can edit the Set up Python step and/or the Build the docs step to fit in with your documentation tool of choice. Remember to edit the build folder in the Save ref and Save docs artifact steps if it is different than docs/_build .

To build docs for additional branches (the defaults are main and develop ), you can edit the push -> branches section. You will be able to rename these branches later (e.g. to make main show up as latest in the docs).

For the second workflow, create a file called deploy_docs.yml in .github/workflows and copy this workflow into it.

You do not have to do any editing/configuring for this workflow, it should work out of the box.

Finally, in the root of your docs branch, create a file called update_versions.py . This is not required: it is only needed if you want to manage versions and/or dynamically change the default docs when new tags are published. If you don’t want any of this functionality, simply skip this step and delete the Update versions step in deploy_docs.yml . Here is a basic implementation of update_versions.py

You can configure which branches you want to show up as versions, and what names you want them to have by editing the branch_names variable.

To configure permalinks, edit the `permalink` array. These names must match with the values in branch_names , except for stable which always points to the highest tagged version number. For example, by using permalinks = ["stable"] we can make https://<user>.github.io/repo/stable/ point to the latest tagged version. So when you release v0.1.1 all of your links aren’t stuck pointing to v0.1.0 . This is handled by creating a symlink in your repo like /stable -> /refs/tags/v0.1.1 , which GitHub Pages will then follow when building your site.

Versions are saved as a file called versions.json at the root of your docs branch. In the example repo, we add some JQuery to our Sphinx theme to pull this file and display a version dropdown based on it, but you can use this file however you like.

Finally, you need to add a file called _config.yml to the root of your docs branch. This allows you to include/exclude files served. For our example, we include files starting with _ (they are excluded by default, but Sphinx uses them) and exclude any .py files.

That’s it! Have fun documenting.

--

--