Server-Side PDF Rendering from HTML Templates Using PHP and Node.js

Automated PDF Generation from HTML

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1285
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1241
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    982
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1033
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1104
  • image_website-_0.webp
    Website development for Red Pear
    554

Automated PDF Generation from HTML

Note: when an invoice is delayed due to manual PDF generation, the business loses money. In one project, generating 500 invoices per day took 4 hours of manual work. We automated the process: now PDFs are generated 5 minutes after the order, and clients receive them immediately. Let me share how we implement server-side PDF generation using HTML-to-PDF. Our client—an online accounting service—saved 1.2 million rubles annually by automating act of completion generation. Manual generation was costing the company 300,000 rubles monthly.

Problems We Solve

Complex layout with CSS. Many libraries don't understand Grid, Flexbox, variables. Solution: headless Chrome via Browsershot (PHP) or Puppeteer (Node.js). They render HTML like a browser—perfect for invoices, contracts, reports.

Fonts and Cyrillic. Without proper configuration, characters turn into garbage. We include Google Fonts via @import or embed local fonts. In TCPDF we use DejaVu Sans—it supports Cyrillic without issues.

Performance. Generating one PDF via browser takes 2-5 seconds. At 5000 documents per hour, that's critical. Solution: async queue (Laravel Queue / Bull) and storage in S3. The user gets an instant response, while the PDF is generated in the background.

How We Do It: Stack and Case

For a client—an online accounting service—we implemented generation of acts of completion using Laravel 11. We used spatie/browsershot (based on Puppeteer). The template was written in Blade with CSS Grid for the service table.

Laravel: Browsershot (Puppeteer)

Browsershot uses headless Chrome to render HTML to PDF—supports CSS Grid, Flexbox, variables, fonts.

use Spatie\Browsershot\Browsershot; class InvoicePdfService { public function generate(Invoice $invoice): string { $html = view('pdf.invoice', ['invoice' => $invoice])->render(); $path = storage_path("app/invoices/invoice-{$invoice->id}.pdf"); Browsershot::html($html) ->format('A4') ->margins(15, 15, 15, 15) // mm ->showBackground() ->emulateMedia('print') ->waitUntilNetworkIdle() // wait for fonts to load ->save($path); return $path; } } // Controller public function download(Invoice $invoice): Response { $path = $this->invoicePdfService->generate($invoice); return response()->download( $path, "invoice-{$invoice->number}.pdf", ['Content-Type' => 'application/pdf'] ); } 
<!-- resources/views/pdf/invoice.blade.php --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap'); * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Inter', sans-serif; font-size: 12px; color: #1a1a1a; } .header { display: flex; justify-content: space-between; margin-bottom: 40px; } .invoice-number { font-size: 24px; font-weight: 700; } table { width: 100%; border-collapse: collapse; margin-top: 20px; } th { background: #f3f4f6; padding: 8px; text-align: left; font-weight: 600; } td { padding: 8px; border-bottom: 1px solid #e5e7eb; } .total { font-size: 16px; font-weight: 700; text-align: right; margin-top: 20px; } @media print { .page-break { page-break-after: always; } } </style> </head> <body> <div class="header"> <div> <img src="{{ public_path('logo.png') }}" height="40"> <div>{{ $invoice->company->name }}</div> </div> <div> <div class="invoice-number">Invoice #{{ $invoice->number }}</div> <div>Date: {{ $invoice->date->format('d.m.Y') }}</div> </div> </div> <table> <thead> <tr><th>Description</th><th>Qty</th><th>Price</th><th>Total</th></tr> </thead> <tbody> @foreach($invoice->items as $item) <tr> <td>{{ $item->description }}</td> <td>{{ $item->quantity }}</td> <td>{{ number_format($item->price, 2) }} ₽</td> <td>{{ number_format($item->total, 2) }} ₽</td> </tr> @endforeach </tbody> </table> <div class="total">Total: {{ number_format($invoice->total, 2) }} ₽</div> </body> </html> 

Node.js: Puppeteer

import puppeteer from 'puppeteer'; import Handlebars from 'handlebars'; async function generateInvoicePdf(invoice: Invoice): Promise<Buffer> { const templateSource = await fs.readFile('./templates/invoice.html', 'utf-8'); const template = Handlebars.compile(templateSource); const html = template(invoice); const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); try { const page = await browser.newPage(); await page.setContent(html, { waitUntil: 'networkidle0' }); return await page.pdf({ format: 'A4', margin: { top: '15mm', right: '15mm', bottom: '15mm', left: '15mm' }, printBackground: true, }); } finally { await browser.close(); } } 

TCPDF: PHP-native generation (no browser)

Suitable for simple documents without complex CSS:

use TCPDF; class ContractPdfService { public function generate(Contract $contract): string { $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8'); $pdf->SetCreator('MyApp'); $pdf->SetAuthor($contract->company->name); $pdf->SetTitle('Contract No. ' . $contract->number); $pdf->SetFont('dejavusans', '', 10); $pdf->AddPage(); $html = view('pdf.contract-simple', compact('contract'))->render(); $pdf->writeHTML($html, true, false, true, false, ''); $path = storage_path("app/contracts/contract-{$contract->id}.pdf"); $pdf->Output($path, 'F'); return $path; } } 

Async generation in a queue

class GenerateInvoicePdfJob implements ShouldQueue { public int $timeout = 120; public function __construct(private Invoice $invoice) {} public function handle(InvoicePdfService $service): void { $path = $service->generate($this->invoice); // Upload to S3 $s3Key = "invoices/{$this->invoice->user_id}/{$this->invoice->id}.pdf"; Storage::disk('s3')->put($s3Key, file_get_contents($path)); $this->invoice->update(['pdf_key' => $s3Key, 'pdf_generated_at' => now()]); unlink($path); // Notify user $this->invoice->user->notify(new InvoiceReadyNotification($this->invoice)); } } 

How to Choose Between Browsershot and TCPDF?

Criteria Browsershot (Laravel) Puppeteer (Node.js) TCPDF (PHP)
CSS Grid/Flexbox Supported Supported Not supported
Rendering speed 2-5 sec 2-5 sec <1 sec
Cyrillic Via Google Fonts Via Google Fonts Built-in DejaVu
Setup complexity Medium Medium Low
Memory usage ~200 MB ~200 MB ~50 MB

Browsershot is 2x faster than TCPDF for complex layouts, but for simple tables TCPDF is more memory-efficient.

Process

  1. Analysis. Identify document types (invoices, contracts, reports). Assess layout complexity.
  2. Design. Create template in Blade or Handlebars. Configure fonts.
  3. Implementation. Write generation service, connect queue for async processing.
  4. Testing. Verify on 50+ documents. Compare size and quality.
  5. Deployment. Configure S3, CDN, monitoring (error logging).

Timelines

  • Basic generation (Browsershot/Puppeteer for one template): 2 to 3 days.
  • With async queue and S3: 3 to 4 days.
  • With digital signature: add 2 days.

What's Included

  • PDF template development incorporating corporate style.
  • Queue and cloud storage setup (S3, MinIO).
  • API generation documentation.
  • Server and repository access transfer.
  • Staff training on launch and monitoring.
  • 2-week post-release support.

Why Trust Us with This Task?

We have 10+ years of web development experience and over 50 PDF generation projects. We use only proven stacks: Laravel + Browsershot, Node.js + Puppeteer. We guarantee stability: code is tested, queue retries on failures. Get a consultation for your project—we'll evaluate it within one day.

Checklist: Typical PDF Generation Errors

Error Cause Solution
Elements overflow boundaries Missing @media print Add media query with page-break
Text sticks to edges No margins set Set margins (15 mm)
Garbage instead of Cyrillic Incorrect fonts Use DejaVu or Google Fonts
PDF size > 10 MB Large base64 images Optimize, use external links
Rendering error Unclosed HTML tags Validate HTML before generation

Set a limit on the number of pages and file size—this will prevent generation hangs. For more details, refer to the official Puppeteer documentation on pptr.dev. As per Wikipedia, PDF is a widely used document format (source: Wikipedia).

Font configuration for stable Cyrillic

In TCPDF, use the dejavusans font—it's built-in and supports Cyrillic. In Browsershot, include Google Fonts via @import and ensure the font is loaded before generation. For offline environments, embed the font locally.