You can generate a PDF from your Jekyll project. You do this by creating a web version of your project that is printer friendly. You then use utility called Prince to iterate through the pages and create a PDF from them. It works quite well and gives you complete control to customize the PDF output through CSS, including page directives and dynamic tags from Prince.

PDF overview

This process for creating a PDF relies on Prince XML to transform the HTML content into PDF. Prince costs about $500 per license. That might seem like a lot, but if you’re creating a PDF, you’re probably working for a company that sells a product, so you likely have access to some resources.

The basic approach is to generate a list of all pages that need to be added to the PDF, and then add leverage Prince to package them up into a PDF.

It may seem like the setup is somewhat cumbersome, but it doesn’t take long. Once you set it up, building a pdf is just a matter of running a couple of commands.

Also, creating a PDF this way gives you a lot more control and customization capabilities than with other methods for creating PDFs. If you know CSS, you can entirely customize the output.

Demo

You can see an example of the finished product here:

1. Set up Prince

Download and install Prince.

You can install a fully functional trial version. The only difference is that the title page will have a small Prince PDF watermark.

2. Create a new configuration file for each of your PDF targets

The PDF configuration file will build on the settings in the regular configuration file but will some additional fields. Here’s the configuration file for the mydoc product within this theme. This configuration file is located in the pdfconfigs folder.

destination: _site/
url: "http://127.0.0.1:4010"
baseurl: "/mydoc-pdf"
port: 4010
output: pdf
product: mydoc
print_title: Jekyll theme for documentation — mydoc product
print_subtitle: version 5.0
output: pdf
defaults:
  -
    scope:
      path: ""
      type: "pages"
    values:
      layout: "page_print"
      comments: true
      search: true

Note that the default page layout specified by this configuration file is page_print. This layout strips out all the sections that shouldn’t appear in the print PDF, such as the sidebar and top navigation bar.

Also note that there’s a output: pdf toggle in case you want to make some of your content unique to PDF output. For example, you could add conditional logic that checks whether site.output is pdf or web. If it’s pdf, then include information only for the PDF, and so on. If you’re using nav tabs, you’ll definitely want to create an alternative experience in the PDF.

In the configuration file, customize the values for the print_title and print_subtitle that you want. These will appear on the title page of the PDF.

3. Make sure your sidebar_doc.yml file has a titlepage.html and tocpage.html

There are two template pages in the root directory that are critical to the PDF:

  • titlepage.html
  • tocpage.html

These pages should appear in your sidebar YML file (in this product, mydoc_sidebar.yml):

  - title:
    output: pdf
    type: frontmatter
    folderitems:
    - title:
      url: /titlepage/
      output: pdf
      type: frontmatter
    - title:
      url: /tocpage/
      output: pdf
      type: frontmatter

Leave these pages here in your sidebar. (The output: pdf property means they won’t appear in your online TOC because the conditional logic of the sidebar.html checks whether web is equal to pdf or not before including the item in the web version of the content.)

The code in the tocpage.html is mostly identical to that of the sidebar.html page. This is essential for Prince to create the page numbers correctly with cross references.

There’s another file (in the root directory of the theme) that is critical to the PDF generation process: prince-list.txt. This file simply iterates through the items in your sidebar and creates a list of links. Prince will consume the list of links from prince-list.txt and create a running PDF that contains all of the pages listed, with appropriate cross references and styling for them all.

4. Customize your headers and footers

Open up the css/printstyles.css file and customize what you want for the headers and footers. At the very least, customize the email address (youremail@domain.com) that appears in the bottom left.

Exactly how the print styling works here is pretty nifty. You don’t need to understand the rest of the content in this section unless you want to customize your PDFs to look different from what I’ve configured. But I’m adding this information here in case you want to understand how to customize the look and feel of the PDF output.

This style creates a page reference for a link:

a[href]::after {
    content: " (page " target-counter(attr(href), page) ")"
}

You don’t want cross references for any link that doesn’t reference another page, so this style specifies that the content after should be blank:

a[href*="mailto"]::after, a[data-toggle="tooltip"]::after, a[href].noCrossRef::after {
    content: "";
}

This style specifies that after links to web resources, the URL should be inserted instead of the page number:

a[href^="http:"]::after, a[href^="https:"]::after {
    content: " (" attr(href) ")";
}

This style sets the page margins:

@page {
    margin: 60pt 90pt 60pt 90pt;
    font-family: sans-serif;
    font-style:none;
    color: gray;

}

To set a specific style property for a particular page, you have to name the page. This allows Prince to identify the page.

First you add frontmatter to the page that specifies the type. For the titlepage.html, here’s the frontmatter:

---
type: title
---

For the tocpage, here’s the frontmatter:

---
type: frontmatter
---

For the index.html page, we have this type tag (among others):

---
type: first_page
---

The default_print.html layout will change the class of the body element based on the type value in the page’s frontmatter:

<body class="{% if page.type == "title"%}title{% elsif page.type == "frontmatter" %}frontmatter{% elsif page.type == "first_page" %}first_page{% endif %} print">

Now in the css/printstyles.css file, you can assign a page name based on a specific class:

body.title { page: title }

This means that for content inside of body class="title", we can style this page in our stylesheet using @page title.

Here’s how that title page is styled:

@page title {
    @top-left {
        content: " ";
    }
    @top-right {
        content: " "
    }
    @bottom-right {
        content: " ";
    }
    @bottom-left {
        content: " ";
    }
}

As you can see, we don’t have any header or footer content, because it’s the title page.

For the tocpage.html, which has the type: frontmatter, this is specified in the stylesheet:

body.frontmatter { page: frontmatter }
body.frontmatter {counter-reset: page 1}


@page frontmatter {
    @top-left {
        content: prince-script(guideName);
    }
    @top-right {
        content: prince-script(datestamp);
    }
    @bottom-right {
        content: counter(page, lower-roman);
    }
    @bottom-left {
        content: "youremail@domain.com";   }
}

With counter(page, lower-roman), we reset the page count to 1 so that the title page doesn’t start the count. Then we also add some header and footer info. The page numbers start counting in lower-roman numerals.

Finally, for the first page (which doesn’t have a specific name), we restart the counting to 1 again and this time use regular numbers.

body.first_page {counter-reset: page 1}

h1 { string-set: doctitle content() }

@page {
    @top-left {
        content: string(doctitle);
        font-size: 11px;
        font-style: italic;
    }
    @top-right {
        content: prince-script(datestamp);
        font-size: 11px;
    }

    @bottom-right {
        content: "Page " counter(page);
        font-size: 11px;
    }
    @bottom-left {
        content: prince-script(guideName);
        font-size: 11px;
    }
}

You’ll see some other items in there such as prince-script. This means we’re using JavaScript to run some functions to dynamically generate that content. These JavaScript functions are located in the _includes/head_print.html:

<script>
    Prince.addScriptFunc("datestamp", function() {
        return "PDF last generated: March 29, 2020";
    });
</script>

<script>
    Prince.addScriptFunc("guideName", function() {
        return " User Guide";
    });
</script>

There are a couple of Prince functions that are default functions from Prince. This gets the heading title of the page:

        content: string(doctitle);

This gets the current page:

        content: "Page " counter(page);

Because the theme uses JavaScript in the CSS, you have to add the --javascript tag in the Prince command (detailed later on this page).

5. Customize the PDF script

Duplicate the pdf-mydocf.sh file in the root directory and customize it for your specific configuration files.

echo 'Killing all Jekyll instances'
kill -9 $(ps aux | grep '[j]ekyll' | awk '{print $2}')
clear

echo "Building PDF-friendly HTML site for Mydoc ...";
jekyll serve --detach --config _config.yml,pdfconfigs/config_mydoc_pdf.yml;
echo "done";

echo "Building the PDF ...";
prince --javascript --input-list=_site/pdfconfigs/prince-list.txt -o _pdf/mydoc.pdf;
echo "done";

Note that the first part kills all Jekyll instances. This way you won’t try to serve Jekyll at a port that is already occupied.

The jekyll serve command serves up the HTML-friendly PDF configurations for our two projects. This web version is where Prince will go to get its content.

The prince script issues a command to the Prince utility. JavaScript is enabled (--javascript), and we tell it exactly where to find the list of files (--input-list) — just point to the prince-list.txt file. Then we tell it where and what to output (-o).

Make sure that the path to the prince-list.txt is correct. For the output directory, I like to output the PDF file into my project’s source (into the files folder). Then when I build the web output, the PDF is included and something I can refer to.

6. Add conditions for your new builds in the sidebarconfigs.html file

In the _includes/custom/sidebarconfigs.html file, there’s a section that looks like this:

{% if site.product == "mydoc" %}
{% assign sidebar_pdf = site.data.sidebars.mydoc_sidebar.entries %}
{% endif %}

{% if site.product == "product1" %}
{% assign sidebar_pdf = site.data.sidebars.product1_sidebar.entries %}
{% endif %}

{% if site.product == "product2" %}
{% assign sidebar_pdf = site.data.sidebars.product2_sidebar.entries %}
{% endif %}

Add your own condition here that points to your sidebar.

What this does is allow the prince-list.txt and toc.html files to use a variable for the sidebar (called sidebar_pdf) when iterating through the sidebar. Otherwise, you would need to create a unique prince-list.txt and toc.html file for each separate PDF output you have.

7. Add a download button for the PDF

You can add a download button for your PDF using some Bootstrap button code:

<a target="_blank" class="noCrossRef" href="/pdf/mydoc.pdf"><button type="button" class="btn btn-default" aria-label="Left Align"><span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span> PDF Download</button></a>

Here’s what that looks like:

<a target=”_blank” class=”noCrossRef” href=pdf/mydoc.pdf”></a>

JavaScript conflicts

If you have JavaScript on any of your pages, Prince will note errors in Terminal like this:

error: TypeError: value is not an object

However, the PDF will still build.

You need to conditionalize out any JavaScript from your PDF web output before building your PDFs. Make sure that the PDF configuration files have the output: pdf property.

Then surround the JavaScript with conditional tags like this:

{% unless site.output == "pdf" %}
javascript content here ...
{% endunless %}

For more detail about using unless in conditional logic, see [Conditional logic][mydoc_conditional_logic]. What this code means is “run this code unless this value is the case.”

Overriding Bootstrap Print Styles

The theme relies on Bootstrap’s CSS for styling. However, for print media, Bootstrap applies the following style:

@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}

This is minified, but basically the * (asterisk) means select all, and applied the color #000 (black). As a result, the Bootstrap style strips out all color from the PDF (for Bootstrap elements).

This is problematic for code snippets that have syntax highlighting. I decided to remove this de-coloring from the print output. I commented out the Bootstrap style:

@media print{*,:after,:before{/*color:#000!important;*/text-shadow:none!important;/*background:0 0!important*/;-webkit-box-shadow:none!important;box-shadow:none!important}

If you update Bootrap, make sure you make this edit. (Sorry, admittedly I couldn’t figure out how to simply overwrite the * selector with a later style.)

I did, however, remove the color from the alerts and lighten the background shading for pre elements. The printstyles.css has this setting.

orzh-introduction

X-WAF-README

nginx-lua-module-zh-wiki

orange_about

agentzh-nginx-guide-01

pra_flame_how

orzh-introduction

X-WAF-README

nginx-lua-module-zh-wiki

orange_about

titlepage

tocpage

p1_landing_page

p1_sample1

p1_sample2

p1_sample3

p1_sample4

p1_sample5

p1_sample6

p1_sample7

titlepage

tocpage

p2_landing_page

p2_sample1

p2_sample2

p2_sample3

p2_sample4

p2_sample5

p2_sample6

p2_sample7

p2_sample8

p2_sample9

p2_sample10

p2_sample11

p2_sample12

p2_sample13

p2_sample14

titlepage

tocpage

p1_landing_page

p1_sample1

p1_sample2

p1_sample3

p1_sample4

p1_sample5

p1_sample6

p1_sample7

titlepage

tocpage

X-WAF-README

xwaf_installation

xwaf_depoly

xwaf_README

xwaf_advanced_readme

xwaf_getting_started

xwaf_faqs_readme

xwaf_roadmap

titlepage

tocpage

nginx-lua-module-zh-wiki

titlepage

tocpage

orange_api_server

orange_build_plugin

orange_dashboard_usage

orange_README

orange_basic_auth

orange_basic_info

orange_divide

orange_key_auth

orange_monitor

orange_rate_limiting

orange_redirect

orange_rewrite

orange_stat

orange_waf

orange_condition

orange_expression

orange_extraction

orange_extractor

orange_handle

orange_judge

orange_rule

orange_about

orange_contributing

orange_issues

orange_usages

titlepage

tocpage

add_new_lua_api

cosocket

get_req_body

get_url_param

helloworld

how_request_http

inline_var

install

install_on_centos

install_on_ubuntu

install_on_windows

install_osx

log_response

outtest

response

safe_sql

share_var

simple_api

sub_request

work_with_location

array_size

break

brief

build_env

call_user_func_array

capture

class

control_structrues

dot_diff

dummy_var

FFI

file

for

function_before_use

function_define

function_descrip

function_parameter

function_result

if_else

local

main

math_library

metatable

module_is_evil

module

not_nill

not_use_lib

not_use_module

object_oriented

operator

re

repeat

string_library

table_library

time_date_function

what_jit

while

pra_flame_how

pra_flame_install

pra_flame_what

pra_flame_when

pra_ngx_lua_allow_deny

pra_ngx_lua_block_io

pra_ngx_lua_cache

pra_ngx_lua_capture

pra_ngx_lua_continue_after_eof

pra_ngx_lua_debug

pra_ngx_lua_ffi

pra_ngx_lua_hot_load

pra_ngx_lua_how_one_instance_time

pra_ngx_lua_how_use_third_lib

pra_ngx_lua_keepalive

pra_ngx_lua_log

pra_ngx_lua_lua-limit

pra_ngx_lua_lua_opt

pra_ngx_lua_lua-variable-scope

pra_ngx_lua_on_abort

pra_ngx_lua_phase

pra_ngx_lua_resolve_the_domain_name

pra_ngx_lua_shared_get_keys

pra_ngx_lua_sleep

pra_ngx_lua_timer

pra_ngx_lua_use_case

pra_ngx_lua_whats_cosocket

pra_redis_auth_connect

pra_redis_dynamic_redis_module_method

pra_redis_out_package

pra_redis_pipeline

pra_redis_pub_sub_package

pra_redis_script

pra_redis_select-keeplive

pra_postgres_health_check

pra_postgres_how_to_use

pra_postgres_not_support_transaction

pra_postgres_sql_inject

pra_postgres_timeout

pra_nginx_balancer

pra_nginx_co-work_of_location

pra_nginx_if_is_evil

pra_nginx_match_uri

pra_nginx_nginx_brief

pra_nginx_nginx_local_pcre

pra_nginx_nginx_log

pra_nginx_nginx

pra_nginx_pitfalls_and_common_mistakes

pra_nginx_reverse_proxy

pra_nginx_static_file

titlepage

tocpage

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_scan_port

candylab_common_sites_log_system

candylab_graylog_clickhouse

candylab_clickhouse_basic

candylab_logs_and_gateway

candylab_sec_system_arch

candylab_logs_dsl_waf

candylab_opensock_list

candylab_opensock_practice

candylab_opensock_email

candylab_base_on_openresty_waf

candylab_dsl_waf

candylab_honeypot_system

candylab_monitor_redis

candylab_threat_replay

candylab_pcap_monitor

candylab_monitor_website

candylab_openrestyplus_waf

candylab_windows_bigdata

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

candylab_clickhouse_basic

titlepage

tocpage

agentzh-nginx-guide-01

agentzh-nginx-guide-02

agentzh-nginx-guide-03

agentzh-nginx-guide-04

agentzh-nginx-guide-05

agentzh-nginx-guide-06

agentzh-nginx-guide-07

agentzh-nginx-guide-08

mydoc_hyperlinks.html#automatedlinks

mydoc_hyperlinks.html#bookmarklinks

mydoc_pages.html#someIdTag

news

nginx-lua-module-zh-wiki

nginx-development-guide-zh

orange_about

X-WAF-README

pra_flame_how