Generate Open Graph Preview Images from HTML with Node.js and Puppeteer
Render an HTML card with Puppeteer, save it as an image, and point your page’s Open Graph metadata at the generated file.
Use Puppeteer to render a dedicated HTML card in Chromium, wait for the assets the design depends on, and save a screenshot. Then publish that image at a stable, publicly reachable URL and put the absolute URL in your page’s og:image metadata. Include og:image:alt as well. The Open Graph protocol defines these metadata properties; it does not establish one universal image size, so choose dimensions for the platform and layout you are targeting.
1. Set up a predictable card
A share image is easier to generate reliably from a small, dedicated template than from an arbitrary page. Set the viewport to the card’s intended dimensions, use a solid background, and keep important content away from the edges. For example, this guide uses 1200 × 630 pixels as an illustrative design size, not a universal platform requirement.
Create a project and install Puppeteer:
mkdir og-image-generator
cd og-image-generator
npm init -y
npm install puppeteer
Puppeteer downloads a compatible browser as part of its standard installation. In restricted build environments, follow Puppeteer’s official installation guidance for browser download and runtime dependencies. Keep the Puppeteer package and browser installation aligned.
2. Render HTML and save the image
This complete Node.js example writes a PNG from inline HTML. It waits for the document to load, for fonts to finish loading, and for images in the template to decode before taking the screenshot. The HTML is intentionally self-contained; if you use remote assets or application data, add readiness checks for those specific dependencies.
// generate-og.js
const puppeteer = require('puppeteer');
const html = `
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; height: 100%; }
body {
display: grid;
place-items: center;
background: #101827;
color: #fff;
font-family: Arial, sans-serif;
}
.card {
width: 100%;
height: 100%;
padding: 72px;
display: flex;
flex-direction: column;
justify-content: space-between;
background: linear-gradient(135deg, #172554, #0f766e);
}
.eyebrow { font-size: 22px; letter-spacing: .12em; text-transform: uppercase; }
h1 { max-width: 960px; margin: 24px 0; font-size: 68px; line-height: 1.05; }
p { margin: 0; font-size: 26px; color: #dbeafe; }
</style>
</head>
<body>
<main class="card">
<div class="eyebrow">Engineering guide</div>
<div>
<h1>Generate preview images from HTML</h1>
<p>A repeatable workflow with Node.js and Puppeteer</p>
</div>
<p>example.com</p>
</main>
</body>
</html>`;
async function main() {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1200, height: 630 },
deviceScaleFactor: 1,
});
await page.setContent(html, { waitUntil: 'load' });
await page.evaluate(async () => {
if (document.fonts?.ready) await document.fonts.ready;
await Promise.all(
Array.from(document.images, (img) =>
img.decode().catch(() => undefined)
)
);
});
await page.screenshot({ path: 'og-image.png', type: 'png' });
console.log('Wrote og-image.png');
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Run it with node generate-og.js. Puppeteer’s Page API documents setContent(); its screenshots guide demonstrates navigation, screenshot saving, and browser cleanup. The example is a documented workflow pattern, not a claim that it was run or tested here.
3. Choose the right capture boundary and format
Use the capture mode that matches the design rather than taking a full-page screenshot by default.
| Option | Use | Notes |
|---|---|---|
path |
Save the image to a file. | If omitted, screenshot data is returned instead of being written to a path. |
type |
Select png, jpeg, or webp where supported. |
PNG is the documented default; a recognized filename extension can also determine the type. |
quality |
Set lossy image quality. | Range is 0–100; it does not apply to PNG. |
fullPage |
Capture the entire document. | Defaults to false. Usually a fixed share card should capture its viewport or element instead. |
clip |
Capture a specific rectangle. | Set its x/y position and width/height when the desired region is not already a standalone element. |
omitBackground |
Make the default page background transparent. | Useful for compositing; confirm that the target format and consuming platform preserve transparency. |
These options are described in Puppeteer’s ScreenshotOptions reference. To capture only one card on an existing page, locate it and screenshot its element:
const card = await page.waitForSelector('[data-og-card]');
if (!card) throw new Error('OG card was not found');
await card.screenshot({ path: 'og-image.png', type: 'png' });
Element screenshots follow the element’s bounds. Use viewport capture for a fixed canvas and fullPage: true only for a genuinely tall output. For retina output, increase deviceScaleFactor and account for the resulting pixel dimensions and file size; CSS viewport dimensions and resulting bitmap dimensions are not the same when the scale factor exceeds one.
4. Wait for the design’s actual dependencies
For a remote page, navigate and take the screenshot after navigation and application-specific readiness conditions:
await page.goto('https://example.com/share-card', {
waitUntil: 'networkidle2',
});
await page.waitForSelector('[data-og-card][data-ready="true"]');
await page.screenshot({ path: 'og-image.png' });
Puppeteer’s guide uses networkidle2 as a navigation example. It is a useful starting point, not proof that every font, image, animation, or asynchronous component is ready. Prefer a template-owned ready marker or explicit checks for required assets. A page with analytics, polling, or persistent connections may never become network-idle; in that case wait for the actual card content rather than relying on network quiet.
- Fonts: wait for
document.fonts.readywhen web fonts affect line breaks or layout. - Images: wait for each required image to load or decode; check
naturalWidthif a broken asset should fail the job. - Application state: expose a readiness selector after data and layout are complete.
- Animations: disable or pause them in a screenshot-specific stylesheet if the captured frame must be deterministic.
- Remote resources: make sure the browser process can reach them and that the URLs remain valid from the capture environment.
5. Publish the image and add Open Graph metadata
Deploy the generated file to a stable URL that the relevant crawlers can fetch. Then add metadata to the page being shared. Use an absolute URL and make the intended image the first value if the document has repeated Open Graph properties.
<meta property="og:image" content="https://example.com/generated/og-image.png">
<meta property="og:image:alt" content="A guide to generating preview images from HTML">
<meta property="og:image:type" content="image/png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
The dimensions above match the illustrative template in this article; they are not a universal size rule. The Open Graph protocol defines og:image and optional image properties including type, width, height, secure URL, and alt text. It recommends supplying og:image:alt when an image is specified. When repeated tags conflict, the first value is preferred; see the protocol repository text.
Check the final image at its public URL and inspect the rendered metadata on the actual page. Sharing systems can cache previews, so after changing an image, verify the behavior with the target platform’s current tools and documentation. Platform-specific crawler rules and size recommendations can change.
6. Automate generation safely
For a build pipeline or content service, treat each render as a job with bounded inputs and cleanup:
- Validate the title, description, theme, and any image URLs before constructing HTML.
- Escape user-provided values or use DOM APIs; do not interpolate untrusted input into executable markup.
- Use a fixed viewport, template version, and output filename so a rebuild is reproducible.
- Set a job timeout and close the page and browser in cleanup paths.
- Write to a temporary file, then publish or rename it only after the screenshot succeeds.
- Record failures with the page URL or template identifier and the failing readiness condition, while avoiding secrets in logs.
When processing many cards, reusing a browser process can avoid repeated startup work, but create isolated pages and close each page when done. Limit concurrent renders to the memory and CPU available in the deployment environment. Do not assume one browser configuration or concurrency level is optimal for every workload.
7. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser fails to launch | Missing system libraries, incompatible browser install, or sandbox restrictions. | Install the runtime dependencies required by Puppeteer’s browser setup, keep package and browser versions aligned, and inspect the launch error. Avoid disabling browser security as a blanket fix. |
| Screenshot is blank or content is missing | Capture happened before app rendering, image loading, or font loading completed. | Add a template readiness selector and explicit font/image checks; verify that the expected content exists before capture. |
| Layout differs between runs | Different viewport or device scale, late-loading fonts, animations, or time-dependent content. | Fix viewport and scale, wait for fonts and application readiness, and freeze dynamic content in the template when appropriate. |
| Image is clipped or unexpectedly tall | Viewport, element bounds, or full-page mode does not match the intended card. | Use the card’s exact dimensions, element screenshot, or a deliberate clip rectangle; omit fullPage for fixed cards. |
| Remote page navigation times out | Network dependency is slow, blocked, or never idle due to persistent traffic. | Check network access and URL, wait for a specific selector instead of global idle where suitable, and use a bounded timeout with actionable error logging. |
| Image file exists but social preview is absent | Metadata is missing or points to a private, relative, stale, or inaccessible URL. | Use a public absolute URL, verify the response and metadata, ensure the intended image tag is first, and recheck the target platform’s crawler guidance. |
| Transparent output has a solid background | The page itself paints a background or the chosen format/consumer does not preserve alpha. | Make the page background transparent, set omitBackground: true, and use a format and destination that support transparency. |
8. Performance, reliability, and cost
Self-hosted Puppeteer gives you control over the HTML, browser viewport, and screenshot options, while making your application responsible for the browser runtime, asset access, job isolation, and cleanup. Browser startup and page rendering consume compute and memory; repeated or concurrent work should be measured in the deployment environment rather than estimated from an unsupported benchmark. Cache a generated image when its template inputs have not changed, and regenerate when those inputs or the template version changes.
For reliability, use explicit readiness conditions, timeouts, a temporary output followed by publish-on-success, and cleanup in finally. A network-idle event alone cannot guarantee a correct design. For cost, include the compute and storage used by your own deployment in the estimate; no universal per-image cost follows from Puppeteer’s API documentation.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a screenshot or PDF. For this card URL, call the API like this (see the ScreenshotNeo API docs):
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/share-card -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/share-card"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/share-card' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, newsletter popups, and chat widgets from more than 60 known consent platforms are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Response headers report the page verdict and billing status.
- An MCP server lets Claude, Cursor, and other MCP clients use screenshot, page-info, and PDF tools.
- The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Every feature is on every plan.
Sign up free for 1,000 screenshots a month, with no card.
FAQ
Does Open Graph require a particular image size?
The protocol properties covered here do not define a universal pixel size. Use the current guidance for the platform you are targeting and design the card for that output.
Should I screenshot the whole website?
Usually not for a share card. A dedicated fixed-size template or a target card element makes the output boundary predictable.
Can I generate the image without writing it to disk?
Yes. Omit the screenshot path and use the returned image data in your storage or publishing flow.
Why can networkidle2 still produce an incomplete image?
Network quiet does not establish that every application component, font, or image has reached the state your design needs. Wait for the template’s explicit readiness condition and required assets.
Which format should I use?
PNG is Puppeteer’s documented default and preserves lossless output. JPEG and WebP are alternatives where supported; choose based on the target platform’s current support and your image’s content.


