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();
}
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>
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
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 }]
});
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)