DEV Community

Mack
Mack

Posted on

Generate PDF Reports from HTML with Screenshots — A Developer's Toolkit

Every developer eventually faces the same request: "Can you make this exportable as a PDF?"

Whether it's invoices, dashboards, weekly reports, or certificates — the answer is always HTML-to-PDF or HTML-to-image. And it's always more painful than it should be.

Here's the toolkit I've built after doing this too many times.

The Problem

You have a beautiful HTML template. You need it as:

  • A PDF attachment in an email
  • A PNG for Slack notifications
  • A downloadable report in your app

The "obvious" solutions all have tradeoffs:

  • wkhtmltopdf: Ancient, rendering issues, deprecated
  • Puppeteer/Playwright locally: Works great... until you deploy to a server without Chrome
  • Prince XML: Excellent but expensive ($3,800/license)

The Modern Approach: API-Based Rendering

The cleanest pattern I've found is treating rendering as a service:

async function generateReport(data) {
  const html = renderTemplate('monthly-report, data);

  const response = await fetch('https://your-render-api/v1/render, {
    method: 'POST,
    headers: {
      'Content-Type: 'application/json,
      'Authorization: 'Bearer YOUR_API_KEY\n    },
    body: JSON.stringify({
      html: html,
      format: 'png,
      width: 1200,
      height: 630,
      quality: 90
    })
  });

  return response.buffer();
}
Enter fullscreen mode Exit fullscreen mode

This decouples rendering from your app server. No Chrome binary in production. No memory spikes. No zombie processes.

Building the Template

HTML/CSS gives you full control:

<div style="width: 1200px; padding: 40px; font-family: Inter, sans-serif;">
  <header style="display: flex; justify-content: space-between; margin-bottom: 30px;">
    <h1 style="color: #1a1a1a;">Monthly Report</h1>
    <span style="color: #666;">{{ month }} {{ year }}</span>
  </header>

  <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;">
    <div style="background: #f0fdf4; padding: 20px; border-radius: 8px;">
      <div style="font-size: 32px; font-weight: bold; color: #16a34a;">{{ revenue }}</div>
      <div style="color: #666;">Revenue</div>
    </div>
    <div style="background: #eff6ff; padding: 20px; border-radius: 8px;">
      <div style="font-size: 32px; font-weight: bold; color: #2563eb;">{{ users }}</div>
      <div style="color: #666;">Active Users</div>
    </div>
    <div style="background: #fef3c7; padding: 20px; border-radius: 8px;">
      <div style="font-size: 32px; font-weight: bold; color: #d97706;">{{ uptime }}</div>
      <div style="color: #666;">Uptime</div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Automating It

Pair this with a cron job and you've got automated reports:

# Rails example with Sidekiq
class WeeklyReportJob < ApplicationJob
  def perform(team_id)
    team = Team.find(team_id)
    data = ReportBuilder.new(team).weekly_stats

    image = RenderClient.html_to_image(
      template: 'weekly-report,
      data: data,
      width: 1200
    )

    SlackNotifier.post(
      channel: team.slack_channel,
      text: "Weekly report for #{Date.today.strftime('%B %d)}",
      attachments: [{ image: image }]
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

Self-Hosted vs API

Factor Self-Hosted (Playwright) Render API
Setup Install Chrome + deps API key
Maintenance You manage updates Zero
Scaling Your problem Their problem
Cost Server resources Per-render pricing
Latency ~2-5s ~1-3s (optimized)

For low volume (< 100/month), self-hosted is fine. Beyond that, the ops overhead isn't worth it.

Tools I Recommend

  • Rendly — Simple API for HTML-to-image and screenshots. Has a playground for testing templates.
  • Playwright — Best self-hosted option if you want full control
  • Gotenberg — Open-source document conversion API (Docker-based)

The Invoice Pattern

The most common use case. Here's the full flow:

const invoiceHtml = generateInvoiceHtml({
  invoiceNumber: 'INV-2026-042,
  client: 'Acme Corp,
  items: [
    { desc: 'Web Development, hours: 40, rate: 150 },
    { desc: 'Design Review, hours: 8, rate: 150 }
  ],
  total: 7200
});

const image = await renderApi.render({
  html: invoiceHtml,
  format: 'png,
  width: 800
});

await sendEmail({
  to: 'billing@acme.com,
  subject: 'Invoice INV-2026-042,
  attachments: [{ filename: 'invoice.png, content: image }]
});
Enter fullscreen mode Exit fullscreen mode

Clean, automated, professional-looking invoices without touching a PDF library.

Wrapping Up

HTML is the best layout engine we have. Use it for reports, invoices, certificates, social images — anything visual. Pair it with a rendering API and you've got a production-ready pipeline in under an hour.

The key insight: don't fight PDF libraries. Render HTML to images instead. Your designers already know HTML/CSS. Your templates are version-controlled. Everyone wins.


Building something that needs automated reports or document generation? Rendly's playground lets you test templates live before committing to an approach.

Top comments (0)