Skip to content

Writing modules

Introduction

Modules are libraries of macros, filters and variables, which can be used by your MkDocs project.

Every module MUST contain a define_env() function, which contains the declarations.

Location of the modules

Local module

By default, the Python code must go into one main.py file in the main website's project directory (generally beside the mkdocs.yml file).

If no main module is available, this is ignored.

If you wish, you can change the name of that module by adding a module_name entry to the mkdocs.yml file (no need to add the .py suffix):

plugins:
  ...
  - macros:
        module_name: source_code

** If you specify a module name, it must be available, or this will raise an error.**

Note

If you wish, you can implement your module as a package (subdirectory) instead of a single file.

Preinstalled modules (pluglets)

If you wish to reuse modules across several documentation projects, you may want to pre-install them, turning them into pluglets.

The define_env() function

Note

New, as of version 0.3.0

As a first step, you need declare a hook function called define_env, with one argument: env (object). This object contains the environment (variables, filters, etc.) of the templating tool (Jinja2).

This is the information that will be used to generate the pure Markdown pages, which will then be translated into HTML (and displayed in a browser).

Registration of variables, macros and filters

The example should be self-explanatory:

"""
Basic example of a Mkdocs-macros module
"""

import math

def define_env(env):
    """
    This is the hook for defining variables, macros and filters

    - variables: the dictionary that contains the environment variables
    - macro: a decorator function, to declare a macro.
    - filter: a function with one of more arguments,
        used to perform a transformation
    """

    # add to the dictionary of variables available to markdown pages:
    env.variables['baz'] = "John Doe"

    # NOTE: you may also treat env.variables as a namespace,
    #       with the dot notation:
    env.variables.baz = "John Doe"

    @env.macro
    def bar(x):
        return (2.3 * x) + 7

    # If you wish, you can  declare a macro with a different name:
    def f(x):
        return x * x
    env.macro(f, 'barbaz')

    # or to export some predefined function
    env.macro(math.floor) # will be exported as 'floor'


    # create a jinja2 filter
    @env.filter
    def reverse(x):
        "Reverse a string (and uppercase)"
        return x.upper()[::-1]
No special imports are required besides those you would need to write your functions (the env object does all the 'magic').

Tip

You can export (as variables, macros or filters) a wide range of objects, and their attributes will remain accessible to the jinja2 template via the standard Python convention, e.g. {{ foo.bar }} (see more information)

Definition of variables/macros/filters

  1. You register a variable for MkDocs-macros by adding a key/value pair to the env.variables dictionary (or namespace). Variables are loaded with each page being rendered.
  2. You register a macro by decorating a function with the expression @env.macro (or by adding it to the env.macros dictionary).
    Macros are loaded in the global namespace of the Jinja2 environment.1.
  3. You register a filter by decorating a function with the expression @env.filter (or by adding it to the env.filters dictionary).

This must be done within that define_env() function. You may, however, place any imports or other declarations outside of the function.

Priority of variables

Warning

In case of conflict, variables declared in the Python module will override those created by users in YAML files (extra). This is a safety feature, to ensure that the maintainers of that file will not accidentally break the setup defined by programmers in the module.

Conversely, keep that fact in mind, if users start complaining that an extra value has a different value than the one which they expected!

Content of the env object

Description

The env object is used for introspection, i.e. is to get information on the project or page.

Here is a list of commonly needed attributes (constants) or functions of that object:

Item Type Description
variables attribute The namespace that contains the variables and macros that will be available in markdown pages with {{ ... }} notation. This dictionary is initialized with the values contained in the extra section of the configuration file (and optionally, with external yaml files). This object is also accessible with the dot notation; e.g. env.variables['foo'] is equivalent to env.variables.foo.
macro function A decorator function that you can use to declare a Python function as a Jinja2 callable ('macro' for MkDocs).
filters attribute A list list of jinja2 filters (default None)
filter function A decorator for declaring a Python function as a jinja2 custom filter
project_dir attribute The source directory of the MkDocs project (useful for finding or including other files)
conf attribute The content of the config file (mkdocs.yaml).
config attribute This can be a useful object; it contains the global context for MkDocs]2.
page attribute The information on the page being served (such as the title, etc.). For more information on its content, see MkDoc's description of the page object.

Technical Note

env is essentially an instance of a subclass of the MkDocs' BasePlugin class, with some additional properties. Whatever you find in the BasePlugin class, you will find in the the env object.

Accessing the whole config file (mkdocs.yaml)

Sometimes, you might need information from the whole config file (mkdocs.yaml), e.g. site_description, theme, copyright, etc.

The property conf of the env object contains that information.

For example you could define such a function:

@env.macro
def site_info():
    "Return general info on the website (name, description and theme)"
    info = (env.conf['site_name'], env.conf['site_description'],
            env.conf['theme'].name)
    return "%s/%s (theme: %s)" % info

Beware the change of name

Beware that the what is usually called config is allied env.conf in the module. That is is because there is already env.config property as part of the BasePlugin class.

Indeed, you will also find the same object under env.variables.config; in other words, it will be thus be accessible as {{ config }} within the markdown pages.

Tip

In order obtain the documents directory (docs), you can use, within the Python module, the value: env.conf['docs_dir'].

Manipulating the MkDocs configuration information

env.config is the object containing the global context for mkdocs, i.e. the data structures that are being manipulated to create the final HTML web site.

You would have to explore it (using the MkDocs documentation on the global context), but it contains the navigation (env.conf['nav']), as well as all objects that could be manipulated.

Note

env.config is thus a superset of the env.conf object (which is env.config['config']).

Caution

This object is not accessible as a variable from the markdown pages. Exposing it might encourage black magic.

Validating environment variables in Python code

By design, the call to define_env() is the last stage of the config process, to create the jinja2 environment that will interpret the jinja2 directives inserted in the markdown code.

It means in particular, that you can test the variables dictionary to validate its key/values, and to take appropriate action.

For example, to check that root branches are present in the variables tree:

MINIMAL_BRANCHES = ('foo', 'bar', 'baz')
def define_env(env):
    """
    This is the hook for defining variables, macros and filters
    ...
    """

    # initial checks
    for branch in MINIMAL_BRANCHES:
        if branch not in env.variables:
            raise KeyError("Branch '%s' is not in environment variables! ")

Tip

This is a place where you could check that you code will not conflict with variables defined in the configuration files.

You may also verify other aspects of the configuration file (env.conf). Note that the attributes of the pluging->macro branch are automatically checked by mkdocs (type and default value).

List of hook functions within a module

define_env() is not the only possible hook within a module.

There are other functions available. Each is triggered by a MkDocs event.

Function Description Typical Use Triggered by MkDoc's event
define_env(env) Main function Create macros, filters, etc. on_config
on_pre_page_macros(env) Executed just before the Jinja2 directives (markdown page) have been rendered Directly modify a markdown page on_page
on_post_page_macros(env) Executed just after the Jinja2 code (markdown page) have been rendered Directly modify a markdown page on_page
on_post_build(env) Executed after the html pages have been produced Add files to the website on_post_build
declare_variables(variables, macro) Main function Deprecated (< version 0.3.0) on_config

Implementing the module as a package (module subdirectory)

Instead of implementing the Python module as a file (typically main.py), you can create a package (i.e. a main subdirectory).

The Python rules for defining a package apply: particularly the dot (.) prefix for relative imports inside the package.

The define_env() function should be accessible through the __init__.py file.

A typical directory file could look like this:

main
├── __init__.py
├── util.py
├── ...
└── ...

The __init__.py file could look like this:

from .util import foo, bar

def define_env(env):
    """
    This is the hook for defining variables, macros and filters
    """

    @env.macro
    def make_foo():
        .......
        return foo(s)

    @env.macro
    def get_bar(s):
        s = bar(s, ...)
        return s

    ...

Notes on Modules

A caution about security

Warning

It is true that you are generating static pages.

Nevertheless, think about potential side effects of macros (in case of error or abuse) or about the risks of exposing sensitive information, if the writers of markdown pages are different persons than the maintainers of the webserver.

Depending on your use case, you may want to give access to the shell (e.g. for a development team). Or else, may you want to "sandbox" your web pages (for business applications).

What you can and can't do with define_env()

The fact is that you cannot actually access page information in the define_env() function, since it operates at the configuration stage of the page building process (during the on_config() event of MkDocs). At that point, you don't have access to specific pages

Vital Note on mkdocs-macros

Of course, you can declare macros, which appear to act on pages. But realize that these are only declarations and that their execution is deferred. The macros will actually be run ** later** (by MkDocs' on_page_markdown() event), just before the markdown is rendered. The framework is so organized that, in macros, you are actually "talking" about objects that don't exist yet.

So you cannot influence the rendering process other than by adding macros, variables and filters to mkdocs_macros.

Do not modify system entities in env.variables

Also, the system information in env.variables is for reading purposes. You could modify it in your Python code, of course (at your own peril). But by design, it may have no effect on the mechanics of mkdocs (these are shallow copies).

Whatever you do in that way, is likely to be branded black magic.

Hook scripts (standard) versus MkDocs-Macros Modules

Similarities

Hook scripts (offered as standard by MkDocs) and modules provided by MkDocs-Macros are python programs that operate on the same principle : they use hooks, which are special predefined functions that will be called at specific point of the code, so that you can customize the behavior of the software according to your needs.

Those scripts take a special object as an argument:

  1. config for MkDocs hook scripts
  2. env for Mkdocs-Macros modules

Differences

Their purpose is, however, very different. To explain this in a simple way:

  1. You can think of a standard hook script as built over a barebone plugin that doesn't do anything more than MkDocs, except providing those hooks.

  2. An MkDocs-Macros module is exploiting the full power of the MkDocs-Macros plugin, which relies on its Jinja2 templating engine and the manipulation of macros, variables and filters.

A standard hook script is usually called hooks.py, while an Mkdocs-Macros module is called main.py by default, though you can change these names if you wish.


  1. From version 0.5.10. Before that, macros were inserted in env.variables

  2. env.config versus env.conf: it is unhappy that env.config represents MkDocs whole context, whereas env.conf represents only the config (YAML) file (a subset). This ambiguity was born from the fact that MkDoc itself used config to represent the context, as a property of the BasePlugin object.