spatie/browsershot
Browsershot renders web pages to images or PDFs using Puppeteer and headless Chrome. Accept HTML, local files, or URLs, run JavaScript, capture body HTML and triggered requests, and customize headless behavior from PHP.
Architecture fit Browsershot is a thin PHP façade over a Node/Puppeteer subprocess. There is no PHP-native rendering engine, no server, and no persistent daemon in the default mode: each call shells out, starts Chrome, renders, writes a file, and exits. That makes it an external process dependency inside your Laravel app, not a library in the conventional sense. Fit is excellent where the app already owns the rendering surface — Blade views, authenticated HTML, or internal URLs — and where output is a document artifact (invoice, statement, report, certificate, audit snapshot). Fit is poor for high-volume, low-latency, or sandboxed-hostile environments.
Integration feasibility
Composer-installable, MIT, actively maintained by Spatie, v5.x line with a documented changelog cadence and roughly weekly patch activity. The API is fluent and small: Browsershot::url(), ->html(), ->htmlFromFilePath(), ->save($path), plus bodyHtml() and triggeredRequests() for post-JS inspection. Extension points matter for us: evaluateOnNewDocument, request interception (now only engaged when needed as of 5.3.0), setNodeEnv, disableRedirects, newHeadless, media features (v5), and remote connection support (throwOnRemoteConnectionError in 5.1.0) for a hosted Chrome endpoint. Real cost is not PHP — it is provisioning Node ≥ 23-compatible Puppeteer, a Chrome/chrome-headless-shell binary, and poppler-utils for the test path.
Technical risk
file:/ checks), which tells us this class of bug recurs. Never pass user-controlled URLs without an allowlist and egress controls.chrome-headless-shell legacy-mode support added in 5.0.8 is a compatibility escape hatch, not a strategy.Key questions
Stack fit
Native Laravel/Lumen/PHP 8.x with Symfony 8 support as of 5.1.1. No framework lock-in beyond Composer. It slots cleanly behind a queued job: a controller or event dispatches GeneratePdfJob, the job builds a Blade view to HTML (or a signed temporary URL), calls Browsershot, writes to Storage::disk(), and records the artifact path. Rendering is synchronous and blocking within a worker, so it must never run in a web request path.
Migration path
Low switching cost in either direction, which is the real strategic value. Because the public API is html|url → save, we can wrap it behind our own DocumentRenderer interface on day one and swap to wkhtmltopdf, Dompdf, Gotenberg, or a hosted rendering API without touching call sites. If v2-style Chrome CLI or an existing Dompdf path is in place, the migration is mechanical: same inputs, better CSS/JS fidelity, one new runtime dependency. Do not let Browsershot:: calls leak into controllers or Blade.
Compatibility
chrome-headless-shell supported for legacy headless.pdftotext/poppler locally and in CI; plan for that in the pipeline image, not as an afterthought.Sequencing
DocumentRenderer contract + BrowsershotRenderer implementation; add config for binary paths, Node path, timeout, and remote endpoint.file:// and internal metadata IPs, disable redirects where feasible, cap timeouts and payload sizes.Maintenance Two version streams to track: the Composer package and the Node/Puppeteer/Chrome trio. The package is well-maintained (frequent patches, Dependabot-driven hygiene, Symfony 8 and PHP 8.5 test coverage), but its updates are the smaller half of the burden. Assume quarterly Chrome upgrades minimum, each requiring a golden-document regression check because PDF output is not guaranteed byte-stable across Chrome versions. Pin exact versions in the image; never float.
Support Spatie offers documentation, a changelog, and a security mailbox, but no commercial SLA for the package. Internal ownership must be explicit: one team owns the render image and the upgrade cadence. Expect to debug at the Node/Chrome layer (missing shared libs, font substitution, font hinting, sandbox flags, locale/timezone differences) rather than in PHP. Timezone and locale set in PHP do not automatically propagate to Chrome — set them explicitly via Node env.
Scaling Per-call subprocess spawning is the cost model: typical renders land in the low hundreds of milliseconds to a few seconds, dominated by Chrome startup. This does not scale horizontally within a single worker — each render needs its own headroom. Practical guidance:
Failure modes
| Failure | Symptom | Mitigation |
|---|---|---|
| Node/Puppeteer missing or version-mismatched | Immediate exception on every render | Pinned image; startup smoke test |
| Chrome sandbox/permissions (containers, AppArmor) | Silent hang or crash | Documented sandbox flags; verify in CI |
| Render timeout on heavy pages | Job failure, orphaned Chrome process | Explicit timeout, process reaping, retry with backoff |
| SSRF via user-supplied URL | Internal network access, data exposure | Allowlist, redirect blocking, egress firewall |
Local file read (file://, UNC, protocol-relative) |
Host filesystem disclosure | Stay on ≥ 5.2.1; validate input, never pass raw user HTML/URLs |
| Missing fonts / substituted fonts | Visibly wrong documents in production only | Bake font set into image; golden-document diff |
| Output drift after Chrome upgrade | Layout regressions on legally material docs | Canary render + baseline diff in release gate |
| Queue saturation | Backlog, stale documents | Dedicated queue, priority lanes, backlog alerting |
Ramp-up A competent Laravel engineer is productive in under a day at the API level — the fluent interface is genuinely simple. Operational competency takes considerably longer: the team must learn Chrome-in-container debugging, process lifecycle, and font/metrics behavior. Budget roughly one sprint end-to-end for a first production-quality integration including image, queue, and observability, and treat the render image as a maintained product artifact rather than a build detail.
How can I help you explore Laravel packages today?