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:
- Creates
NodeJsWrapperandNpmWrapperinstances to manage Node.js dependencies. - Launches
PuppeteerPdfGeneratorScriptfromquarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/pdf/PuppeteerPdfGeneratorScript.kt. - Starts a temporary
LocalFileWebServeron a free TCP port (starting from 8096). - Constructs the URL
http://localhost:<port>/?print-pdf. - Invokes the Node.js script via
NodeJsWrapper.eval(), passing the bundledpdf.jsresource.
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.ktwraps HTML content using thewrapper.html.jtetemplate, creating a WebSocket-driven double-iframe system for smooth live reloading. - PDF generation via
HtmlPdfExporterreuses the sameLocalFileWebServerinfrastructure to serve content to a headless Puppeteer browser instance. - The
?print-pdfquery parameter signals the bundledpdf.jsscript to executepage.pdf()rather than standard navigation. - Node.js integration is managed through wrapper classes
NodeJsWrapperandNpmWrapper, withPuppeteerNodeModuleensuring 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →