Documentation

PDF Export & Client Integration

Serverless Puppeteer and print routing.

PaperCast generates high-quality, print-ready PDFs directly from the rendered React components using Puppeteer in a serverless environment.

The Architecture

When a user clicks "Save as PDF", the JSON schema is sent to a Next.js API route (src/app/api/pdf/route.ts). This route spins up a headless browser (Puppeteer) and navigates to a dedicated print page.

1. The API Route (/api/pdf)

export const runtime = "nodejs"; // Must NOT be 'edge' for Puppeteer
export const maxDuration = 60; // PDF generation often needs more time

Because Puppeteer spins up a real Chromium instance, it cannot run on the Edge runtime. It must run in a standard Node.js environment. We also increase the maxDuration to 60 seconds because booting Chromium and rendering complex documents can exceed the default 10-second serverless timeout.

2. Passing Data to the Headless Browser

To render the document, the headless browser needs the JSON schema. Instead of making a network request from the headless browser back to our database (which can cause auth issues or race conditions), we inject the JSON directly into the window object:

await page.evaluateOnNewDocument((data) => {
  (window as any).__PRINT_DATA__ = data;
}, jsonString);

3. The Print Route (/print)

The headless browser navigates to ${host}/print.

The src/app/print/page.tsx file is optimized strictly for printing. It reads the injected window.__PRINT_DATA__, parses it into the DocumentSchema, and renders it using the NodeRenderer.

const dataString =
  typeof window !== "undefined" ? (window as any).__PRINT_DATA__ : null;

4. Waiting for Rendering

The API route waits for a specific CSS selector to appear before taking the PDF snapshot:

await page.waitForSelector(".print-page", { timeout: 10000 });

This guarantees that the React tree has finished rendering, fonts have loaded, and the Pagination Engine has successfully measured and split the document across pages.

Layout Configuration

The API route reads the user's pageSize and orientation settings from the AST metadata to configure Puppeteer correctly:

const pdf = await page.pdf({
  printBackground: true,
  format: format ?? "A4",
  landscape,
});
Background Colors

printBackground: true is essential! Without it, any background colors, images, or dark themes you applied to your layout elements will be completely white in the final PDF.

Client-Side Integration

To provide a seamless experience for your users, you should integrate keyboard shortcuts (CTRL + P or CMD + P) and a dedicated "Download PDF" button directly into your frontend application.

Intercepting Print Shortcuts

You can capture the standard print shortcut to trigger the browser's native print dialog over your specific document container, rather than printing the entire web app UI.

import { useEffect } from "react";

export function usePrintShortcut(onPrint: () => void) {
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Check for CTRL+P (Windows/Linux) or CMD+P (Mac)
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "p") {
        e.preventDefault();
        onPrint();
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [onPrint]);
}

Usage in your component:

usePrintShortcut(() => {
  window.print();
});

Downloading as PDF

To trigger the serverless Puppeteer generation and download the resulting file, send the full JSON schema to your /api/pdf endpoint.

const handleDownload = async (jsonString: string) => {
  try {
    const res = await fetch("/api/pdf", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: jsonString,
    });

    if (!res.ok) throw new Error("Failed to generate PDF");

    const blob = await res.blob();
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "document.pdf";
    document.body.appendChild(a);
    a.click();

    // Cleanup
    window.URL.revokeObjectURL(url);
    document.body.removeChild(a);
  } catch (error) {
    console.error("Download error:", error);
  }
};
Vite Proxy Configuration

If you are developing a pure frontend app (e.g., Vite) and the API runs on a separate Next.js backend, ensure you proxy /api/pdf to the Next.js server port in your vite.config.ts.