How LivePreviewEndpoint Works with Puppeteer for PDF Generation in Quarkdown

The LivePreviewEndpoint serves HTML through a double-iframe WebSocket wrapper for live reloading, while PDF generation reuses the same static file server to launch a headless Puppeteer instance that renders the identical page to PDF via a ?print-pdf query parameter.

Quarkdown, an open-source markdown rendering engine by iamgio, provides two complementary runtime features that share the same underlying infrastructure. The LivePreviewEndpoint delivers real-time browser previews through WebSocket-driven reloads, while the PDF export pipeline leverages Puppeteer to convert the same rendered HTML into print-ready documents. Both systems rely on the identical static file serving logic and HTML templates to guarantee visual parity between preview and export.

Architecture Overview

Quarkdown implements two distinct user-facing features that reuse a common server foundation:

  • Live Preview: A Ktor endpoint that wraps rendered HTML in a double-buffered iframe system with WebSocket notifications for hot reloading.
  • PDF Export: A temporary server instance that feeds the same HTML to a headless Chrome browser controlled by Puppeteer, writing the rendered output to a PDF file.

The shared component between these workflows is the LocalFileWebServer class, which provides minimal static file hosting used by both the live preview endpoint and the PDF generation script.

Live Preview Implementation

The live preview system centers on the LivePreviewEndpoint.kt file located at quarkdown-server/src/main/kotlin/com/quarkdown/server/endpoints/LivePreviewEndpoint.kt.

Request Handling and File Resolution

When the server receives a request at /live/<file>, the handleRequest method processes the incoming path. The getTargetFile function resolves the requested file within the configured origin directory.

For HTML files specifically, the endpoint does not stream the file directly. Instead, it invokes createHtmlWrapperText, which renders the JTE template wrapper.html.jte from quarkdown-server/src/main/resources/live-preview/wrapper.html.jte.

Double-Iframe WebSocket Wrapper

The wrapper template receives three critical values:

.value("srcFile", sourceFile)         // e.g. "/docs/example.html"
.value("serverHost", SERVER_HOST)     // usually "localhost"
.value("serverPort", serverPort)      // the port the Ktor server is listening on

The rendered HTML contains two iframes (frame-0 and frame-1) and a WebSocket client that connects to ws://localhost:<port>/reload. The client-side JavaScript manages double-buffering by swapping iframe visibility, preserving scroll position, and listening for postRenderingCompleted messages to ensure flicker-free updates during file changes.

PDF Generation Pipeline

PDF export functionality resides in quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/pdf/HtmlPdfExporter.kt.

High-Level Execution Flow

The HtmlPdfExporter class orchestrates the conversion process through these steps:

  1. Creates NodeJsWrapper and NpmWrapper instances to manage Node.js dependencies.
  2. Launches PuppeteerPdfGeneratorScript from quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/pdf/PuppeteerPdfGeneratorScript.kt.
  3. Starts a temporary LocalFileWebServer on a free TCP port (starting from 8096).
  4. Constructs the URL http://localhost:<port>/?print-pdf.
  5. Invokes the Node.js script via NodeJsWrapper.eval(), passing the bundled pdf.js resource.

All operations execute synchronously; the temporary server terminates immediately after the PDF writes to disk.

Puppeteer Integration Details

The PuppeteerNodeModule class declares the puppeteer npm dependency, ensuring installation via NodeNpmHelper before script execution. The actual browser automation logic lives in pdf.js, packaged as a resource at quarkdown-html/src/main/resources/pdf/pdf.js.

The Puppeteer script loads the target URL, detects the ?print-pdf query parameter, and invokes page.pdf() to generate the output file. The query string triggers PDF mode without requiring special server-side routing—the static file handler ignores the parameter while the client-side script interprets it.

The Shared Static File Server

The LocalFileWebServer class in quarkdown-server/src/main/kotlin/com/quarkdown/server/LocalFileWebServer.kt provides the common HTTP foundation. This minimal Ktor implementation serves static files from a specified directory, used by:

  • The live preview endpoint for continuous browser serving.
  • The PDF exporter for temporary hosting during Puppeteer rendering.

By reusing this server, Quarkdown guarantees that fonts, CSS, JavaScript (including MathJax and Mermaid diagrams), and layout render identically in both preview and PDF contexts.

Practical Usage Examples

Command Line Interface

Start the live preview server:

quarkdown -w docs/ --preview

Then open http://localhost:8080/live/index.html to view the double-iframe preview with automatic reloading.

Export to PDF using the same underlying rendering pipeline:

quarkdown -i docs/ -o out.pdf --pdf

Programmatic Kotlin API

For custom integrations, instantiate HtmlPdfExporter directly:

import com.quarkdown.rendering.html.pdf.HtmlPdfExporter
import com.quarkdown.rendering.html.pdf.HtmlPdfExportOptions
import java.io.File

val srcDir = File("docs/")
val outPdf = File("mydoc.pdf")
val options = HtmlPdfExportOptions(
    nodeJsPath = "/usr/local/bin/node",
    npmPath = "/usr/local/bin/npm",
    noSandbox = true
)

HtmlPdfExporter(options).export(srcDir, outPdf)

This API launches the temporary server, manages the Puppeteer process through NodeJsWrapper and NpmWrapper, and handles cleanup automatically.

Summary

  • LivePreviewEndpoint in LivePreviewEndpoint.kt wraps HTML content using the wrapper.html.jte template, creating a WebSocket-driven double-iframe system for smooth live reloading.
  • PDF generation via HtmlPdfExporter reuses the same LocalFileWebServer infrastructure to serve content to a headless Puppeteer browser instance.
  • The ?print-pdf query parameter signals the bundled pdf.js script to execute page.pdf() rather than standard navigation.
  • Node.js integration is managed through wrapper classes NodeJsWrapper and NpmWrapper, with PuppeteerNodeModule ensuring the npm dependency is available.
  • Both features guarantee identical rendering by sharing the same static file server and HTML templates, ensuring the PDF matches the live preview exactly.

Frequently Asked Questions

What port does the temporary PDF server use?

The PuppeteerPdfGeneratorScript searches for an available TCP port starting at 8096, incrementing until it finds an open port. This ephemeral server runs only for the duration of the PDF generation process and shuts down immediately after the file is written.

How does the live preview avoid flickering when reloading?

The wrapper.html.jte template implements double-buffering using two iframes. While one iframe displays the current content, the other loads the updated version in the background. Once the postRenderingCompleted message fires, the visibility swaps instantly, preserving scroll position and eliminating visual flash.

Why does PDF generation use the same server as live preview?

Reusing LocalFileWebServer ensures that all relative paths, CSS imports, client-side JavaScript libraries, and font resources resolve identically in both contexts. This architectural choice guarantees that the PDF output visually matches the browser preview, including complex elements like MathJax equations and Mermaid diagrams.

Where is the actual Puppeteer automation code located?

The JavaScript code that controls Puppeteer resides in pdf.js, packaged as a JVM resource at quarkdown-html/src/main/resources/pdf/pdf.js. The PuppeteerPdfGeneratorScript loads this resource via javaClass.getResourceAsStream("/pdf/pdf.js") and executes it through the NodeJsWrapper, passing the target URL and output path as arguments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →