How to Convert HTML to Image in Node.js
You have HTML. You want an image. Maybe it's a dynamic email header, a receipt, a generated certificate, a social card. The HTML is already templated — you just need a PNG back.
The canvas approach is painful (no CSS support). The Puppeteer approach means managing a browser. Here's the direct path:
import fs from 'fs';
const html = `
<div style="
width: 800px;
padding: 48px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: Inter, sans-serif;
color: white;
border-radius: 12px;
">
<h1 style="font-size: 48px; margin: 0 0 16px;">Certificate of Completion</h1>
<p style="font-size: 24px; opacity: 0.9;">Awarded to <strong>Jane Smith</strong></p>
<p style="font-size: 18px; opacity: 0.7;">Advanced Node.js Development · February 2026</p>
</div>`;
const response = await fetch('https://api.pagebolt.dev/v1/screenshot', {
method: 'POST',
headers: {
'x-api-key': process.env.PAGEBOLT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ html, format: 'png' })
});
fs.writeFileSync('certificate.png', Buffer.from(await response.arrayBuffer()));
Pass html instead of url. The API renders it in a real browser — gradients, custom fonts, flexbox, shadows all work exactly as in a browser.
Output format
// PNG (default)
body: JSON.stringify({ html, format: 'png' })
// JPEG (smaller file, lossy)
body: JSON.stringify({ html, format: 'jpeg', quality: 90 })
// WebP (best compression)
body: JSON.stringify({ html, format: 'webp', quality: 85 })
Controlling the viewport
The rendered image dimensions match the viewport. Set it to match your HTML's intended size:
body: JSON.stringify({
html,
viewport: { width: 800, height: 400 }
})
For a component that's smaller than the viewport, use clip to capture just the element's bounding box, or design the HTML to fill the container exactly.
Custom fonts
Google Fonts work out of the box — include the <link> in your HTML:
const html = `
<html>
<head>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
<style>
body { margin: 0; }
.card {
width: 600px;
padding: 40px;
background: #0f172a;
font-family: 'Playfair Display', serif;
color: #f8fafc;
}
</style>
</head>
<body>
<div class="card">
<h1>Your receipt</h1>
<p>Order #1042 · $144.00</p>
</div>
</body>
</html>`;
const response = await fetch('https://api.pagebolt.dev/v1/screenshot', {
method: 'POST',
headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ html, viewport: { width: 600, height: 200 } })
});
In a web server (generate on request)
app.get('/certificate/:userId', async (req, res) => {
const user = await getUser(req.params.userId);
const html = `<div style="..."><h1>Certificate for ${user.name}</h1></div>`;
const upstream = await fetch('https://api.pagebolt.dev/v1/screenshot', {
method: 'POST',
headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ html, format: 'png' })
});
res.setHeader('Content-Type', 'image/png');
res.setHeader('Cache-Control', 'public, max-age=86400');
upstream.body.pipe(res);
});
Cache at the CDN layer — one render per unique user, served from cache afterward.
No canvas. No Puppeteer. Full CSS support. One fetch call.
Free tier: 100 requests/month, no credit card. → pagebolt.dev
Top comments (0)