mkdocs
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.
Installation⚑
- Install the basic packages.
pip install \
mkdocs \
mkdocs-material \
mkdocs-autolink-plugin \
mkdocs-minify-plugin \
pymdown-extensions \
mkdocs-git-revision-date-localized-plugin
- 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}
nav:
- Introduction: index.md
- Basic Usage: basic_usage.md
- Configuration: configuration.md
- Update: update.md
- Advanced Usage:
- Projects: projects.md
- Tags: tags.md
plugins:
- search
- autolinks
- git-revision-date-localized:
type: timeago
- minify:
minify_html: true
markdown_extensions:
- 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
theme:
name: material
custom_dir: theme
logo: images/logo.png
palette:
primary: blue grey
accent: light blue
extra_css:
- 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: 'https://github.com/lyz-code/pydo'
-
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 underdocs/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
:
theme:
palette:
# Light mode
- media: '(prefers-color-scheme: light)'
scheme: default
primary: blue grey
accent: light blue
toggle:
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
toggle:
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:
theme:
features:
- navigation.top
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
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
with:
# Number of commits to fetch. 0 indicates all history.
# Default: 1
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v1
with:
python-version: '3.7'
architecture: x64
- name: Cache dependencies
uses: actions/cache@v1
with:
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
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
publish_dir: ./docs/site
-
Create an SSH deploy key
-
Activate
GitHub Pages
repository configuration withgh-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
.
plugins:
# Not compatible with mermaid2
# - minify:
# minify_html: true
- mermaid2:
arguments:
securityLevel: loose
markdown_extensions:
- pymdownx.superfences:
# make exceptions to highlighting of code:
custom_fences:
- 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⚑
Files⚑
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.
File⚑
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
orabs_src_path
: Relative or absolute path to the original path, for example the markdown file.dest_path
orabs_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 tosrc_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
: IfFalse
, 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 inpath
. Theuse_directory_urls
argument has no effect on non-Markdown files. By default MkDocs usesTrue
.
Navigation⚑
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.
Page⚑
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.
Section⚑
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 []
.
SectionPage⚑
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 []
.
Events⚑
on_config⚑
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.
Parameters:
config
: global configuration object
Returns:
- global configuration object
on_files⚑
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.
Parameters:
files
: global files collectionconfig
: global configuration object
Returns:
- global files collection
on_nav⚑
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.
Parameters:
nav
: global navigation object.config
: global configuration object.files
: global files collection.
Returns:
- 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
orFiles
objects. - You don't need to create the
Page
,Section
,SectionPage
objects. - More robust as you rely on existent MkDocs functionality.
Testing⚑
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/mkdocs_plugin.py
. 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/conftest.py
:
import os
import shutil
from mkdocs import config
from mkdocs.config.base import Config
@pytest.fixture(name="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/test_plugin.py
:
def test_plugin_builds_newsletters(full_repo: Repo, config: Config) -> None:
build.build(config) # act
newsletter_path = f"{full_repo.working_dir}/site/newsletter/2021_02/index.html"
with open(newsletter_path, "r") as newsletter_file:
newsletter = newsletter_file.read()
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.
Prerequisites:
- An administrator has configured the server for GitLab Pages custom domains
- A GitLab Pages website up and running, served under the default Pages domain (*.gitlab.io, for GitLab.com).
- A custom domain name example.com or subdomain subdomain.example.com.
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.
Tips⚑
Center images⚑
In your config enable the attr_list
extension:
markdown_extensions:
- 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}
Issues⚑
Once they are closed:
- Mkdocs Deprecation warning, once it's solved remove the warning filter on mkdocs-newsletter
pyproject.toml
. - Mkdocs-Material Deprecation warning, once it's solved remove the warning filter on mkdocs-newsletter
pyproject.toml
.
References⚑
- Git
- Homepage.
- Material theme configuration guide
- Internal developers and plugin maintainers discussion forum