DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to convert HTML to image in Node.js (PNG, JPEG, WebP)

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()));
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode

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 }
})
Enter fullscreen mode Exit fullscreen mode

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 } })
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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)