Generate PDFs in Laravel with Blade Templates

If you are looking for a clean way to generate PDFs in Laravel, the best starting point is usually Blade + HTML + CSS. Instead of maintaining a browser cluster or fighting with limited PDF libraries, you can render a Blade template to HTML and send it to Doppio for PDF generation. This guide shows a practical Laravel integration with a controller, a service class, and an invoice example you can adapt to reports, certificates, statements, or contracts.

Why use Doppio for Laravel PDF generation?

Laravel developers often start with packages that render basic HTML to PDF locally. That can work for simple documents, but it becomes harder when you need modern CSS, JavaScript charts, reliable page breaks, or predictable production scaling. Doppio moves the rendering part behind an API, so your Laravel app stays focused on templates and business logic.

  • Keep using Blade templates you already know.
  • Render modern HTML and CSS with Chromium.
  • Avoid managing Chrome binaries and memory usage on your app servers.
  • Reuse the same HTML for browser previews and PDF export.
  • Support invoices, reports, receipts, quotes, and certificates with one workflow.

For many teams, the winning architecture is simple: Laravel builds the HTML, Doppio renders the PDF.

1. Create a Laravel project

If you do not already have a Laravel app, create one with Composer:

composer create-project laravel/laravel laravel-pdf-demo
cd laravel-pdf-demo

Then add your Doppio API key to .env:

DOPPIO_API_KEY=your_api_key_here

Expose it through config/services.php:

'doppio' => [
    'key' => env('DOPPIO_API_KEY'),
],

2. Create a Blade template for the PDF

Laravel makes this easy because you can design the document exactly as you design any server-rendered page. In this example, we will generate a branded invoice PDF from a Blade view stored in resources/views/pdf/invoice.blade.php.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Invoice #{{ $invoice->number }}</title>
  <style>
    @page {
      size: A4;
      margin: 20mm;
    }

    body {
      font-family: Arial, sans-serif;
      color: #1f2937;
      font-size: 14px;
    }

    .invoice-header {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      margin-bottom: 32px;
    }

    .invoice-title {
      font-size: 28px;
      font-weight: 700;
      color: #5935dd;
      margin: 0 0 8px;
    }

    table {
      width: 100%;
      border-collapse: collapse;
      margin-top: 24px;
    }

    th, td {
      border-bottom: 1px solid #e5e7eb;
      padding: 12px 8px;
      text-align: left;
    }

    th:last-child,
    td:last-child {
      text-align: right;
    }

    .totals {
      margin-top: 24px;
      width: 320px;
      margin-left: auto;
    }
  </style>
</head>
<body>
  <div class="invoice-header">
    <div>
      <p class="invoice-title">Invoice</p>
      <p>#{{ $invoice->number }}</p>
      <p>Issued on {{ $invoice->issued_at->format('M d, Y') }}</p>
    </div>
    <div>
      <strong>{{ config('app.name') }}</strong>
      <p>support@example.com</p>
    </div>
  </div>

  <h2>Bill to</h2>
  <p>{{ $invoice->customer_name }}<br>{{ $invoice->customer_email }}</p>

  <table>
    <thead>
      <tr>
        <th>Description</th>
        <th>Qty</th>
        <th>Unit price</th>
        <th>Total</th>
      </tr>
    </thead>
    <tbody>
      @foreach ($invoice->lines as $line)
        <tr>
          <td>{{ $line['description'] }}</td>
          <td>{{ $line['quantity'] }}</td>
          <td>${{ number_format($line['unit_price'], 2) }}</td>
          <td>${{ number_format($line['quantity'] * $line['unit_price'], 2) }}</td>
        </tr>
      @endforeach
    </tbody>
  </table>

  <div class="totals">
    <p><strong>Total:</strong> ${{ number_format($invoice->total, 2) }}</p>
  </div>
</body>
</html>

You can push this much further with native paged CSS. If you want running headers, page numbers, page margins, or cleaner print layouts, see our CSS Paged Media guide.

3. Create a Laravel service class for Doppio

Instead of calling the API directly from your controller, keep the integration in a dedicated service. That makes the code easier to test and reuse across invoices, reports, and account exports.

Create app/Services/DoppioPdfService.php:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use RuntimeException;

class DoppioPdfService
{
    public function renderHtml(string $html): string
    {
        $response = Http::withToken(config('services.doppio.key'))
            ->acceptJson()
            ->post('https://api.doppio.sh/v1/render/pdf/sync', [
                'page' => [
                    'setContent' => [
                        'html' => base64_encode($html),
                    ],
                    'pdf' => [
                        'format' => 'A4',
                        'printBackground' => true,
                    ],
                ],
            ]);

        if ($response->failed()) {
            throw new RuntimeException('Unable to generate PDF with Doppio.');
        }

        return $response->json('documentUrl');
    }
}

This example uses the synchronous endpoint because it is easy to understand and works well for on-demand document downloads. For high-volume flows, you can move the call into a queue job and use Doppio's asynchronous workflow.

4. Add the controller and route

Now render the Blade view to HTML, pass it to the service, and redirect the user to the generated PDF URL.

Create app/Http/Controllers/InvoicePdfController.php:

<?php

namespace App\Http\Controllers;

use App\Models\Invoice;
use App\Services\DoppioPdfService;

class InvoicePdfController extends Controller
{
    public function show(Invoice $invoice, DoppioPdfService $pdfService)
    {
        $html = view('pdf.invoice', [
            'invoice' => $invoice,
        ])->render();

        $documentUrl = $pdfService->renderHtml($html);

        return redirect()->away($documentUrl);
    }
}

Then declare the route in routes/web.php:

use App\Http\Controllers\InvoicePdfController;
use Illuminate\Support\Facades\Route;

Route::get('/invoices/{invoice}/pdf', [InvoicePdfController::class, 'show']);

5. Test the full Laravel PDF flow

Start the app as usual:

php artisan serve

Visit /invoices/1/pdf and Laravel will:

  1. Load your invoice data.
  2. Render the Blade template to HTML.
  3. Send the HTML to Doppio.
  4. Return a ready-to-download PDF document.

6. Add production-friendly improvements

Once the basic integration works, most teams want the same few upgrades:

  • Move generation into a queue job for large reports or batch exports.
  • Persist the returned PDF URL or your own storage URL on the model.
  • Cache generated PDFs when the document does not change often.
  • Use dedicated Blade partials for headers, totals, and reusable page sections.
  • Apply print-specific CSS to avoid broken tables and awkward page breaks.

7. Laravel PDF best practices

If your goal is a strong production setup, these patterns usually pay off quickly:

Use Blade as your document source of truth

Do not build separate PDF markup unless you really need to. A dedicated Blade template or layout keeps the rendering logic close to Laravel and makes ongoing maintenance much easier.

Prefer HTML and CSS over canvas-heavy PDFs

Text-based HTML documents are easier to maintain, easier to internationalize, and better for consistency. They also map well to invoices, statements, offer letters, and financial documents.

Use paged CSS intentionally

For better print output, define page size, margins, page breaks, and repeated layout rules up front. Doppio is especially useful when you need modern rendering and want to avoid old HTML-to-PDF compromises.

When to use this Laravel + Doppio pattern

This approach is a strong fit if you need:

  • Laravel invoice PDF generation from dynamic data
  • HTML-to-PDF export from authenticated business pages
  • PDF reports generated from jobs, admin panels, or customer actions
  • A cleaner alternative to running Puppeteer or Chrome inside your Laravel stack

Conclusion

For Laravel teams, PDF generation does not need to become its own infrastructure project. Render a Blade template, send the HTML to Doppio, and keep the rest of the workflow inside Laravel where it belongs. It is a simple architecture, easy to explain, and easy to scale.

If you also work with other frameworks, you may want to read our related guides on Python PDF invoices and Next.js PDF generation on Vercel.

FAQ

Can I generate PDFs from a private Laravel page?

Yes. One common pattern is to render the Blade view server-side inside Laravel, then send the HTML directly to Doppio instead of exposing a public URL.

Should I use queues for PDF generation in Laravel?

For single user-triggered documents, synchronous generation can be enough. For batch exports, reports, or high-traffic workloads, queues are usually the better choice.

Can I generate more than invoices?

Absolutely. The same setup works for quotes, account statements, contracts, certificates, shipping documents, and internal reports.

Generate PDFs in Laravel with Blade Templates