Skip to content

Post-production of pages


Within mkdocs-macros, we call post-production all actions that are defined outside of ("after") the code in the define_env(env) hook.

In practice most of those actions occur after the execution of the Jinja2 engine that converts variables and macros into raw Markdown. Hence the term "post-production", by analogy to photography or video editing.


It is now also possible to modify the markdown page just before the rendering of the macros. Hence "post-production" is somewhat a misnomer, but we are now stuck with that term.

They are of three types of post-production:

  1. Modifying markdown pages (either before or after macro rendering)
  2. Influencing the HTML pages generated by mkdocs
  3. Performing operations after mkdocs has completely executed

Modifying Markdown pages


From version 0.5.2

From version 1.0.0

The env.raw_markdown attribute is deprecated. Use env.markdown instead.

There are specific cases where you want your module code to be able to modify the markdown code of a page, without using the mechanism of variables and macros. One typical example is when you wish to alter the pages generated, e.g. by adding footer information at the bottom of each page, or header information at the top of each page.

The proper time to do so, would be before or after the macros (Jinja2 directives) have been processed.

Technical note: a limitation of the macros mechanism

The define_env() function operates at the time when MkDocs prepares the configuration of website (the on_config() event). This is a global event, i.e. any change made at this point will affect the whole website.

The limitation is that the define_env() function is "aware" of the general configuration, not of the content of single pages.

True, it allows you to declare macros which will be interpreted later, for each page (on the on_page() event). But it won't allow you to modify pages outside of that mechanism.

To act on such cases that vary with each markdown page (and depend on each page, not on the general configuration), you may use the two hooks in your module, which are executed before the markdown page is rendered into HTML:

  1. on_pre_page_macros(env) : just before the macros are rendered on a specific page (macros are still present). At this stage you can still insert text that contains macros.
  2. on_post_page_macros(env) : just after the macros are rendered on a specific page (macros have turned into their markdown equivalent). At this stage you can insert Markdown text or HTML, but no longer macros.

You can access the markdown through the env.markdown property (string).

Application to headers or footers

From version 1.0.0

on_pre_page_macros(env) works as shown only from that version.

For lower versions, in on_post_page_macros(env) replace env.raw_markdown with env.markdown

Headers or footers can be added before or after the rendering process. If you add, e.g. a footer before the rendering process, the macros inserted will be interpreted.

You may a add a variable footer after the rendering process, but in that case you must use the Python syntax of the module to calculate the values that go in each page footer.

Availability of env.markdown

env.markdown is accessible only at this stage of the production process (page production). It is not available in the define_env() hook, which is executed before the production of the pages has started.

If you wish to add a footer, you have the choice between:

  • using macros (before rendering):
def on_pre_page_macros(env):
    Actions to be done before macro interpretation,
    when the macros have not yet been rendered.
    footer = "\n\n{{ page.title }}"
    env.markdown += footer
  • or doing it programmatically:
def on_post_page_macros(env):
    Actions to be done after macro interpretation,
    when the macros have been rendered
    This will add a (Markdown or HTML) footer -- produced by Python.
    footer = "f'\n\n'{}"
    env.markdown += footer

If you wish to use jinja2 directives, do it it before rendering:

def on_pre_page_macros(env):
    Actions to be done before macro interpretation,
    footer = "\n\n{% include('') %}"
    env.markdown += footer

Technical Notes

Time of execution

on_pre_page_macros(env) and on_post_page_macros(env) are executed by the on_page_markdown() event of MkDocs:

  • before or after rendering of the macros into raw Markdown, respectively
  • before the rendering the page from Markdown to HTML

They operates on all pages, page by page.

Use of global variables

To facilitate the communication within your module between define_env() and on_page_markdown() you may want to define global variables. For a refresher on this, see the summary on W3 Schools.

Content and availability of attributes

the env. markdown and attributes are available only from the point of on_pre_page_macros() on. They are not available for the define_env(env) hook.

env.markdown contains the markdown of the page, before rendering (in on_pre_page_macros()) or after rendering (in on_post_page_macros())) contains notably the following information:

Attribute Value
title title of the page
abs_url the absolute url of the page from the top of the hierarchy
canonical_url the complete url of the page (typically with https://...)
markdown the whole markdown code (before or after rendering).
meta the meta data dictionary, as updated (typically) from the YAML header. is a copy so modifying it won't affect the rendering process (no black magic allowed).

Html templates


It is possible to influence to some degree, the production of html files, mostly by acting on the template HTML files (with .html extension) used in theme of MkDocs.

Do not confuse!

Note that that the standard Jinja2 engine of MkDocs that operates on the theme's pages HTML files, with the .html extension) is completely distinct from the mkdocs-macros Jinja2 engine, which is used to render macros on Markdown pages (with the .md extension).

The rendering of variables in HTML templates occurs at a later stage of the page production process.

It is important to understand that the macros defined in MkDocs-Macros cannot be used with the standard Jinja2 engine used for templates. You can, however, play around with variables, to some degree.

Here, the subject is those .html files.

Using the variables defined in the configuration file

You can perfectly use the variables defined in the extra section. This is a standard feature of MkDocs (it works without MkDocs-Macros).

Suppose you define this variable in the config file:

    production_year: 2024

This statement

Production year: <b>{{ config.extra.production_year }}</b>
will convert into:

Production year: <b>2024</b>


The statement {{ production_year }} will not work in that context. Be careful that it will not cause an error, but print a blank string. This is a feature of the MkDocs.

Trickling values into the MkDocs theme

You may want to programmatically add some meta values to a page, which should be forwarded to the Jinja2 engine that processes the pages.

This we call trickling (values are trickled from the page environment of variables to the html template), and it is also a standard feature of MkDocs (it works without Mkdocs-Macros).

For example you'd want to be able to always have a value for this:

<meta name="description" content="{{ page.meta.description }}" />

Normally metadata would be defined in the YAML header of the markdown page:

title: my title
description: This is a description


But supposing the metadata was not defined in that page? Or supposing you want to guaranteed the presence of, or alter that information? In that case, you can use a post-production feature offered by Mkdocs-Macros, to modify that metadata:

def on_post_page_macros(env):
    Actions to be done after macro interpretation,
    when the markdown is already generated
    # This information will get carried into the HTML template.['description'] = ...

Post-build files

From version 0.5


Sometimes, you want your Python code to add some files to the HTML website that MkDocs has just produced (after MkDoc's usual production workflow).

These could be:

  • an extra HTML page
  • an additional or updated image
  • a RSS feed
  • a form processor (written for example in the php language)
  • ....


The logical idea is to add files to the site (HTML) directory, which is given by env.conf['site_dir'].

Beware the of the 'disappeared file' trap

One problem will occur if you attempt to add files to the site directory from within the define_env() function in your macro module.

The file will be created, but nevertheless it is going to "disappear".

The reason is that the code of define_env() is executed during the on_config event of MkDocs; and you can expect the site directory to be wiped out later, during the build phase (which produces the HTML files). So, of course, the files you just created will be deleted.

Post-Build Actions

The solution to do that, is to perform those additions as post-build actions (i.e. executed with on_post_build event).

Here is an example. Suppose you want to add a special file (e.g. HTML).

import os
MY_FILENAME = 'foo.html'
my_HTML = None

def define_env(env):
    "Definition of the module"

    # put here your HTML content
    my_HTML = ......

def on_post_build(env):
    "Post-build actions"

    site_dir = env.conf['site_dir']
    file_path = os.path.join(site_dir, MY_FILENAME)
    with open(file_path, 'w') as f:

The mkdocs-macros plugin will pick up that function and execute it during as on on_post_build() action.

Argument of on_post_build()

In this case, the argument is env (as for define_env()); it is not config as in the on_post_build() method in an MkDocs plugin.

If you want to get the plugin's arguments, you can find them in the env.conf dictionary.

Global variables

To facilitate the communication between define_env() and on_post_build you may want to define global variables within your module (in this example: MY_FILENAME and my_HTML).


Do not forget that any variable assigned for the first time within a function is by default a local variable: its content will be lost once the function is fully executed.

In the example above, my_HTML must appear in the global definitions; which is why it was assigned an empty value.