← All guides

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 flex and grid utilities 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