Preskoči na sadržaj

Publishing (Material for) MkDocs website to GitHub Pages using custom Actions workflow

As you can probably see, this website is built using the Material theme for MkDocs, which we have been happily using for over one year after using Sphinx for many years prior to that. GitHub Pages offers built-in support for Jekyll, but not for MkDocs and therefore it requires the manual building and deployment of our website. However, it automates many other things, including HTTPS certificate provisioning on our domain via Let's Encrypt.

There are several somewhat related approaches using GitHub Actions for automating the deployment of MkDocs-generated sites, usually with the Material theme, to GitHub Pages. These guides are not only found on blogs written by enthusiasts; the official Getting started section of the Material for MkDocs documentation describes the usage of GitHub Actions for deployment and provides a generic YAML file for that purpose.

Using the approaches mentioned above avoids the requirement to run the build and gh-deploy steps locally; GitHub Actions does both on GitHub's CI/CD servers, where the free plan offers 2000 minutes of GitHub-hosted runners per month. As many sites build in less than a minute, this amount allows from 50 to 100 builds and deployments per day, which is quite a bit more than most sites require. Additionally, the repository layout remains the same as it would be if the build and deployment steps were done locally; the main branch contains the site source in Markdown and the gh-pages branch contains the site files that get built for serving.

Since this summer, GitHub offers publishing Pages using a custom Actions workflow as a public beta, which was a unique feature of GitLab Pages for years. I thought that it would be interesting to see if we could use the existing GitHub Actions workflow configuration for Jekyll and simply replace the Jekyll build step with the MkDocs build step. This would streamline the usage of MkDocs with GitHub Pages, and, in particular, eliminate the requirement for publishing the site from a separate gh-pages branch, offering a Jekyll-like experience.

Let's see how far we can get. Without going into details about the syntax for GitHub Actions, here is the starter workflow configuration file for deploying a Jekyll site to GitHub Pages:

# Sample workflow for building and deploying a Jekyll site to GitHub Pages
name: Deploy Jekyll with GitHub Pages dependencies preinstalled

on:
  # Runs on pushes targeting the default branch
  push:
    branches: [$default-branch]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  # Build job
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Pages
        uses: actions/configure-pages@v5
      - name: Build with Jekyll
        uses: actions/jekyll-build-pages@v1
        with:
          source: ./
          destination: ./_site
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3

  # Deployment job
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

The highlighted lines are Jekyll-specific. We can easily replace these lines with:

In this case, since we want a drop-in replacement for Jekyll so that the remaining commands work perfectly, we will perform the MkDocs build using the mkdocs.yml configuration file in the current directory and write the built site output files into the _site directory.

The .github/workflows/mkdocs-gh-pages.yml file will look like:

# Sample workflow for building and deploying a MkDocs site to GitHub Pages
name: Deploy MkDocs with GitHub Pages dependencies preinstalled

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ["main"]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  # Build job
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Pages
        uses: actions/configure-pages@v5
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'
      - name: Install yamllint
        run: pip install yamllint
      - name: Check MkDocs YAML configuration
        run: yamllint ./mkdocs.yml
        continue-on-error: true
      - name: Check Markdown files
        uses: DavidAnson/markdownlint-cli2-action@v16
        with:
          globs: '**/*.md'
        continue-on-error: true
      - name: Install required packages
        run: pip install -r requirements.txt
      - name: Build site (_site directory name is used for Jekyll compatiblity)
        run: mkdocs build --config-file ./mkdocs.yml --strict --site-dir ./_site
        env:
          CI: true
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3

  # Deployment job
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

Two linters are used:

  • for mkdocs.yml, yamllint configuration is in the .yamllint.yaml file. It should reside in the root of the repository and contain the following:

    extends: default
    
    rules:
      document-end:
        present: false
      document-start:
        present: false
      line-length:
        level: warning
        allow-non-breakable-inline-mappings: true
    
  • for Markdown files, markdownlint configuration is in the .markdownlint.json file. It should also reside in the root of the repository and contain the following:

    {
      "default": true,
      "MD007": { "indent": 4 }
    }
    

Finally, we can see the mention of the requirements.txt file. You guessed it, it should reside in the root of the repository as well. It should contain the following text:

mkdocs-material[recommended,imaging]

And that's it! There is no more requirement for the .nojekyll file as Jekyll never gets ran in the build process. There is also no more separate gh-pages branch that the built files get pushed to, so there is also no more worry whether the site builds over time will add up to the 1 GB soft limit.

Finally, if you want to use a custom domain, having the CNAME file in the repository root or the docs subfolder will no longer have the desired effect; the domain has to be configured through the repository settings or using the API.

Updated on 2022-11-25: changed Python version from 3.10 to 3.11, resulting in faster docs builds (see Faster CPython for details).

Updated on 2022-12-03: changed caching to use github.sha instead of github.ref, enabling rebuilds of social cards when site contents change.

Updated on 2023-06-06: rebased our additions on top of the latest version of jekyll-gh-pages.yml from Starter Workflows. Changed Python version from 3.11 to the latest stable 3.x, which is 3.11 at the moment. However, using the current beta version of Python 3.12 already works well with mkdocs-material, so it's unlikely to cause issues even when 3.12 gets released and becomes the latest stable version.

Updated on 2023-09-08: simplified the workflow to use the existing requirements.txt file instead of duplicating the package names in the pip command run.

Updated on 2023-10-24: updated requirements.txt to use the extras for the installation of the optional dependencies.

Updated on 2023-12-28: bumped Actions versions by rebasing our additions on top of the latest version of jekyll-gh-pages.yml from Starter Workflows.

Updated on 2024-05-12: added yamllint and markdownlint steps. Removed caching as (Material for) MkDocs version is not pinned and therefore the site builds are not reproducible.