How to Generate Open Graph Images with Puppeteer and a Local HTML Template
Render a local HTML template into a share image with Puppeteer, from fixed viewport and asset readiness to PNG, JPEG, and WebP output.
To generate an Open Graph image from a local HTML template, create a Puppeteer page, set its viewport to your chosen image dimensions, load the template with page.setContent(), wait for the template’s assets and layout to be ready, then save page.screenshot() to a file. The example below creates a 1200 × 630 PNG. Choose dimensions using the current guidance for the destination where you will publish the image; there is no single dimension verified here for every platform.
This method is useful when your design is a fixed canvas made with HTML and CSS. The browser renders your template, and Puppeteer captures the result. Use a page screenshot for the whole canvas or an element screenshot when the design is contained in a specific element.
1. Set up Puppeteer
In a new Node.js project, install Puppeteer:
npm install puppeteer
Save the template as og-template.html. For this example, keep dependent assets local and use paths that resolve from the template’s base URL, as shown below. Puppeteer’s API can vary by version, so consult the documentation for the version installed in your project: Getting started.
2. Create the HTML template
A fixed-size canvas makes the captured bounds predictable. This sample uses CSS gradients and text, so it has no external image or font dependency:
<!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: 1200px; height: 630px; }
body {
display: grid;
place-items: center;
color: #f8fafc;
font: 700 64px/1.05 system-ui, sans-serif;
background: linear-gradient(135deg, #111827, #3730a3);
}
.card {
width: 100%;
height: 100%;
padding: 72px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.eyebrow { font-size: 22px; letter-spacing: .12em; text-transform: uppercase; }
h1 { max-width: 950px; margin: 0; }
.site { font-size: 24px; font-weight: 500; }
</style>
</head>
<body>
<main class="card">
<div class="eyebrow">Engineering guide</div>
<h1>A title worth sharing</h1>
<div class="site">example.com</div>
</main>
</body>
</html>
3. Render the template and save a screenshot
Save the following as generate-og.js. Run it with node generate-og.js. The script sets the viewport before rendering, reads the HTML from disk, uses setContent(), checks a concrete readiness condition, captures a PNG, and closes the browser even if rendering fails.
const fs = require('node:fs/promises');
const path = require('node:path');
const puppeteer = require('puppeteer');
async function main() {
const htmlPath = path.resolve('og-template.html');
const outputPath = path.resolve('og-image.png');
const html = await fs.readFile(htmlPath, 'utf8');
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 1 });
await page.setContent(html, { waitUntil: 'load' });
// The template itself declares when its visual content is ready.
await page.waitForFunction(() => document.fonts.status === 'loaded');
await page.waitForSelector('.card', { visible: true });
await page.screenshot({ path: outputPath, type: 'png' });
console.log(`Wrote ${outputPath}`);
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
The font check waits for the document’s font loading set to finish; it does not prove that a desired custom font loaded successfully. If your template depends on a particular font, verify that it is available and loaded before capture. For images, wait for them explicitly too. For a build with asynchronous template logic, add a readiness signal such as setting window.ogReady = true after the template finishes, then wait for window.ogReady before capture. A network-idle condition alone is not a universal guarantee that every font or image is ready.
4. Choose page or element capture
Use page.screenshot() when the page is itself the fixed-size image canvas. For a design inside one target element, wait for that element and use its screenshot method:
const card = await page.waitForSelector('.card', { visible: true });
if (!card) throw new Error('The .card element was not found');
await card.screenshot({ path: 'og-card.png', type: 'png' });
An element screenshot captures that element’s bounds, so its resulting dimensions follow the element rather than the entire viewport. Puppeteer’s screenshot guide covers both page and element capture, including waiting for a selector: Screenshots guide.
5. Select dimensions and image format
Set the viewport width and height to the output canvas dimensions. The deviceScaleFactor controls the device scale used for rendering; a value above 1 can produce more pixels for the same CSS viewport. Confirm the required aspect ratio and pixel dimensions with the platform that will display the image, since platform requirements can change.
For PNG, use a .png path and type: 'png'. Puppeteer also supports JPEG and WebP screenshots; use a matching extension and set quality where applicable:
await page.screenshot({ path: 'og-image.webp', type: 'webp', quality: 85 });
await page.screenshot({ path: 'og-image.jpg', type: 'jpeg', quality: 85 });
Use PNG for crisp text and graphics when file size is acceptable. JPEG and WebP can reduce file size, but lossy compression may soften edges. Check the actual output with the target platform’s current requirements.
6. Use local assets reliably
page.setContent() assigns markup to the page; it does not make arbitrary relative asset URLs resolve against your HTML file on disk. For a portable template, embed small assets as data URLs or convert local images and fonts to data URLs before building the HTML. For larger assets, serve the template and assets from a local HTTP server and navigate to its URL, or set a suitable base URL in the document. Make asset loading part of the readiness condition.
For example, to wait until every image in the document has either loaded or failed, evaluate its completion state:
await page.waitForFunction(() =>
[...document.images].every((image) => image.complete)
);
If a failed image should stop the build, check naturalWidth as well:
const brokenImages = await page.evaluate(() =>
[...document.images]
.filter((image) => !image.complete || image.naturalWidth === 0)
.map((image) => image.src)
);
if (brokenImages.length) {
throw new Error(`Image assets failed: ${brokenImages.join(', ')}`);
}
7. Handle dynamic templates and batches
For data-driven images, inject escaped data into the template or render the data into the DOM before capture. Do not concatenate untrusted content into executable HTML. Wait for a selector or an explicit readiness flag from the code that populates the page. Puppeteer’s page API documents content assignment, viewport, and screenshot methods: Page API and Page.setContent API.
For multiple images, reuse one browser process and create a fresh page per image or a small bounded pool of pages. Close each page when finished. Avoid launching an unbounded number of Chromium processes: concurrency increases memory use and can make output less reliable on constrained machines. If an image fails, log the input data and error, then retry only errors that may be transient. Keep deterministic template inputs so a retry produces the same design.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Output is blank or missing content | Capture happened before the template rendered, selector was wrong, or async script failed. | Wait for a visible template selector or explicit readiness flag; inspect page errors and confirm the selector exists. |
| Images or fonts are missing | Relative paths do not resolve from setContent(), the resource is inaccessible, or capture occurred before loading finished. |
Use data URLs, a local server, or a valid base URL; wait for resources and fail the build when required assets are missing. |
| Text uses a fallback font | The custom font was unavailable, failed to load, or was not ready. | Check the font URL and browser access; await font loading and verify the intended font before screenshotting. |
| Image has the wrong dimensions | Viewport or device scale differs from the intended output, or an element screenshot captured different bounds. | Set viewport dimensions and scale explicitly; use page capture for a full canvas and inspect the output dimensions. |
| Browser process hangs or fails to launch | Browser launch environment, resource limits, or an error path that leaves the process open. | Use Puppeteer’s getting-started guidance for the installed version and environment; close the browser in a finally block and bound parallel jobs. |
| Screenshot intermittently omits late content | A fixed delay or network-idle event did not represent completion of the template’s own asynchronous work. | Define readiness in the template and wait for that signal; wait for specific assets when they affect the image. |
9. Performance, reliability, and cost
Rendering locally means you manage Chromium installation, memory, concurrency, cleanup, and retries. Reuse a browser process for a batch, limit simultaneous pages, avoid unnecessary network requests, and keep templates and assets local when possible. Ensure cleanup runs on both success and failure. Pin the Puppeteer dependency in your project and check documentation matching that installed version because API behavior is version-sensitive.
The dossier provides no benchmark, so throughput and resource use depend on the template, browser environment, assets, and concurrency. Measure your own batch workload before choosing worker counts. Direct rendering has no per-image API charge, but it consumes your machine or server resources and requires you to operate the browser environment.
10. Or skip the browser setup
If the image is a screenshot of a live page, ScreenshotNeo can return an image or PDF with one GET request. The example captures a page; it does not render your custom local HTML template. See the ScreenshotNeo API documentation for options and parameter details.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month, with no card required.
FAQ
Can Puppeteer turn an HTML string into an image without saving an HTML file?
Yes. Build the markup in your Node.js program and pass the string to page.setContent(). Keep the same viewport, readiness, asset, screenshot, and browser cleanup steps.
Should I use a full-page or element screenshot?
Use a page screenshot when the viewport is the complete image canvas. Use an element screenshot when a single element defines the image bounds.
Does the example generate an image for every social platform?
It generates an image at the dimensions you set. Check the destination platform’s current image guidance when platform-specific dimensions matter.


