safermobility/laravel-gotenberg
A Laravel wrapper for Gotenberg that simplifies generating PDFs from HTML, URLs, Markdown, and Office documents. It offers a fluent, configurable API for sending content to your Gotenberg service and returning polished PDF output.
This is a thin Laravel wrapper around Gotenberg (via the gotenberg/gotenberg-php client) that exposes a Gotenberg facade for turning HTML, URLs, and Markdown into PDFs. You still need a running Gotenberg container — the package is the client, not the renderer.
composer require safermobility/laravel-gotenberg
php artisan vendor:publish --tag="gotenberg-config"
GOTENBERG_HOST=http://gotenberg:3000
docker run --rm -p 3000:3000 gotenberg/gotenberg:8
curl http://localhost:3000/health should return {"status":"up"}. Most "package is broken" reports are actually an unreachable container or a wrong port.use Safermobility\LaravelGotenberg\Facades\Gotenberg;
$html = view('invoices.pdf', ['invoice' => $invoice])->render();
Gotenberg::html()
->html($html)
->save(storage_path("app/invoices/{$invoice->id}.pdf"));
Note this release (v1.0.3) specifically fixes saving the PDF to the local filesystem. If you are starting fresh, require ^1.0.3 rather than an earlier 1.0.x — save() to a local path is the exact code path that was broken.
Wrap generation in a service or action, not in controllers. The facade returns a builder; keeping the chain in one class makes it easy to swap paper size, margins, or the whole renderer later.
class InvoicePdf
{
public function render(Invoice $invoice): string
{
$path = storage_path("app/invoices/{$invoice->id}.pdf");
Gotenberg::html()
->html(view('invoices.pdf', compact('invoice'))->render())
->paperSize(21, 29.7) // A4, in cm — verify units against the client
->margins(1, 1, 1, 1)
->save($path);
return $path;
}
}
Push it onto a queue. PDF rendering is I/O-bound and slow (hundreds of ms to several seconds). A ShouldQueue job with an explicit $timeout and $tries keeps web requests fast and gives you retries when Gotenberg is temporarily down. Write the file in the job, then dispatch a follow-up job to email or upload it.
Bridge to Laravel Storage. The package writes to a filesystem path, so the usual pattern is: render to a local temp file, then hand off to Storage. This keeps S3/GCS credentials out of the Gotenberg call entirely.
$tmp = tempnam(sys_get_temp_dir(), 'pdf_');
Gotenberg::html()->html($html)->save($tmp);
Storage::disk('s3')->put("invoices/{$id}.pdf", file_get_contents($tmp));
unlink($tmp);
Pick the right source type. HTML is best for Blade-rendered documents (invoices, statements, certificates). Markdown suits generated reports. URL rendering is best for archiving existing pages — but remember Gotenberg fetches it server-side, so internal/intranet URLs only work if the container can reach them.
Bundle your assets. CSS, fonts, and images referenced by relative URL won't resolve inside the container. Either inline everything (base64 data URIs, <style> blocks) or pass the assets explicitly so Gotenberg gets them in the same request. Base64 inlining is the most predictable approach for a handful of files.
Test against a real container. Unit-testing the facade only proves you called the builder. Put Gotenberg in your CI as a service container and assert on the resulting bytes:
services:
gotenberg:
image: gotenberg/gotenberg:8
ports: ['3000:3000']
then assert the file exists and starts with %PDF-. For fast unit tests, bind a fake implementation in the container so you're not spinning up Docker for every test run.
Pin the Gotenberg image tag, not latest. The wrapper passes options through to the API, and Gotenberg 8 changed several route and option names relative to 7. An unexpected image bump is the most likely source of a sudden 400 response.
storage_path() or a temp file, upgrade — the fix is the whole point of the release. It's a patch, no breaking changes, so composer update safermobility/laravel-gotenberg is low risk.actions/checkout, git-auto-commit-action, fetch-metadata) are GitHub Actions versions in the package's own workflows. They have zero effect on your runtime.storage_path('app/...') must be writable by the PHP-FPM/queue user, not just your shell user. A "fixed" save that still fails on production is almost always a permissions or missing-directory issue — create the directory first, don't assume save() does.GOTENBERG_HOST in .env, run php artisan config:clear. Cached config in a container image is a classic silent failure.max_execution_time. If the worker kills the job first, you get a confusing "job deleted" rather than a clear timeout. Keep the worker timeout comfortably above the HTTP timeout.@font-face). Embedding is the portable fix.curl the Gotenberg health endpoint; then hit the raw route with curl and the same payload. That tells you in three steps whether the problem is your HTML, the client, or the container.gotenberg-php client from the container and call it directly rather than forking — the wrapper is thin enough that mixing the two is fine. Wrapping the facade behind your own interface also makes it trivial to swap in a different renderer later without touching call sites.How can I help you explore Laravel packages today?