Skip to content

MkDocs

Like GitBook, MkDocs is a fast and simple static site generator with template, plugin and extension support. Documentation source files are written also in Markdown, and configured with a single YAML configuration file. MkDocs brings modern and customizable style, lots of possible extensions with powerful markdown interpretation.

This site use the material theme but others are possible, too. Material theme has responsive design and fluid layout for all kinds of screens and devices, designed to serve your project documentation in a user-friendly way in 34 languages with optimal readability. Some basic customization like primary and accent color, fonts... could be configured.

Also a collection of useful extensions are included here, too. So this is not only a description of the basics but presenting you a fully usable and optimal setup of it.

Docker

You can use a ready and complete docker image to create mkdocs pages. This gives you an easy solution without thinking about all the installation details with the extensions and plugins.

CLI call

After installing docker you can directly run it:

# run with mkdocs in the current directory
docker run -v $(pwd):/data alinex/mkdocs
# run with documentation in specific directory
docker run -v /my-project:/data alinex/mkdocs

The given directory has to contain the mkdocs.yml configuration file.

Create in GitLab CI

Use the following CI script to run the document creation within GitLab CI and deploy to GitLab pages:

  • create documentation
  • deploy to pages
image: alinex/mkdocs

pages:
  stage: deploy
  script:
    - "/run.sh"
    - rm -rf public
    - mv site public
  artifacts:
    paths:
      - public

Run as NPM Script

To allow easy call within NodeJS projects add the following configuration:

    "scripts": {
        ...
        "docs": "docker run -v $(pwd):/data alinex/mkdocs && xdg-open file:///$(pwd)/site/index.html"
    }

Afterwards you can call it with npm run docs and it will create them and open it in your default browser.

Check Locally

To check your result locally you simply open the file site/index.html in the browser.

Install

Info

As an easy alternative to install mkdocs, use it in a ready to run docker container with all of the functionality of this page build together, including PDF creation.

You only need to read this chapter if you don't use docker.

Python 3 should already be installed in new releases, so nothing to do.

On Debian the following steps should be enough to get it locally running:

sudo apt install build-essential python3-dev python3-pip python3-setuptools python3-wheel python3-cffi libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info

Take care that you use python 3!

$ ll -al $(which python)
lrwxrwxrwx 1 root root 18 Mär  8 10:03 /usr/bin/python -> /usr/bin/python3.6*

If this points to python 2.7 you should change that on problems first.

Now you can install the python packages:

python -m pip install --upgrade pip
python -m pip install mkdocs
python -m pip install mkdocs-material
python -m pip install pymdown-extensions
python -m pip install markdown-blockdiag
python -m pip install markdown-include
python -m pip install mkdocs-with-pdf
python -m pip install django-weasyprint
python -m pip install mkdocs-awesome-pages-plugin
python -m pip install mkdocs-minify-plugin
python -m pip install mkdocs-git-revision-date-localized-plugin
python -m pip install mkdocs-include-markdown-plugin

You should be able to directly call mkdocs on the console, now.

To make it accessible in path, add the following to ~/.bashrc:

PATH=$PATH:~/.local/bin

And for the epub conversion you need to have calibre installed as package or using:

curl -sL https://download.calibre-ebook.com/linux-installer.sh | sudo -E bash -

Bug

The epub output is not really useable at the moment.

Problems

mkdocs could not be installed

If the above won't install mkdocs try to install some tools first:

sudo apt-get install python-setuptools
python -m pip install wheel

After that retry to install mkdocs and it's extensions.

Problem with cairocffi

Maybe your cairocffi version is not matching and you get some errors like Requirement.parse('cairocffi>=0.9.0'), {'weasyprint'}), then you can check your version like:

$ python -m pip show cairocffi
Name: cairocffi
Version: 0.9.0
...

To install a specific version use:

python -m pip uninstall  cairocffi
python -m pip install cairocffi==1.0.1

Update

To later update your installation only call the following:

python -m pip install --upgrade pip
python -m pip install --upgrade mkdocs
python -m pip install --upgrade mkdocs-material
python -m pip install --upgrade pymdown-extensions
python -m pip install --upgrade markdown-blockdiag
python -m pip install --upgrade markdown-include
python -m pip install --upgrade mkdocs-with-pdf
python -m pip install --upgrade django-weasyprint
python -m pip install --upgrade mkdocs-awesome-pages-plugin
python -m pip install --upgrade mkdocs-minify-plugin
python -m pip install --upgrade mkdocs-git-revision-date-localized-plugin
python -m pip install --upgrade mkdocs-mermaid2-plugin
python -m pip install --upgrade mkdocs-include-markdown-plugin

Preview Server

While you are working on the documentation and create new stuff it is often necessary to immediately see how it looks like. This is possible if you start an development server of mkdocs using:

mkdocs serve # from within the project home

This will start an development server which automatically reloads on changes.

Build Documentation

To create the documentation in the site sub folder use:

mkdocs build

Configuration

The setup is completely done in a mkdocs.yml file within your project's root directory. All in all you can and have to specify a lot, but this section will guide you.

First some descriptive information for the site:

site_name: Alinex Development Guide
site_description: A book to learn modern web technologies.
site_author: Alexander Schilling
copyright: Copyright &copy; 2016 - 2022 <a href="https://alinex.de">Alexander Schilling</a>

While the site_name is used as heading the site_description and site_author goes into the meta data. And the copyright line will be displayed in the footer with optional HTML links as seen above.

The navigation may be

  • auto detected
  • defined using the nav section
  • defined by .pages files

A navigation section in the mkdocs.yml will look like:

nav:
    - Home:
          - README.md
          - alinex.md
    - Languages:
          - Overview: lang/README.md
          - Markdown: lang/markdown.m
          - Handlebars: lang/handlebars.md
          - ... | lang/*.md

Chapters can not contain a direct page. A title can be given for each page. If not the title setting at the top of each page is used or the first heading.

And you can also add all not individually added pages anywhere with ... as entry or using a glob pattern like ... | lang/*.md, ... | flat | lang/*.md or regexp patterns ... | regex=page-[0-9]+.md.

Through the Awesome Pages Plugin Another alternative is to use .pages files in each documentation directory which specifies this part:

  • use a nav section with the markdown files and subdirectories (from this folder)
  • add ... there to add the undeclared ones
  • add sort: asc to sort automatically added entries
  • add collapse: true to not make a folder entry while there is only one element
  • add hide: true to exclude this directory
  • add title: Section Title to give this section a specific title

Theme

Now the theme definition, here we use the material theme as a basis:

use_directory_urls: false

theme:
    name: material
    icon:
        logo: material/book-open-variant
    favicon: assets/favicon.ico
    language: en
    palette:
        scheme: slate
        primary: grey
        accent: dark orange
    font:
        text: Lato
        code: Roboto Mono
    features:
        - navigation.instant
        - content.code.annotate

The first line with use_directory_urls: false makes the site also browsable locally.

The logo can be a name from the material icons (displayed on the top left beside the page heading). The favicon has to be set to an image within the docs folder. If feature/tabs is set the first level of navigation is put at tabs on the top.

repo_name: "alinex/alinex.gitlab.io"
repo_url: "https://gitlab.com/alinex/alinex.gitlab.io"
edit_uri: "" (1)
  1. This line prevents the edit icon, remove this line or set a correct url to allow editing.

header

Like shown in the image the repository will be displayed on the right and if no edit_uri: "...." is given or set an icon to edit the page source is added, too. To prevent this in the example config edit_uri is set to an empty string.

extra:
    social:
        - icon: material/gitlab
          link: https://gitlab.com/alinex
        - icon: material/github
          link: https://github.com/alinex
        - icon: material/home
          link: https://alinex.de

footer

The social links use the FontAwesome names as type with a link. They will be displayed at the bottom right corner of the page.

extra_css:
    - assets/extra.css

With the extra_css section you may add more stylesheets to the generated HTML which are used to:

Also you should at least add the folowing two javascript files:

extra_javascript:
    - assets/extra.js

The two files will look like:

@import url("https://fonts.googleapis.com/css2?family=Oswald&display=swap");

/* use image for color theme */
.md-footer,
.md-header,
.md-tabs,
body {
    /*    background-image: url('blue-tunnel.jpg');*/
    background-attachment: fixed;
    background-image: url("default.jpg");
    background-size: cover;
}
.md-container,
.md-search__inner {
    background-color: rgba(0, 0, 0, 0.9);
}
.md-footer-meta,
.md-footer-nav {
    background: transparent;
}

/* General style */
.md-typeset h1,
.md-tabs,
.md-header-nav__topic,
.md-sidebar {
    font-family: "Oswald", sans-serif;
}
.md-tabs a {
    font-size: 0.9rem;
}
.md-sidebar label {
    font-size: 0.85rem;
}
.md-sidebar a {
    font-size: 0.75rem;
    margin-top: 0.4em;
}
.md-typeset h1 {
    color: white;
}
.md-typeset__table th {
    border-bottom: 2px solid darkorange;
    font-weight: bold;
}
.md-typeset__table tr:nth-child(even) {
    background: rgb(61, 61, 76);
}
.md-typeset table:not([class]) tr:nth-child(even):hover {
    background-color: rgba(61, 61, 76, 0.035);
}

/* change link colors for grey theme */
.md-header-nav__button:hover {
    color: darkorange;
    opacity: 1;
    transition: color 0.5s;
}
[data-md-color-primary="grey"] .md-typeset a {
    color: #00ade2;
}
[data-md-color-primary="grey"] .md-typeset a:hover {
    color: darkorange;
}
.md-nav__item .md-nav__link--active {
    color: white;
}
.md-sidebar label,
.md-nav__item--nested > .md-nav__link,
.md-sidebar a {
    color: gray;
}
.md-nav__link:hover {
    color: darkorange;
}
.md-nav__link[data-md-state="blur"] {
    color: rgb(180, 180, 180);
}
.md-nav__item .md-nav__link--active {
    color: white;
}
.md-footer-nav__link {
    font-weight: bold;
}
.md-footer-nav__link:hover {
    color: darkorange;
    opacity: 1;
    transition: color 0.5s;
}
a.headerlink {
    color: gray;
}

hr {
    border-color: white;
}

/* display external links with icon */
div.md-content a[href^="http://"]:not([href*="alinex.gitlab.io"]):after,
div.md-content a[href^="https://"]:not([href*="alinex.gitlab.io"]):after,
div.md-content a[href^="//"]:not([href*="alinex.gitlab.io"])
{
    content: "↗";
    display: inline-block;
    font-size: 70%;
    font-style: normal;
    font-weight: normal;
    text-decoration: none;
    vertical-align: top;
}

/* attribute classes to be used via {: .class} */
.left {
    /* left align with text float on the right */
    float: left;
    padding-right: 20px;
}
.right {
    /* right align with text float on the left */
    float: right;
    padding-left: 20px;
}
.icon {
    /* change image size */
    width: 25%;
}
.border {
    /* add drop shadow */
    -moz-box-shadow: 0 0 18px 5px rgba(250, 249, 249, 0.5);
    -webkit-box-shadow: 0 0 18px 5px rgba(250, 249, 249, 0.5);
    border: 1px solid #7f8081;
    box-shadow: 0 0 18px 5px rgba(250, 249, 249, 0.5);
}

/* special use tags ==...== */
mark,
.md-typeset mark {
    background-color: rgb(255, 253, 130);
    color: black;
    font-weight: bold;
}

/* blockdiag */
img[src*="%3Ctitle%3Eblockdiag%3C"] {
    background-color: lightgray;
}

/* image zoom */
img {
    height: auto;
    width: auto;
}
.zoom,
.zoom2 {
    cursor: zoom-in;
    transition: transform ease-in-out 0.5s;
}
.image-zoom {
    background: #000;
    box-shadow: 0 4px 8px 0 rgba(238, 238, 238, 0.5),
        0 6px 20px 0 rgba(238, 238, 238, 0.5);
    cursor: zoom-out;
    position: absolute;+
    transform: scale(1);
    z-index: 100;
}
.image-zoom2 {
    background: #000;
    cursor: zoom-out;
    box-shadow: 0 4px 8px 0 rgba(238, 238, 238, 0.5),
        0 6px 20px 0 rgba(238, 238, 238, 0.5);
    position: absolute;
    transform: scale(1.5);
    z-index: 100;
}

/* progress bar */
.progress-label {
    font-weight: 700;
    line-height: 1.2rem;
    margin: 0;
    overflow: hidden;
    position: absolute;
    text-align: center;
    text-shadow: 0 0 6px #000000;
    width: 100%;
    white-space: nowrap;
}
.progress-bar {
    background-color: #2979ff;
    float: left;
    height: 1.2rem;
}
.progress {
    background-color: #cccccc8a;
    display: block;
    height: 1.2rem;
    margin: 0.5rem 0;
    position: relative;
    width: 100%;
}
.progress.thin {
    height: 0.4rem;
    margin-top: 0.9rem;
}
.progress.thin .progress-label {
    margin-top: -0.4rem;
}
.progress.thin .progress-bar {
    height: 0.4rem;
}
.progress-100plus .progress-bar {
    background-color: #00e676;
}
.progress-80plus .progress-bar {
    background-color: #fbc02d;
}
.progress-60plus .progress-bar {
    background-color: #ff9100;
}
.progress-40plus .progress-bar {
    background-color: #ff5252;
}
.progress-20plus .progress-bar {
    background-color: #ff1744;
}
.progress-0plus .progress-bar {
    background-color: #f50057;
}
/* Synchronized Tabs */
const tabs = document.querySelectorAll('.tabbed-set > input')
for (const tab of tabs) {
  tab.addEventListener('click', () => {
    const current = document.querySelector(`label[for=${tab.id}]`)
    const pos = current.getBoundingClientRect().top
    const labelContent = current.innerHTML
    const labels = document.querySelectorAll('.tabbed-set > label, .tabbed-alternate > .tabbed-labels > label')
    for (const label of labels) {
      if (label.innerHTML === labelContent) {
        document.querySelector(`input[id=${label.getAttribute('for')}]`).checked = true
      }
    }
    // Preserve scroll position
    const delta = (current.getBoundingClientRect().top) - pos
    window.scrollBy(0, delta)
  })
}

/* make tables sortable */
document$.subscribe(function() {
  var tables = document.querySelectorAll('article table:not([class])')
  tables.forEach(function(table) {
    new Tablesort(table)
  })
})

/* MATHJax */
window.MathJax = {
  tex: {
    inlineMath: [['\\(', '\\)']],
    displayMath: [['\\[', '\\]']],
    processEscapes: true,
    processEnvironments: true
  },
  options: {
    ignoreHtmlClass: '.*',
    processHtmlClass: 'arithmatex'
  }
};

/* ZOOM */
document.querySelectorAll('.zoom').forEach(item => {
    item.addEventListener('click', function () {
        this.classList.toggle('image-zoom');
    })
});
document.querySelectorAll('.zoom2').forEach(item => {
    item.addEventListener('click', function () {
        this.classList.toggle('image-zoom2');
    })
});

This also contains a part needed for Math (see below).

Plugins and Extensions

And at last some plugins and extensions for more markdown possibilities like described below:

plugins:
    - search
    - awesome-pages
    - include-markdown
    - minify:
          minify_html: true
          htmlmin_opts:
              remove_comments: true

markdown_extensions:
    - extra
    - toc:
          permalink: true
    - pymdownx.caret
    - pymdownx.tilde
    - pymdownx.mark
    - admonition
    - pymdownx.details
    - pymdownx.highlight
    - pymdownx.inlinehilite
    - pymdownx.snippets
    - pymdownx.superfences
    - pymdownx.tabbed:
          alternate_style: true
    - pymdownx.betterem:
          smart_enable: all
    - pymdownx.emoji:
          emoji_index: !!python/name:materialx.emoji.twemoji
          emoji_generator: !!python/name:materialx.emoji.to_svg
    - pymdownx.keys
    - pymdownx.smartsymbols
    - pymdownx.tasklist:
          custom_checkbox: true
    - markdown_blockdiag:
          format: svg
    - markdown_include.include
    - pymdownx.arithmatex:
          generic: true
    - pymdownx.progressbar

If you use the pymdownx.keys extension with the ++ syntax you can find all possible names. But if something is missing you can add it in the setup:

markdown_extensions:
    - pymdownx.keys:
          key_map:
              {
                  "circumflex": "^",
                  "dollar": "$",
                  "percent": "%",
                  "parenthesis-left": "(",
                  "parenthesis-right": ")",
              }

If VS Code with the Prettier plugin is used, set the tab width to 4 spaces for correct Markdown formatting in MkDocs.

{
    "MD007": { "indent": 4 },
    "MD013": false,
    "MD030": false,
    "MD036": false,
    "MD041": false,
    "MD046": false
}

See the complete setup of this book.

Add Tablesort

You need the following additions to make all tables sortable by clicking on the headers:

extra_javascript:
  - https://cdnjs.cloudflare.com/ajax/libs/tablesort/5.2.1/tablesort.min.js
/* make tables sortable */
document$.subscribe(function() {
  var tables = document.querySelectorAll("article table:not([class])")
  tables.forEach(function(table) {
    new Tablesort(table)
  })
})

Allow Math

To enable math formulas, the following has to be added in mkdocs.yml:

extra_javascript:
    - https://polyfill.io/v3/polyfill.min.js?features=es6
    - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js

plugins:
    - pymdownx.arithmatex:
          generic: true

The extra.js file has to contain the mathjay section:

/* Synchronized Tabs */
const tabs = document.querySelectorAll('.tabbed-set > input')
for (const tab of tabs) {
  tab.addEventListener('click', () => {
    const current = document.querySelector(`label[for=${tab.id}]`)
    const pos = current.getBoundingClientRect().top
    const labelContent = current.innerHTML
    const labels = document.querySelectorAll('.tabbed-set > label, .tabbed-alternate > .tabbed-labels > label')
    for (const label of labels) {
      if (label.innerHTML === labelContent) {
        document.querySelector(`input[id=${label.getAttribute('for')}]`).checked = true
      }
    }
    // Preserve scroll position
    const delta = (current.getBoundingClientRect().top) - pos
    window.scrollBy(0, delta)
  })
}

/* make tables sortable */
document$.subscribe(function() {
  var tables = document.querySelectorAll('article table:not([class])')
  tables.forEach(function(table) {
    new Tablesort(table)
  })
})

/* MATHJax */
window.MathJax = {
  tex: {
    inlineMath: [['\\(', '\\)']],
    displayMath: [['\\[', '\\]']],
    processEscapes: true,
    processEnvironments: true
  },
  options: {
    ignoreHtmlClass: '.*',
    processHtmlClass: 'arithmatex'
  }
};

/* ZOOM */
document.querySelectorAll('.zoom').forEach(item => {
    item.addEventListener('click', function () {
        this.classList.toggle('image-zoom');
    })
});
document.querySelectorAll('.zoom2').forEach(item => {
    item.addEventListener('click', function () {
        this.classList.toggle('image-zoom2');
    })
});

Last Update Date

It is possible to automatically add the last update date below the page content. Therefore add the plugin in mkdocs.yml:

pluǵins:
    - git-revision-date-localized

If you use a build environment you have to setup it to don't only fetch the last commit:

  • GitLab runners: set GIT_DEPTH to 0 - howto
  • GitHub actions: set fetch_depth to 0 - howto
  • Bitbucket pipelines: set clone: depth: full - howto

PDF and EPub

To create a PDF the following additions in mkdocs.yml has to be made:

plugins:
    - with-pdf:
          cover_subtitle: Framework running powerful deep tests for standalone use or to enhance monitoring
          cover_logo: https://assets.gitlab-static.net/uploads/-/system/project/avatar/12586261/images__1_.png
          output_path: alinex-checkup.pdf

This will build the PDF, you can set more settings, see mkdocs-with-pdf.

An epub can be created using calibre, but the output is very ugly, so I won't do this at the moment.

ebook-convert site/$NAME.pdf site/$NAME.epub

In the setup below the documentation will be stored under site/alinex-book.pdf.

Writing Documentation

Pages are written in markdown Format and stored as *.md files within the doc folder. The Markdown implementation is nearly the same as used on GitHub but with some additions. See mkdocs markdown for a detailed writing guild with examples.

Further reading


Last update: January 1, 2022