# How LivePreviewEndpoint Works with Puppeteer for PDF Generation in Quarkdown

> Discover how Quarkdown's LivePreviewEndpoint leverages Puppeteer for PDF generation, reusing the static file server and a print query parameter for identical page rendering.

- Repository: [Giorgio Garofalo/quarkdown](https://github.com/iamgio/quarkdown)
- Tags: internals
- Published: 2026-04-29

---

**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`](https://github.com/iamgio/quarkdown/blob/main/LivePreviewEndpoint.kt) file located at [`quarkdown-server/src/main/kotlin/com/quarkdown/server/endpoints/LivePreviewEndpoint.kt`](https://github.com/iamgio/quarkdown/blob/main/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:

```kotlin
.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`](https://github.com/iamgio/quarkdown/blob/main/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`](https://github.com/iamgio/quarkdown/blob/main/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`](https://github.com/iamgio/quarkdown/blob/main/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`](https://github.com/iamgio/quarkdown/blob/main/pdf.js), packaged as a resource at [`quarkdown-html/src/main/resources/pdf/pdf.js`](https://github.com/iamgio/quarkdown/blob/main/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`](https://github.com/iamgio/quarkdown/blob/main/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:

```bash
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:

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

```

### Programmatic Kotlin API

For custom integrations, instantiate `HtmlPdfExporter` directly:

```kotlin
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`](https://github.com/iamgio/quarkdown/blob/main/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`](https://github.com/iamgio/quarkdown/blob/main/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`](https://github.com/iamgio/quarkdown/blob/main/pdf.js), packaged as a JVM resource at [`quarkdown-html/src/main/resources/pdf/pdf.js`](https://github.com/iamgio/quarkdown/blob/main/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.