How to generate PDFs in Laravel (2026)
Laravel makes it easy to render a Blade view to HTML. Turning that HTML into a PDF that looks the way you designed it is where projects get stuck. This guide covers the two packages most Laravel apps use, barryvdh/laravel-dompdf and spatie/browsershot, explains where each one falls short, and then shows how to do the same job with a single HTTP request to PDFOven using Laravel's Http facade.
Option 1: barryvdh/laravel-dompdf
The Laravel wrapper around Dompdf is the most common starting point. It is pure PHP, needs no binaries, and works anywhere Laravel runs.
composer require barryvdh/laravel-dompdf
<?php
namespace App\Http\Controllers;
use App\Models\Invoice;
use Barryvdh\DomPDF\Facade\Pdf;
class InvoicePdfController extends Controller
{
public function __invoke(Invoice $invoice)
{
$pdf = Pdf::loadView('invoices.pdf', ['invoice' => $invoice])
->setPaper('a4', 'portrait');
return $pdf->download("invoice-{$invoice->number}.pdf");
}
}
Use ->stream() instead of ->download() to display the PDF in the browser, or ->output() to get the bytes for Storage::put(). Publish the config with php artisan vendor:publish --provider="Barryvdh\DomPDF\ServiceProvider" to change defaults such as the paper size or remote asset loading.
What to know:
- Dompdf implements CSS 2.1 and parts of CSS 3. Flexbox and grid are not supported, so a Blade view styled with Tailwind's
flexandgridutilities will not lay out as it does in the browser. Tables and floats are the reliable tools. - There is no JavaScript, so charts drawn client-side will be blank.
- Custom fonts must be TTF and declared with
@font-face. Large fonts, especially CJK, increase memory use noticeably. - Long tables are slow and memory hungry because layout happens inside the PHP process, which competes with your request's memory limit.
- Keep remote asset loading disabled unless you need it, and never enable it for user-supplied HTML, or the renderer can be used to fetch internal URLs.
For simple receipts and letters, laravel-dompdf is a perfectly good choice. For anything designed in a modern browser, expect to maintain a separate, simplified stylesheet for PDFs.
Option 2: spatie/browsershot
Browsershot drives headless Chrome through Puppeteer, so the output matches Chrome's print preview, including flexbox, grid, web fonts and JavaScript.
composer require spatie/browsershot
npm install puppeteer
<?php
use App\Models\Invoice;
use Spatie\Browsershot\Browsershot;
Route::get('/invoices/{invoice}/pdf', function (Invoice $invoice) {
$html = view('invoices.pdf', ['invoice' => $invoice])->render();
$pdf = Browsershot::html($html)
->format('A4')
->margins(12, 12, 12, 12)
->showBackground()
->waitUntilNetworkIdle()
->pdf();
return response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="invoice-' . $invoice->number . '.pdf"',
]);
});
What to know:
- Every server that renders PDFs now needs Node, npm, a Chromium build and its system libraries. On Forge-provisioned servers that is a few extra provisioning steps; on Laravel Vapor (AWS Lambda) Chromium does not fit without a dedicated layer and extra configuration.
- Each call starts a Node process and a browser. That is slower than Dompdf and uses hundreds of megabytes of memory per render, so move rendering into a queued job rather than doing it inside a web request.
- Fonts must be installed at the OS level on every server, or the output will silently fall back to a different typeface.
- You own Chromium updates, crash handling and zombie processes when a render times out.
Browsershot gives the best fidelity of any self-hosted option for Laravel. The cost is operational.
The PDFOven way: the Http facade
PDFOven renders with Chromium on its own infrastructure, so you get Browsershot-level fidelity while your Laravel app only makes an HTTPS request. No Node, no Chromium, nothing to install on Vapor.
Add your key to config/services.php:
'pdfoven' => [
'key' => env('PDFOVEN_KEY'),
],
Then render the Blade view and return a download response:
<?php
namespace App\Http\Controllers;
use App\Models\Invoice;
use Illuminate\Support\Facades\Http;
class InvoicePdfController extends Controller
{
public function __invoke(Invoice $invoice)
{
$html = view('invoices.pdf', ['invoice' => $invoice])->render();
$response = Http::withToken(config('services.pdfoven.key'))
->timeout(60)
->post('https://pdfoven.com/v1/pdf', [
'html' => $html,
'options' => [
'format' => 'A4',
'margin' => '0.5in',
'print_background' => true,
'footer_template' => '<div style="font-size:9px;width:100%;text-align:center">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
],
]);
if ($response->failed()) {
$error = $response->json('error');
abort(502, "PDF render failed: {$error['code']}");
}
$filename = "invoice-{$invoice->number}.pdf";
return response()->streamDownload(
fn () => print($response->body()),
$filename,
['Content-Type' => 'application/pdf'],
);
}
}
The request body takes exactly one source: html, markdown, url, template with data, or template_id with data. Because rendering is Chromium, your Tailwind flexbox and grid layouts work unchanged. If the view draws charts with JavaScript, add 'wait_for_selector' => '#charts-ready' and set that element once the charts finish.
Saving from a queued job
For emailing invoices, render in a job and store the result yourself. PDFOven does not keep a copy of generated files.
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
public function handle(): void
{
$response = Http::withToken(config('services.pdfoven.key'))
->timeout(60)
->retry(3, 2000, fn ($e) => $e instanceof RequestException && $e->response->serverError())
->post('https://pdfoven.com/v1/pdf', [
'html' => view('invoices.pdf', ['invoice' => $this->invoice])->render(),
'options' => ['format' => 'A4', 'print_background' => true],
])
->throw();
Storage::put("invoices/{$this->invoice->number}.pdf", $response->body());
}
The retry callback only retries server errors. A 429 with code quota_exceeded means the monthly quota is used up, and retrying will not help.
Templates instead of Blade
If several apps need the same document, store a Handlebars template in PDFOven and send only data with template_id and data. Helpers such as formatCurrency, formatDate, lineTotal and sum handle the arithmetic and formatting that would otherwise live in your Blade view.
Summary
| laravel-dompdf | Browsershot | PDFOven | |
|---|---|---|---|
| Flexbox / grid | No | Yes | Yes |
| JavaScript | No | Yes | Yes |
| Server dependencies | None | Node, Chromium, fonts | None |
| Works on Vapor | Yes | With extra work | Yes |
PDFOven's free plan includes 100 renders per month; paid plans are $12, $39 and $99 for 2,000, 15,000 and 60,000 renders. For plain PHP without Laravel, see the PHP guide, and to test a Blade view's output first, paste the rendered HTML into the free HTML to PDF tool.
Skip the browser plumbing
PDFOven runs Chromium for you. Send HTML, Markdown, a URL or JSON to a template and get a PDF back. 100 free renders every month.
Get a free API key