PDF-Generierung mit Tailwind CSS und Puppeteer
AI generated
</>
tw
Tailwind CSS · Puppeteer · PDF · Headless Chrome
PDF-Generierung mit Tailwind CSS und Puppeteer
Headless Chrome als präzise Render-Engine

Wer Reports, Rechnungen oder Zertifikate als PDF erzeugen muss, kann dafür dieselbe Tailwind-CSS-Vorlage nutzen, die auch im Browser läuft. Puppeteer steuert Headless Chrome fern und ruft page.pdf() auf, wodurch das Layout inklusive Flexbox, Grid und Custom Properties originalgetreu ins PDF übernommen wird.

18 Min. Lesezeit Tailwind CSS · Puppeteer · Node.js Reports · Rechnungen · Zertifikate

1. Warum Headless Chrome die zuverlässigste PDF-Engine ist

Die Herausforderung bei jeder PDF-Generierung aus HTML ist, dass die meisten PDF-Bibliotheken ein eigenes, unvollständiges CSS-Subset implementieren. Flexbox, CSS Grid, Custom Properties oder moderne Selektoren wie :has() werden von klassischen HTML-zu-PDF-Konvertern oft nur teilweise oder gar nicht unterstützt. Genau hier liegt der entscheidende Vorteil von Puppeteer: Es steuert echtes, aktuelles Chrome im Headless-Modus fern, also dieselbe Render-Engine, mit der auch Tailwind CSS im Browser getestet wird. Was im Browser korrekt aussieht, sieht im PDF ebenso korrekt aus, weil dieselbe Engine beides rendert.

Diese Eigenschaft macht Puppeteer besonders attraktiv für Teams, die bereits Tailwind CSS im Frontend einsetzen. Statt eine zweite, eingeschränktere Template-Sprache für PDFs zu pflegen, wird eine ganz normale HTML-Seite mit Tailwind-Klassen geschrieben und anschließend per page.pdf() in ein Dokument exportiert. Das reduziert doppelte Pflege von Layout-Logik erheblich, weil dieselben Utility-Klassen, Komponenten und sogar dasselbe Tailwind-Theme für Web und PDF gleichermaßen funktionieren.

2. Puppeteer und Tailwind aufsetzen

Der Einstieg benötigt nur zwei Pakete: puppeteer für die Chrome-Fernsteuerung und die bereits vorhandene Tailwind-Toolchain für das CSS. Puppeteer bringt beim Installieren automatisch eine kompatible Chromium-Version mit, sodass keine separate Browser-Installation im Deployment-Environment nötig ist. Für Server ohne grafische Oberfläche, etwa in Docker-Containern, müssen einige zusätzliche Systembibliotheken installiert werden, die Chrome für das Headless-Rendering benötigt.

Die Tailwind-Seite für das PDF wird wie jede andere HTML-Seite gebaut, entweder als statische Datei oder als serverseitig gerenderte Vorlage, die Daten aus der Datenbank einsetzt. Wichtig ist, dass das kompilierte Tailwind-Stylesheet entweder inline im <head> steht oder über einen lokalen Dateipfad eingebunden wird, den Puppeteer beim Laden der Seite auflösen kann. Ein Verweis auf ein externes CDN funktioniert zwar meist auch, verlangsamt aber jeden PDF-Export um die Netzwerklatenz.


# Install Puppeteer alongside the existing Tailwind build
npm install puppeteer

# Compile the Tailwind stylesheet used by the PDF template
npx tailwindcss -i ./src/pdf.css -o ./dist/pdf.css --minify

3. Die Grundfunktion: page.pdf() im Detail

Der Kern jeder Puppeteer-basierten PDF-Generierung ist die Methode page.pdf(). Sie erwartet unter anderem das Papierformat, die Seitenränder und ob Hintergrundfarben und -bilder mit ausgegeben werden sollen. Standardmäßig unterdrückt Chrome Hintergrundfarben beim Drucken, was in Tailwind-Layouts mit farbigen Boxen oder Badges sofort auffällt. Die Option printBackground: true ist deshalb in praktisch jedem Tailwind-basierten PDF-Export Pflicht.

Vor dem Aufruf von page.pdf() muss die Seite vollständig geladen sein, inklusive aller Bilder und Web-Fonts. Puppeteer bietet dafür page.goto(url, { waitUntil: "networkidle0" }), das wartet, bis für mindestens 500 Millisekunden keine Netzwerkaktivität mehr stattfindet. Bei Web-Fonts, die asynchron nachgeladen werden, reicht das manchmal nicht aus, weshalb zusätzlich await page.evaluateHandle("document.fonts.ready") sinnvoll ist, um sicherzustellen, dass alle Schriftarten vor dem PDF-Export tatsächlich gerendert wurden.


// generate-pdf.js — render a Tailwind HTML page to PDF with Puppeteer
const puppeteer = require("puppeteer");

async function renderPdf(htmlPath, outputPath) {
  const browser = await puppeteer.launch({ headless: "new" });
  const page = await browser.newPage();

  await page.goto(`file://${htmlPath}`, { waitUntil: "networkidle0" });
  // Ensure web fonts are fully loaded before rendering
  await page.evaluateHandle("document.fonts.ready");

  await page.pdf({
    path: outputPath,
    format: "A4",
    printBackground: true,
    margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
  });

  await browser.close();
}

renderPdf("/tmp/report.html", "/tmp/report.pdf");

4. Print-spezifisches Tailwind-CSS für Seitenumbrüche

Chrome respektiert beim PDF-Export die CSS-Eigenschaften break-inside, break-before und break-after, die in Tailwind über Klassen wie break-inside-avoid oder break-before-page zugänglich sind. Ohne diese Klassen kann eine Tabellenzeile oder eine Karte mitten im Seitenumbruch zerschnitten werden, was bei mehrseitigen Reports besonders störend wirkt. Die Klasse break-inside-avoid auf einer Karte oder Tabellenzeile weist Chrome an, das Element im Zweifel komplett auf die nächste Seite zu verschieben, statt es aufzuteilen.

Für erzwungene Seitenumbrüche, etwa zwischen Kapiteln eines Reports, eignet sich break-before-page auf dem ersten Element eines neuen Abschnitts. Zusätzlich lohnt sich eine eigene @media print-Regel im Tailwind-Stylesheet, die interaktive Elemente wie Buttons ausblendet, die im PDF ohnehin keine Funktion haben, sowie Navigationsleisten, die nur im Browser-Kontext Sinn ergeben. So bleibt eine einzige HTML-Vorlage sowohl für die Bildschirmansicht als auch für den PDF-Export nutzbar.


/* pdf.css — print-only overrides layered on top of Tailwind's utilities */
@media print {
  .no-print {
    display: none !important;
  }
  .page-break {
    break-before: page;
  }
  table tr {
    break-inside: avoid;
  }
}

5. Kopf- und Fußzeilen mit headerTemplate und footerTemplate

Puppeteer erlaubt über die Optionen headerTemplate und footerTemplate das Einfügen von HTML-Fragmenten, die auf jeder Seite des PDFs wiederholt werden, etwa für Seitenzahlen, Firmenlogo oder Datum. Diese Templates laufen in einem eigenen, sehr eingeschränkten Rendering-Kontext, der kein externes CSS lädt, weshalb hier zwingend Inline Styles statt Tailwind-Klassen verwendet werden müssen. Für die dynamische Seitenzahl stehen die speziellen CSS-Klassen pageNumber und totalPages zur Verfügung, die Puppeteer automatisch befüllt.

Ein häufiger Fehler ist, displayHeaderFooter: true zu vergessen, ohne das die Templates komplett ignoriert werden, selbst wenn sie korrekt übergeben wurden. Ebenso wichtig: Die Seitenränder im Hauptdokument müssen groß genug gewählt werden, damit Kopf- und Fußzeile nicht mit dem eigentlichen Inhalt kollidieren, da Puppeteer diese Bereiche nicht automatisch vom Content-Bereich abzieht.


// Add a repeating footer with dynamic page numbers to every PDF page
await page.pdf({
  path: "/tmp/report.pdf",
  format: "A4",
  printBackground: true,
  displayHeaderFooter: true,
  headerTemplate: "<span></span>", // empty header, inline styles only
  footerTemplate: `
    <div style="font-size:9px;width:100%;text-align:center;color:#64748b;">
      Seite <span class="pageNumber"></span> von <span class="totalPages"></span>
    </div>
  `,
  margin: { top: "20mm", bottom: "20mm" },
});

6. Praxisbeispiel: mehrseitiger Report als PDF

Ein realistisches Szenario ist ein monatlicher Umsatzreport mit einer Titelseite, mehreren Diagrammen und einer detaillierten Tabelle über mehrere Seiten. Die Titelseite nutzt break-before-page für den nachfolgenden Abschnitt, sodass Tabelle und Diagramme garantiert auf einer neuen Seite beginnen. Die Tabelle selbst verwendet break-inside-avoid auf jeder Zeile, damit keine Zahlenreihe an einem Seitenumbruch zerrissen wird, während der Tabellenkopf mit thead auf jeder neuen Seite automatisch wiederholt wird, wenn die Tabelle als natives HTML-<table>-Element aufgebaut ist.

Diagramme, die im Browser über eine JavaScript-Bibliothek gerendert werden, müssen vor dem PDF-Export vollständig fertig gezeichnet sein. Deshalb wartet das Puppeteer-Skript zusätzlich auf ein benutzerdefiniertes Signal, etwa ein CSS-Attribut wie data-charts-ready="true", das die Seite selbst setzt, sobald alle Diagramme fertig gerendert sind. Erst wenn dieses Attribut per page.waitForSelector() gefunden wurde, ruft das Skript page.pdf() auf, um leere oder halb gezeichnete Diagramme im fertigen Dokument zu vermeiden.

7. Performance: Browser-Pooling statt Neustart pro PDF

Das Starten einer neuen Chrome-Instanz dauert typischerweise mehrere hundert Millisekunden bis wenige Sekunden, was bei einzelnen PDF-Exporten kein Problem darstellt, bei hoher Anfragefrequenz aber schnell zum Engpass wird. Statt für jedes PDF einen neuen Browser-Prozess zu starten und zu beenden, hält ein produktionsreifer Service eine Puppeteer-Browser-Instanz dauerhaft im Speicher und öffnet für jeden Export nur einen neuen Tab über browser.newPage(). Nach Abschluss des Exports wird der Tab geschlossen, der Browser-Prozess selbst bleibt bestehen.

Für sehr hohe Lastspitzen empfiehlt sich ein kleiner Pool aus mehreren gleichzeitig laufenden Browser-Instanzen, ähnlich einem Datenbank-Connection-Pool. Bibliotheken wie generic-pool lassen sich direkt mit Puppeteer kombinieren, um eine feste Anzahl an Browser-Instanzen wiederzuverwenden und gleichzeitig eine Obergrenze für parallele PDF-Exporte zu setzen, damit der Server nicht durch zu viele gleichzeitige Chrome-Prozesse überlastet wird.


// pdf-pool.js — reuse a single browser instance across many PDF exports
const puppeteer = require("puppeteer");

let browserInstance = null;

async function getBrowser() {
  if (!browserInstance) {
    browserInstance = await puppeteer.launch({ headless: "new" });
  }
  return browserInstance;
}

async function renderPdf(htmlPath, outputPath) {
  const browser = await getBrowser();
  const page = await browser.newPage();
  await page.goto(`file://${htmlPath}`, { waitUntil: "networkidle0" });
  await page.pdf({ path: outputPath, format: "A4", printBackground: true });
  await page.close(); // close the tab, keep the browser process alive
}

8. Typische Stolpersteine bei Fonts und Bildern

Ein häufiges Problem in Docker-Umgebungen: Chrome findet die im Tailwind-Design verwendeten Web-Fonts nicht, weil das Container-Image standardmäßig keine oder nur sehr wenige Schriftarten mitbringt. Fehlt eine Schriftart, fällt Chrome lautlos auf eine System-Standardschrift zurück, ohne einen Fehler zu werfen, was im PDF zu einem völlig anderen Schriftbild führt als im Browser. Die zuverlässigste Lösung ist, die benötigten Font-Dateien direkt in das Docker-Image zu kopieren und per @font-face mit einem lokalen Dateipfad einzubinden, statt auf ein Google-Fonts-CDN zu vertrauen.

Bilder, die per relativer URL eingebunden sind, funktionieren beim Laden von lokalen HTML-Dateien über file:// oft nicht, weil der Browser den relativen Pfad nicht auflösen kann. Zuverlässiger ist es, Bilder entweder als absolute file://-Pfade einzubinden oder direkt als Base64-kodierte Data-URI in das HTML einzubetten, was zusätzlich verhindert, dass Puppeteer auf externe HTTP-Anfragen warten muss, bevor die Seite als vollständig geladen gilt.

9. Puppeteer im Vergleich zu anderen PDF-Ansätzen

Puppeteer ist nicht die einzige Möglichkeit, aus Tailwind-CSS-Layouts PDFs zu erzeugen. Die folgende Übersicht zeigt, wann sich der Aufwand einer echten Chrome-Instanz lohnt und wann leichtgewichtigere Alternativen ausreichen.

Ansatz CSS-Unterstützung Ressourcenverbrauch Eignung
Puppeteer (Headless Chrome) Vollständig, Flexbox und Grid Hoch, ganzer Browser-Prozess Komplexe Layouts, hohe Treue zum Design
wkhtmltopdf (WebKit) Gut, kein modernes Grid Niedriger als Chrome Einfachere Dokumente ohne Grid-Layout
Dompdf (PHP) Eingeschränkt, kein Flexbox Niedrig, kein Browser nötig Reine PHP-Umgebungen ohne Node
Externer PDF-Dienst Abhängig vom Anbieter Ausgelagert Wenn eigene Infrastruktur vermieden werden soll

Für Teams, die Tailwind CSS bereits im Frontend nutzen und Wert auf pixelgenaue Übereinstimmung zwischen Bildschirm und PDF legen, bleibt Puppeteer die naheliegendste Wahl, weil dieselbe Chrome-Engine beide Darstellungen rendert. Der höhere Ressourcenverbrauch eines vollständigen Browser-Prozesses lässt sich durch Browser-Pooling und die Wiederverwendung einer laufenden Instanz gut kompensieren.


# Required system libraries for headless Chrome in a minimal Docker image
apt-get install -y \
  libnss3 libatk-bridge2.0-0 libx11-xcb1 \
  libxcomposite1 libxrandr2 libgbm1 libpango-1.0-0 \
  fonts-liberation fonts-dejavu-core
# Verify the Chromium binary bundled with Puppeteer actually starts
node -e "require('puppeteer').launch().then(b => b.close())"

Mironsoft

Tailwind CSS, Puppeteer-Services und PDF-Reporting für Node- und PHP-Projekte

Reports und Dokumente als pixelgenaues PDF?

Wir bauen Puppeteer-Services, die Tailwind-Vorlagen zuverlässig als PDF exportieren, inklusive Seitenumbrüchen, Kopf- und Fußzeilen und Browser-Pooling für hohe Lastspitzen.

PDF-Service

Puppeteer-basierter Renderdienst mit Browser-Pooling und Queue

Template-Design

Tailwind-Vorlagen mit sauberen Seitenumbrüchen und Kopfzeilen

Deployment

Docker-Setup mit Fonts, Chromium und stabiler Ressourcenauslastung

10. Zusammenfassung

Puppeteer macht aus Headless Chrome eine PDF-Engine, die dieselbe Render-Genauigkeit liefert wie der Browser selbst, in dem eine Tailwind-CSS-Vorlage entwickelt wurde. Flexbox, Grid und moderne CSS-Features werden vollständig unterstützt, weil keine separate, eingeschränkte PDF-Bibliothek zwischengeschaltet ist. page.pdf() übernimmt den eigentlichen Export, während Print-Media-Klassen wie break-inside-avoid für saubere Seitenumbrüche sorgen und headerTemplate/footerTemplate wiederkehrende Kopf- und Fußzeilen ermöglichen.

In der Produktion zahlt sich Browser-Pooling aus, weil das wiederholte Starten und Beenden von Chrome-Prozessen unnötige Latenz erzeugt. Wer Fonts und Bilder korrekt lokal einbindet und auf vollständiges Laden der Seite wartet, bevor page.pdf() aufgerufen wird, erhält mit Puppeteer eine PDF-Generierung, die exakt dem entspricht, was im Browser zu sehen ist, ganz ohne Kompromisse beim Tailwind-CSS-Funktionsumfang.

Für Rechnungen und andere buchhalterisch relevante Dokumente lohnt sich zusätzlich ein Blick auf reine PHP-Alternativen wie Dompdf, sobald eine Node.js-Laufzeit im Deployment nicht gewünscht ist. Die grundlegenden Prinzipien für Seitenumbrüche und Print-CSS bleiben dabei über beide Ansätze hinweg übertragbar.

PDF-Generierung mit Tailwind CSS und Puppeteer — Das Wichtigste auf einen Blick

Warum Puppeteer

Echtes Chrome rendert dieselbe Tailwind-Seite wie im Browser, inklusive Flexbox, Grid und Custom Properties.

Kernfunktion

page.pdf() mit printBackground: true, Seitenrändern und Papierformat als zentraler Export-Aufruf.

Seitenumbrüche

break-inside-avoid und break-before-page steuern, wo Tabellenzeilen und Abschnitte umbrechen.

Performance

Browser-Instanz wiederverwenden statt pro PDF neu starten, Fonts und Bilder lokal einbinden.

11. FAQ: PDF-Generierung mit Tailwind CSS und Puppeteer

1Warum eignet sich Puppeteer für Tailwind-PDFs?
Es steuert echtes Chrome, dieselbe Engine wie im Browser, deshalb funktionieren Flexbox und Grid identisch.
2Warum sind Hintergrundfarben unsichtbar?
Chrome unterdrückt sie standardmäßig beim Drucken. printBackground: true muss explizit gesetzt werden.
3Wie verhindere ich zerschnittene Tabellenzeilen?
Die Klasse break-inside-avoid verschiebt das Element im Zweifel komplett auf die nächste Seite.
4Wie füge ich Seitenzahlen ein?
Über footerTemplate mit pageNumber und totalPages, kombiniert mit displayHeaderFooter: true.
5Warum sehen Fonts im PDF anders aus?
Meist fehlt die Schriftart im Container. Fonts direkt lokal ins Image einbinden statt auf ein CDN zu vertrauen.
6Muss der Browser pro PDF neu starten?
Nein. Eine laufende Instanz mit neuen Tabs pro Export reduziert die Latenz deutlich.
7Wie stelle ich fertige Diagramme sicher?
Mit einem eigenen Signal wie einem data-Attribut, auf das per page.waitForSelector() gewartet wird.
8Funktionieren relative Bildpfade?
Oft nicht zuverlässig bei file://-Seiten. Absolute Pfade oder Base64-Data-URIs sind robuster.
9Ist Puppeteer für hohe Lastspitzen geeignet?
Ja, mit einem Pool aus mehreren Browser-Instanzen, verwaltet über eine Pool-Bibliothek.
10Wann lohnt sich wkhtmltopdf oder Dompdf?
Bei einfacheren Dokumenten ohne Grid oder in reinen PHP-Umgebungen ohne Node.js.