Headless Chrome als Build-Tool – Screenshots ohne npm-Ballast
Headless Chrome als Build-Tool: Screenshots und PDFs aus HTML erzeugen – pixelgenau, ohne npm-Abhängigkeiten. Mit echtem Bash-Script aus der Praxis.
Irgendwann braucht man Bilder aus HTML. Social-Media-Grafiken, die zum Website-Design passen. Ein PDF aus einer gestylten Seite. Eine Vorschaukachel für jeden Blogpost. Der erste Reflex: ein npm-Paket suchen. puppeteer installieren – und schon zieht man 300 MB Chromium-Binary und einen Sack voll Dependencies in ein Projekt, das eigentlich nur ein paar PNGs braucht.
Dabei steht das Werkzeug schon auf fast jedem Rechner: Chrome selbst. Im Headless-Modus ist der Browser ein vollwertiges Render-Tool, das man direkt von der Kommandozeile fährt. Kein npm, keine Abhängigkeiten, kein Build-Schritt. So geht’s.
Die Grundidee
Chrome kann ohne Fenster starten, eine beliebige URL (auch eine lokale file://-Datei) rendern und das Ergebnis als PNG oder PDF speichern. Ein einziger Befehl:
chrome --headless --screenshot=out.png --window-size=1080,1080 \
"file:///pfad/zu/grafik.html"
Das ist der Kern. Alles Weitere ist Komfort drumherum. Was hier passiert: Chrome lädt die HTML-Datei, rendert sie in ein virtuelles Fenster von 1080×1080 Pixeln und schießt einen Screenshot. Das PNG ist pixelgenau das, was du auch im echten Browser sehen würdest – inklusive Web-Fonts, CSS-Gradients, allem.
Chrome zuverlässig finden
Das erste Praxisproblem: Chrome heißt auf jedem System anders. Auf dem Mac liegt es tief in einem .app-Bundle, unter Linux gibt es google-chrome, chromium oder chromium-browser. Ein robustes Script probiert die Kandidaten der Reihe nach durch:
CHROME=""
for c in \
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
"/Applications/Chromium.app/Contents/MacOS/Chromium" \
"$(command -v google-chrome 2>/dev/null || true)" \
"$(command -v chromium 2>/dev/null || true)" \
"$(command -v chromium-browser 2>/dev/null || true)"; do
if [ -n "$c" ] && [ -x "$c" ]; then CHROME="$c"; break; fi
done
if [ -z "$CHROME" ]; then
echo "✗ Kein Chrome/Chromium gefunden." >&2
exit 1
fi
Der erste ausführbare Treffer gewinnt. Das || true verhindert, dass set -e über ein leeres command -v stolpert.
Die Flags, auf die es ankommt
Headless Chrome hat dutzende Optionen, aber eine Handvoll macht den Unterschied zwischen “läuft” und “läuft sauber”:
"$CHROME" --headless \
--disable-gpu \ # vermeidet GPU-Probleme im headless-Betrieb
--no-sandbox \ # nötig in CI/Containern ohne User-Namespaces
--force-color-profile=srgb \ # konsistente Farben über Systeme hinweg
--hide-scrollbars \ # keine Scrollbalken im Bild
--default-background-color=00000000 \ # transparenter Hintergrund (RGBA)
--window-size=1080,1080 \
--screenshot=out.png \
"file://$PWD/grafik.html"
Drei davon sind die heimlichen Helden:
--force-color-profile=srgb– ohne das rendert Chrome auf einem Mac mit P3-Display andere Farben als auf einem Linux-Server. Dein Markenrot ist dann plötzlich nicht mehr dasselbe Rot. Das Flag zwingt überall denselben Farbraum.--hide-scrollbars– sonst fotografiert Chrome bei knappem Layout die Scrollbalken mit ins Bild.--default-background-color=00000000– die acht Nullen sind RGBA-Hex für vollständig transparent. Praktisch für Logos und Grafiken mit Freisteller.
Eine Bühne, viele Assets
Jetzt wird es interessant. Statt für jede Grafik eine eigene HTML-Datei zu pflegen, baut man eine Datei, die alle Kacheln enthält – als Galerie zum Anschauen –, und exportiert per URL-Parameter gezielt eine einzelne davon.
Im HTML bekommt jede exportierbare Fläche Metadaten als data-Attribute:
<div class="canvas post" data-name="post_01" data-w="1080" data-h="1080">
... Inhalt ...
</div>
Ein kleines Script schaltet beim Export per ?export=N in den Einzelmodus: Es blendet alles aus außer der gewählten Kachel und setzt sie randlos nach oben links.
const params = new URLSearchParams(location.search);
// Ohne ?export: normale Galerie-Ansicht, nichts tun.
if (params.has('export')) {
const idx = parseInt(params.get('export'), 10);
const target = document.querySelectorAll('.canvas')[idx];
// alles weg außer der Zielkachel, randlos
const style = document.createElement('style');
style.textContent = `
body { margin:0 !important; padding:0 !important; }
.canvas { display:none !important; }
.canvas.__export { display:flex !important; position:absolute;
top:0; left:0; }
`;
document.head.appendChild(style);
target.classList.add('__export');
}
So sieht man im Browser alle Grafiken nebeneinander – aber der Export liefert jede einzeln, pixelgenau in ihrer echten Größe.
Das Ganze automatisieren
Jetzt fügt sich alles zusammen. Das Script liest das gerenderte DOM aus (Chrome kann das mit --dump-dom), zieht aus jedem data-name/data-w/data-h die Maße und exportiert eine Kachel nach der anderen:
URL="file://$PWD/posts.html"
# gerendertes DOM holen, pro .canvas eine Zeile "name<TAB>w<TAB>h"
META="$("$CHROME" --headless --dump-dom "$URL" \
| grep -oE 'data-name="[^"]+" data-w="[0-9]+" data-h="[0-9]+"' \
| sed -E 's/.*name="([^"]+)" data-w="([0-9]+)" data-h="([0-9]+)"/\1\t\2\t\3/')"
idx=0
while IFS=$'\t' read -r name w h; do
"$CHROME" --headless --disable-gpu --no-sandbox \
--force-color-profile=srgb --hide-scrollbars \
--window-size="${w},${h}" \
--screenshot="export/${name}.png" \
"${URL}?export=${idx}"
echo " ✓ ${name}.png (${w}×${h})"
idx=$((idx+1))
done <<< "$META"
Ein Aufruf, alle Assets fallen raus. Ändert sich das Design, ändert man das HTML/CSS einmal und exportiert neu – kein Bild von Hand nachziehen.
⚠️ Eine Falle aus eigener Erfahrung: Wenn die Kacheln per JavaScript aus einem Array erzeugt werden, taucht im
--dump-domauch der ungerenderte Template-Quelltext im<script>auf – inklusive Platzhaltern wie${n}. Dasgrep-Muster muss die rausfiltern, sonst exportiert man eine Datei namenspost_${n}.png. Lösung: im Muster das Dollarzeichen ausschließen, etwadata-name="[^"$]+". Nur echte, gerenderte Attribute passieren den Filter.
Und ein PDF?
Derselbe Mechanismus, ein anderes Flag. Statt --screenshot nimmt man --print-to-pdf:
chrome --headless --print-to-pdf=lebenslauf.pdf \
--no-pdf-header-footer \
"file://$PWD/cv.html"
Damit wird aus einer druckoptimierten HTML-Seite (siehe @page-CSS und @media print) ein sauberes PDF – wieder ohne ein einziges npm-Paket.
Kurze Übersicht
chrome --headless --screenshot=X.png → HTML zu PNG
chrome --headless --print-to-pdf=X.pdf → HTML zu PDF
Wichtige Flags:
--window-size=W,H Maße der Aufnahme
--force-color-profile=srgb konsistente Farben überall
--hide-scrollbars keine Balken im Bild
--default-background-color=0 transparenter Hintergrund (RGBA-Hex)
--dump-dom gerendertes DOM auslesen (für Automatisierung)
Pattern: eine HTML-Datei = Galerie, ?export=N isoliert eine Kachel
Nicht jedes Problem braucht eine npm-Abhängigkeit. Chrome ist ohnehin installiert, sein Headless-Modus ist eine ausgereifte Rendering-Engine, und ein paar Zeilen Bash verwandeln ihn in eine Asset-Pipeline, die man komplett versteht und besitzt. Kein node_modules, kein Versions-Roulette, kein Build-Schritt – nur HTML rein, PNG raus. Manchmal ist das beste Tool das, das schon da ist.