Skip to content


MkDocs is a fast, simple and downright gorgeous static site generator that's geared towards building project documentation. Documentation source files are written in Markdown, and configured with a single YAML configuration file.

Note: I've automated the creation of the mkdocs site in this cookiecutter template.


  • Install the basic packages.
pip install \
    mkdocs \
    mkdocs-material \
    mkdocs-autolink-plugin \
    mkdocs-minify-plugin \
    pymdown-extensions \
  • Create the docs repository.
mkdocs new docs
  • Although there are several themes, I usually use the material one. I won't dive into the different options, just show a working template of the mkdocs.yaml file.
site_name: {{site_name: null}: null}
site_author: {{your_name: null}: null}
site_url: {{site_url: null}: null}
  - Introduction:
  - Basic Usage:
  - Configuration:
  - Update:
  - Advanced Usage:
      - Projects:
      - Tags:

  - search
  - autolinks
  - git-revision-date-localized:
      type: timeago
  - minify:
      minify_html: true

  - admonition
  - meta
  - toc:
      permalink: true
      baselevel: 2
  - pymdownx.arithmatex
  - pymdownx.betterem:
      smart_enable: all
  - pymdownx.caret
  - pymdownx.critic
  - pymdownx.details
  - pymdownx.emoji:
      emoji_generator: !%21python/name:pymdownx.emoji.to_svg
  - pymdownx.inlinehilite
  - pymdownx.magiclink
  - pymdownx.mark
  - pymdownx.smartsymbols
  - pymdownx.superfences
  - pymdownx.tasklist:
      custom_checkbox: true
  - pymdownx.tilde

  name: material
  custom_dir: theme
  logo: images/logo.png
    primary: blue grey
    accent: light blue

  - stylesheets/extra.css
  - stylesheets/links.css

repo_name: {{repository_name: null}: null} # for example: 'lyz-code/pydo'
repo_url: {{repository_url: null}: null} # for example: ''
  • Configure your logo by saving it into docs/images/logo.png.

  • I like to show a small image above each link so you know where is it pointing to. To do so add the content of this directory to theme. and these files under docs/stylesheets.

  • Initialize the git repository and create the first commit.

  • Start the server to see everything is alright.

mkdocs serve

Material theme customizations

Color palette toggle

Since 7.1.0, you can have a light-dark mode on the site using a toggle in the upper bar.

To enable it add to your mkdocs.yml:


    # Light mode
    - media: '(prefers-color-scheme: light)'
      scheme: default
      primary: blue grey
      accent: light blue
        icon: material/toggle-switch-off-outline
        name: Switch to dark mode

    # Dark mode
    - media: '(prefers-color-scheme: dark)'
      scheme: slate
      primary: blue grey
      accent: light blue
        icon: material/toggle-switch
        name: Switch to light mode

Changing your desired colors for each mode

Back to top button

Since 7.1.0, a back-to-top button can be shown when the user, after scrolling down, starts to scroll up again. It's rendered in the lower right corner of the viewport. Add the following lines to mkdocs.yml:


Add a github pages hook.

  • Save your requirements.txt.
pip freeze > requirements.txt
  • Create the .github/workflows/gh-pages.yml file with the following contents.
name: Github pages

      - master

    runs-on: ubuntu-18.04
      - uses: actions/checkout@v2
          # Number of commits to fetch. 0 indicates all history.
          # Default: 1
          fetch-depth: 0

      - name: Setup Python
        uses: actions/setup-python@v1
          python-version: '3.7'
          architecture: x64

      - name: Cache dependencies
        uses: actions/cache@v1
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-

      - name: Install dependencies
        run: |
          python3 -m pip install --upgrade pip
          python3 -m pip install -r ./requirements.txt

      - run: |
          cd docs
          mkdocs build

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
          deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
          publish_dir: ./docs/site
  • Create an SSH deploy key

  • Activate GitHub Pages repository configuration with gh-pages branch.

  • Make a new commit and push to check it's working.

Create MermaidJS diagrams

Even though the Material theme supports mermaid diagrams it's only giving it for the paid users. The funding needs to reach 5000$ so it's released to the general public.

The alternative is to use the mkdocs-mermaid2-plugin plugin, which can't be used with mkdocs-minify-plugin and doesn't adapt to dark mode.

To install it:

  • Download the package: pip install mkdocs-mermaid2-plugin.

  • Enable the plugin in mkdocs.yml.

  # Not compatible with mermaid2
  # - minify:
  #    minify_html: true
  - mermaid2:
        securityLevel: loose
  - pymdownx.superfences:
      # make exceptions to highlighting of code:
        - name: mermaid
          class: mermaid
          format: !%21python/name:mermaid2.fence_mermaid

Check the MermaidJS article to see how to create the diagrams.

Plugin development

Like MkDocs, plugins must be written in Python. It is expected that each plugin would be distributed as a separate Python module. At a minimum, a MkDocs Plugin must consist of a BasePlugin subclass and an entry point which points to it.

The BasePlugin class is meant to have on_<event_name> methods that run actions on the MkDocs defined events.

The same object is called at the different events, so you can save objects from one event to the other in the object attributes.

Keep in mind that the order of execution of the plugins follows the ordering of the list of the mkdocs.yml file where they are defined.

Interesting objects


mkdocs.structure.files.Files contains a list of File objects under the ._files attribute and allows you to append files to the collection. As well as extracting the different file types:

  • documentation_pages: Iterable of markdown page file objects.
  • static_pages: Iterable of static page file objects.
  • media_files: Iterable of all files that are not documentation or static pages.
  • javascript_files: Iterable of javascript files.
  • css_files: Iterable of css files.

It is initialized with a list of File objects.


mkdocs.structure.files.File objects points to the source and destination locations of a file. It has the following interesting attributes:

  • name: Name of the file without the extension.
  • src_path or abs_src_path: Relative or absolute path to the original path, for example the markdown file.
  • dest_path or abs_dest_path: Relative or absolute path to the destination path, for example the html file generated from the markdown one.
  • url: Url where the file is going to be exposed.

It is initialized with the arguments:

  • path: Must be a path that exists relative to src_dir.
  • src_dir: Absolute path on the local file system to the directory where the docs are.
  • dest_dir: Absolute path on the local file system to the directory where the site is going to be built.
  • use_directory_urls: If False, a Markdown file is mapped to an HTML file of the same name (the file extension is changed to .html). If True, a Markdown file is mapped to an HTML index file (index.html) nested in a directory using the "name" of the file in path. The use_directory_urls argument has no effect on non-Markdown files. By default MkDocs uses True.

mkdocs.structure.nav.Navigation objects hold the information to build the navigation of the site. It has the following interesting attributes:

  • items: Nested List with full navigation of Sections, SectionPages, Pages, and Links.
  • pages: Flat List of subset of Pages in nav, in order.

The Navigation object has no __eq__ method, so when testing, instead of trying to build a similar Navigation object and compare them, you need to assert that the contents of the object are what you expect.


mkdocs.structure.pages.Page models each page of the site.

To initialize it you need the title, the File object of the page, and the MkDocs config object.


mkdocs.structure.nav.Section object models a section of the navigation of a MkDocs site.

To initialize it you need the title of the section and the children which are the elements that belong to the section. If you don't yet know the children, pass an empty list [].


mkdocs_section_index.SectionPage , part of the mkdocs-section-index plugin, models Section objects that have an associated Page, allowing you to have nav sections that when clicked, load the Page and not only opens the menu for the children elements.

To initialize it you need the title of the section, the File object of the page, , the MkDocs config object, and the children which are the elements that belong to the section. If you don't yet know the children, pass an empty list [].



The config event is the first event called on build and is run immediately after the user configuration is loaded and validated. Any alterations to the config should be made here.


  • config: global configuration object


  • global configuration object


The files event is called after the files collection is populated from the docs_dir. Use this event to add, remove, or alter files in the collection. Note that Page objects have not yet been associated with the file objects in the collection. Use Page Events to manipulate page specific data.




The nav event is called after the site navigation is created and can be used to alter the site navigation.

Warning: Read the following section if you want to add new files.



  • global navigation object

Adding new files

Note: "TL;DR: Add them in the on_config event."

To add new files to the repository you will need two phases:

  • Create the markdown article pages.
  • Add them to the navigation.

My first idea as a MkDocs user, and newborn plugin developer was to add the navigation items to the nav key in the config object, as it's more easy to add items to a dictionary I'm used to work with than to dive into the code and understand how MkDocs creates the navigation. As I understood from the docs, the files should be created in the on_files event. the problem with this approach is that the only event that allows you to change the config is the on_config event, which is before the on_files one, so you can't build the navigation this way after you've created the files.

Next idea was to add the items in the on_nav event, that means creating yourself the Section, Pages, SectionPages or Link objects and append them to the nav.items. The problem is that MkDocs initializes and processes the Navigation object in the get_navigation function. If you want to add items with a plugin in the on_nav event, you need to manually run all the post processing functions such as building the pages attribute, by running the _get_by_type, _add_previous_and_next_links or _add_parent_links yourself. Additionally, when building the site you'll get the The following pages exist in the docs directory, but are not included in the "nav" configuration error, because that check is done before all plugins change the navigation in the on_nav object.

The last approach is to build the files and tweak the navigation in the on_config event. This approach has the next advantages:

  • You need less knowledge of how MkDocs works.
  • You don't need to create the File or Files objects.
  • You don't need to create the Page, Section, SectionPage objects.
  • More robust as you rely on existent MkDocs functionality.


I haven't found any official documentation on how to test MkDocs plugins, in the issues they suggest you look at how they test it in the search plugin. I've looked at other plugins such as mkdocs_blog and used the next way to test mkdocs-newsletter.

I see the plugin definition as an entrypoint to the functionality of our program, that's why I feel the definition should be in src/mkdocs_newsletter/entrypoints/ As any entrypoint, the best way to test them are in end-to-end tests.

You need to have a working test site in tests/assets/test_data, with it's mkdocs.yml file that loads your plugin and some fake articles.

To prepare the test we can define the next fixture that prepares the building of the site:

File: tests/

import os
import shutil

from mkdocs import config
from mkdocs.config.base import Config

def config_(tmp_path: Path) -> Config:
    """Load the mkdocs configuration."""
    repo_path = tmp_path / "test_data"
    shutil.copytree("tests/assets/test_data", repo_path)
    mkdocs_config = config.load_config(os.path.join(repo_path, "mkdocs.yml"))
    mkdocs_config["site_dir"] = os.path.join(repo_path, "site")
    return mkdocs_config

It does the next steps:

  • Copy the fake MkDocs site to a temporal directory
  • Prepare the MkDocs Config object to build the site.

Now we can use it in the e2e tests:

File: tests/e2e/

def test_plugin_builds_newsletters(full_repo: Repo, config: Config) -> None:  # act

    newsletter_path = f"{full_repo.working_dir}/site/newsletter/2021_02/index.html"
    with open(newsletter_path, "r") as newsletter_file:
        newsletter =
    assert "<title>February of 2021 - The Blue Book</title>" in newsletter

That test is meant to ensure that our plugin works with the MkDocs ecosystem, so the assertions should be done against the created html files.

If your functionality can't be covered by the happy path of the end-to-end test, it's better to create unit tests to make sure that they work as you want.

You can see a full example here.

Use your custom domain

Use your custom domain in Gitlab

To set up Pages with a custom domain name, read the requirements and steps below.


  • An administrator has configured the server for GitLab Pages custom domains
  • A GitLab Pages website up and running, served under the default Pages domain (*, for
  • A custom domain name or subdomain

Access to your domain’s server control panel to set up DNS records:

  • A DNS record (A, ALIAS, or CNAME) pointing your domain to the GitLab Pages server. If there are multiple DNS records on that name, you must use an ALIAS record.
  • A DNS TXT record to verify your domain’s ownership.

Use your custom domain in github pages

You can't define a subdomain for a repo static page. You define a subdomain for your user github page and then the repo is added afterwards with a domain/repo structure.

Redirect Github Pages to custom domain

There is no "beautiful non-plugin solution". Indeed, the solution I found is not beautiful, it is more kind of a workaround, but it works.

  • Set up CNAME redirection
  • Go to your GitHub Pages repo. Click Settings and below GitHub Pages, under Custom domain, enter your custom domain and click Save.

  • Now open another tab on your browser, open DevTools (F12), select the Network tab. Try to access your GitHub Pages website and see that a 301 redirect happens.

  • GitHub is going to complain that your domain is not configured properly. Well, that does not really matter. What matters is that you have the 301 redirect required by the Google Search Console's Change of Address Tool and your website won't lose its Google ranking.


Center images

In your config enable the attr_list extension:

  - attr_list

On your extra.css file add the center class

.center {
    display: block;
    margin: 0 auto;

Now you can center elements by appending the attribute:

![image](../_imatges/ebc_form_01.jpg){: .center}


Once they are closed:


Plugin development