Skip to main content

Export the content page as it is

[TOC]

Overview

Customizations aren't appearing in PDF exports becuase BookStack's PDF engine (by default, dompdf) is a separate process that doesn't execute JavaScript and only recognizes a subset of CSS.

To ensure your styles are applied to PDF exports follows the steps

Steps

1. Install npm

apt update
apt install npm -y

2. Install Puppeteer

npm install puppeteer

Install Chrome for the Web User

# Define a local cache path and install
PUPPETEER_CACHE_DIR=/opt/bookstack/.cache npx puppeteer browsers install chrome

Set Cache Permissions

# Replace 'www-data' with your web user if different (e.g., 'nginx')
chown -R www-data:www-data /opt/bookstack/.cache

Update .env to manipulated export command

Edit .env adding:

ALLOW_UNTRUSTED_SERVER_FETCHING=true
EXPORT_PDF_COMMAND="node /opt/bookstack/puppeteer-pdf.js {input_html_path} {output_pdf_path}"
EXPORT_PDF_COMMAND_TIMEOUT=30

Create the Export Script

In BookStack root directory (/opt/bookstack) create a new file named puppeteer-pdf.js

const puppeteer = require('puppeteer');
const fs = require('fs');

(async () => {
    const browser = await puppeteer.launch({
        executablePath: '/opt/bookstack/.cache/chrome/linux-146.0.7680.76/chrome-linux64/chrome',
        args: ['--no-sandbox', '--disable-setuid-sandbox']
    });
    const page = await browser.newPage();

    const htmlContent = fs.readFileSync(process.argv[2], 'utf8');
    await page.setContent(htmlContent, { waitUntil: 'networkidle0' });

    // --- NEW TOC GENERATION LOGIC ---
    await page.evaluate(() => {
        const tocPlaceholder = document.body.innerHTML.match(/\[TOC\]/g);
        if (!tocPlaceholder) return;

        // Find all headings (h1 to h4)
        const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4'));
        let tocHtml = '<div class="table-of-contents"><h2>Table of Contents</h2><ul>';

        headings.forEach((heading, index) => {
            const id = heading.id || `heading-${index}`;
            heading.id = id; // Ensure heading has an ID to link to
            const level = heading.tagName.toLowerCase();
            tocHtml += `<li class="toc-${level}"><a href="#${id}">${heading.innerText}</a></li>`;
        });

        tocHtml += '</ul></div>';

        // Replace the [TOC] text with our generated HTML
        document.body.innerHTML = document.body.innerHTML.replace('[TOC]', tocHtml);
    });
    // --------------------------------

    await page.pdf({
        path: process.argv[3],
        format: 'A4',
        printBackground: true,
        margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' }
    });

    await browser.close();
})();