Skip to content


Print Pages to PDF files

For offline reading or printing, the document should be exported to PDF format. Here is a plugin to automatically export all site's posts to PDF during the build time.

Last update: 2022-05-07


The cover page#

When printing to a PDF file, the first page should show the post title and its short description. This page is called the cover page which will be created only in printing mode.

Create an element with class cover in the post-cover.html template to wrap the cover section. In print mode, this element should cover the full height (100%) of the first paper and align its content vertically. After the line of tags, the updated date will be shown to easily check the latest version of the document:

overrides\partials\post-cover.html
{# the cover page #}
<style>
    .md-typeset .cover {
        margin-bottom: 1em;
    }
    .md-typeset .page-category {
        color: gray;
        font-size: large;
    }
    .md-typeset .page-title {
        margin-left: -0.0625em;
    }
    .md-typeset .page-extra {
        color: gray;
        font-size: small;
    }
    .md-typeset .page-tags {
        margin: 0;
    }
    .md-typeset .page-date {
        margin: 0;
        text-align: end;
    }
    @media print {
        .md-typeset .cover {
            height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: center;
        }
        .md-typeset .cover + * {
            margin-top: 0;
        }
    }
</style>

<div class="cover">
    {# category #}
    {% if page.meta and page.meta.category %}
    <span class="page-category">
        {{ page.meta.category }} »
    </span>
    <br>
    {% endif %}

    {# title #}
    <h1 class="page-title"> {{ page_title | d(config.site_name, true) }} </h1>

    {# description #}
    {% if page.meta and page.meta.description %}
        <p class="page-description">
            {{ page.meta.description }}
        </p>
    {% endif %}

    {% if page.markdown == '' and page.parent.children %}

    {% else %}
        <div class="page-extra row">
            <div class="col">
            {% if page.meta and page.meta.tags %}
                <p class="page-tags">
                    {% for tag in page.meta.tags %}
                        <a class="tag" href="{{ config.site_url }}tags/#{{tag}}">
                            <span class="tag-name">
                                #{{ tag }} &nbsp;
                            </span>
                        </a>
                    {% endfor %}
                </p>
            {% endif %}

            </div>
            <div class="col">
                <p class="page-date">
                    <span>
                    {% if page.meta.git_revision_date_localized_raw_iso_date %}
                        {{ lang.t("source.file.date.updated") }}:
                        {{ page.meta.git_revision_date_localized_raw_iso_date }}
                    {% endif %}
                    </span>
                </p>
            </div>
        </div>
    {% endif %}
</div>

The Table of Content page#

When displaying on a screen, the Table of Content is displayed in the right sidebar. In printed pages, there should be a page to display the table of content too. This page is also only visible in printing.

The base Material for MkDocs theme has a partial block for Table of Content section, so I just need to declare it in post-toc.html and include it in the main.html template, between the cover page and the main content.

overrides\partials\post-toc.html
{# the table of content page #}
<style>
    .md-typeset .toc {
        display: none;
    }
    .md-typeset .toc label {
        display: none;
    }
    .md-typeset .toc .md-nav {
        font-size: unset;
        line-height: 1.6;
    }
    .md-typeset .toc .md-nav--secondary {
        margin-left: -2em;
    }
    .md-typeset .toc .md-nav__list {
        margin: 0;
    }
    .md-typeset .toc ul {
        list-style: none;
    }
    @media print {
        .md-typeset .toc {
            display: block;
            page-break-after: always;
        }
        .md-typeset .toc .md-nav__link {
            color: var(--md-typeset-a-color);
        }
        .md-typeset .toc .md-nav__link.md-nav__link--active {
            font-weight: unset;
        }
        .md-typeset .toc + * {
            margin-top: 0;
        }
    }
</style>

<div class="toc">
    <h2>Table of Content</h2>
    {% include "partials/toc.html" %}
</div>

There are some styles applied for this section:

  • Hide the default label and add a new <h2> header
  • Remove list-style to make a clear list
  • When printing, remove color effect on link items

Preview of the printing document

Printing styles#

There are some more additional styles need to be applied on the page when printing. I preview the printed version using Save to PDF option in the Chrome browser.

Set the paper size and printing margins:

@page {
    size: a4 portrait;
    margin: 25mm 15mm 25mm 20mm;
}

Some elements only show in printing version, add media query type to display them:

.md-typeset .print-only {
    display: none;
}
@media print {
    .md-typeset .print-only {
        display: block;
    }
    .md-typeset .screen-only {
        display: none;
    }
}

Tabs labels should be marked in printing as they are selected:

.md-typeset .tabbed-set > label {
    border-color: var(--md-accent-fg-color);
    color: var(--md-accent-fg-color);
}

The Disqus section also needs to be hidden in printing:

@media print {
    .md-typeset #__comments,
    .md-typeset #disqus_recommendations,
    .md-typeset #disqus_thread {
        display: none;
    }
}

Image and its caption should be displayed in the same page:

@media print {
    .md-typeset figure {
        page-break-inside: avoid;
    }
}

Admonition can be printed on multiple pages:

@media print {
    .md-typeset .admonition,
    .md-typeset details {
        page-break-inside: auto;
    }
}

This feature is disabled by default !!!

The plugin depends on Chrome and Chrome Driver, and it also consumes quite long time to finish rederning. It is recommended to manually print pages that you need.

The MkDocs PDF with JS Plugin1 exports documentation in PDF format with rendered JavaScript content. This is very useful if documents have mermaid diagrams. A download button will be added to the top of the page, and it is hidden in the PDF files.

For executing the JavaScript code, ChromeDriver is used, so it is necessary to:

  1. Install Chrome, find the Chrome version in About section.
  2. Download ChromeDriver, note to choose correct version of driver based on your installed Chrome version.
  3. Add the ChromeDriver to OS user’s PATH environment.


After that, install the plugin:

pip install -U git+https://github.com/vuquangtrong/mkdocs-pdf-with-js-plugin.git

Install the original plugin with pip install mkdocs-pdf-with-js-plugin if don’t need a customized version. The following features are not implemented in the original version.

Enable the plugin:

plugins:
    - search # built-in search must be always activated
    - pdf-with-js

While building mkdocs build or serving mkdocs serve the documentation, the PDF files will be generated. They are stored in the site\pdfs folder.

The command sent to ChromeDriver to print a page is Page.printToPDF, read more at Chrome DevTools Protocol — printToPDF.

This command needs some parameters to control the printing, which include:

landscape : boolean
Paper orientation. Defaults to false.
displayHeaderFooter : boolean
Display header and footer. Defaults to false.
headerTemplate: string

HTML template for the print header. Should be valid HTML markup with following classes used to inject printing values into them:

  • date: formatted print date
  • title: document title
  • url: document location
  • pageNumber: current page number
  • totalPages: total pages in the document

For example, <span class=title></span> would generate a span containing the title.

footerTemplate : string
HTML template for the print footer. Should use the same format as the headerTemplate.

Those parameters are initialized in the __init__ function:

def __init__(self):
    self.displayHeaderFooter = True
    self.headerTemplate = \
        '<div style="font-size:8px; margin:auto;">' \
        '<span class=title></span>' \
        '</div>'
    self.footerTemplate= \
        '<div style="font-size:8px; margin:auto;">' \
        'Page <span class="pageNumber"></span> of ' \
        '<span class="totalPages"></span>' \
        '</div>'

and they are used to creating print options in a dictionary variable:

def _get_print_options(self):
    return {
        'landscape': False,
        'displayHeaderFooter': self.displayHeaderFooter,
        'footerTemplate': self.footerTemplate,
        'headerTemplate': self.headerTemplate,
        'printBackground': True,
        'preferCSSPageSize': True,
    }

Finally, the print options are used in the print command:

def print_to_pdf(self, driver, page):
    driver.get(page["url"])
    result = self._send_devtools_command(
        driver, "Page.printToPDF",
        self._get_print_options()
    )
    self._write_file(result['data'], page["pdf_file"])

Add plugin config options#

To allow user to change the print options in the project config file mkdocs.yml, add the config fields into the plugin.py file.

class PdfWithJS(BasePlugin):
    config_scheme = (
        ('enable', config_options.Type(bool, default=True)),
        ('display_header_footer', config_options.Type(bool, default=False)),
        ('header_template', config_options.Type(str, default='')),
        ('footer_template', config_options.Type(str, default='')),
    )

When the MkDocs engine calls to on_config() function in this plugin, save the user’s configs as below:

def on_config(self, config, **kwargs):
    self.enabled = self.config['enable']
    self.printer.set_config (
        self.config['display_header_footer'],
        self.config['header_template'],
        self.config['footer_template']
        )
    return config

By doing this, users can add their parameters to the pdf-with-js entry under the plugins field in the config file mkdocs.yml:

plugins:
    - search # built-in search must be always activated
    - pdf-with-js:
        enable: false # should enable only when need PDF files
        add_download_button: false
        display_header_footer: true
        header_template: >-
            <div style="font-size:8px; margin:auto; color:lightgray;">
                <span class="title"></span>
            </div>
        footer_template: >-
            <div style="font-size:8px; margin:auto; color:lightgray;">
                Page <span class="pageNumber"></span> of 
                <span class="totalPages"></span>
            </div>

Add a download button#

Create an element to contain the download button at the beginning of the document content in the base.html template. This element should be hidden in printing mode.

The plugin will find the <div class="btn-actions"> element to insert a button. If there is no such existing element, the plugin will create a new element and insert to the page content.

def _add_link(self, soup, page_paths):

    icon = BeautifulSoup(''
        '<span class="twemoji">'
            '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">'
                '<path d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7 7-7z"></path>'
            '</svg>'
        '</span>',
        'html.parser')
    text = "PDF"

    btn = soup.new_tag("a", href=page_paths["relpath"])
    btn.append(icon)
    btn.append(text)
    btn['class'] = 'md-button'

    bar = soup.find("div", {"class" : "btn-actions"})
    if bar:
        bar.p.insert(0, btn)
    else:
        toc = soup.find("div", {"class" : "toc"})
        if toc:
            div = BeautifulSoup(''
                '<div class="btn-actions screen-only">'
                    '<p></p>'
                '</div>',
                'html.parser')
            div.p.insert(0, btn)
            toc.insert_after(div)

    return soup

That’s it. All blog posts now have a download button for users to get the PDF version.


  1. originally developed by smaxtec 

Comments