Create WhatsApp Link Preview Thumbnails from Webpages with Node.js Puppeteer
Use Puppeteer to create a webpage screenshot, host it publicly, and connect it to WhatsApp previews with Open Graph metadata.
Short answer: Puppeteer can render a webpage and save its pixels as a PNG, JPEG, or WebP image. To associate that image with a WhatsApp link preview, host it at a publicly fetchable absolute URL and add that URL as og:image in the page’s HTML head. A screenshot alone does not set the preview; WhatsApp reads page metadata and decides whether to show a card.
If the page already has a suitable social image, you may not need Puppeteer at all: check its public og:image and the other metadata first. Create a screenshot when the current page itself is the image you want to present.
1. Understand the screenshot-to-preview flow
There are two separate jobs:
- Image generation: Puppeteer opens the webpage in a browser and captures the full page or a selected element.
- Preview discovery: The webpage advertises the resulting image URL through Open Graph metadata. WhatsApp or the sending integration fetches the page and may display a preview.
This distinction helps diagnose failures. A valid image file proves that capture worked; it does not prove that WhatsApp fetched the page metadata, could retrieve the image, or supports a preview for the message type and client being used.
2. Install Puppeteer and capture the webpage
Use a current Node.js installation and install Puppeteer in your project. Puppeteer’s screenshot guide documents Page.screenshot() for a page and ElementHandle.screenshot() for an element. The example below captures the page viewport after navigation and saves a PNG.
npm install puppeteer
// capture.mjs
import puppeteer from 'puppeteer';
const targetUrl = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1200, height: 630 },
deviceScaleFactor: 1,
});
const response = await page.goto(targetUrl, {
waitUntil: 'networkidle2',
timeout: 45_000,
});
if (!response) {
throw new Error('Navigation returned no HTTP response');
}
if (!response.ok()) {
throw new Error(`Navigation failed: HTTP ${response.status()}`);
}
await page.screenshot({
path: 'whatsapp-preview.png',
type: 'png',
});
console.log('Saved whatsapp-preview.png');
} finally {
await browser.close();
}
node capture.mjs https://example.com
networkidle2 is the readiness condition used in Puppeteer’s screenshot example, but it is not a universal signal that a page is visually complete. Analytics, chat, polling, and other long-running requests can keep a page active. For a page with a known main element, wait for that element instead:
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 45_000 });
await page.waitForSelector('main article', { timeout: 15_000 });
await page.screenshot({ path: 'whatsapp-preview.png', type: 'png' });
For a page that needs a short animation or client-side render to settle, use a bounded delay after the relevant readiness condition. Avoid unbounded waits: they make capture jobs slow and prone to hanging.
3. Choose full-page or element capture
| Capture | Use it when | Trade-off |
|---|---|---|
| Viewport screenshot | The important content fits in a designed browser viewport. | Content outside the viewport is omitted. |
| Full page | The full document is the intended image. | Long pages can produce a very tall image that is unsuitable for a compact preview. |
| Element screenshot | A hero, chart, or card is the intended thumbnail. | The selector must resolve to a visible, stable element before capture. |
For an element capture, wait for the target and use its handle:
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 45_000 });
const hero = await page.waitForSelector('.hero', { timeout: 15_000 });
if (!hero) throw new Error('The .hero element was not found');
await hero.screenshot({ path: 'whatsapp-preview.png', type: 'png' });
For a full-page image, change the screenshot call to await page.screenshot({ path: 'whatsapp-preview.png', fullPage: true }). A tall full-page capture may need cropping or a deliberate element capture so the subject remains legible in the preview.
4. Publish the image and add Open Graph metadata
Upload the generated image to a location that serves it publicly over HTTPS, then use its absolute URL in the source page’s <head>. The WhatsApp developer guidance summarized in the linked reference calls for nonempty og:title, og:description, og:url, and og:image. The general Open Graph protocol lists og:title, og:type, og:image, and og:url as its four required basic properties.
<head>
<title>Example article</title>
<meta property="og:title" content="Example article">
<meta property="og:description" content="A concise summary of the article.">
<meta property="og:type" content="article">
<meta property="og:url" content="https://example.com/articles/example">
<meta property="og:image" content="https://cdn.example.com/previews/example.png">
<meta property="og:image:alt" content="Illustration representing the example article">
</head>
Use the canonical page URL for og:url, and make the image URL absolute rather than a relative path. Put the tags in the initial HTML response, not only in client-side JavaScript. The WhatsApp guidance says the metadata head should occur within the first 300 KB of HTML. It describes a preview image under 600 KB, at least 300 pixels wide, and with a width-to-height ratio no greater than 4:1. Treat these as constraints from that WhatsApp guidance, not as a guarantee of display. The cited WhatsApp guidance is preserved through a third-party mirror; consult the linked [WhatsApp preview guidance](https://chatarchitect.com/en/blog/whatsapp-link-previews-meta-tags-2025) for its context. The general [Open Graph protocol](https://ogp.me/) also describes image properties such as width, height, type, secure URL, and alt text.
5. Check the delivered HTML and image
- Request the exact page URL you intend to share, including its final redirect destination.
- Inspect the returned HTML source and confirm the Open Graph tags are present, nonempty, and in the head near the beginning.
- Open the absolute
og:imageURL without being logged in. Confirm it returns the image rather than an HTML error, login page, or expired signed URL. - Check image dimensions, file size, and aspect ratio against the WhatsApp guidance above.
- Send a permitted test using the actual WhatsApp client or business integration and message type that matters. Record the exact URL, sending tool, client, and time if the preview is absent.
Correct metadata is necessary for discovery, but no page setting can guarantee that every WhatsApp client or integration displays a card. A manual paste into a personal chat does not establish what a separate business integration supports.
6. Troubleshoot missing or incorrect previews
| Symptom | Likely cause | What to check or change |
|---|---|---|
| No card appears | The exact page response lacks metadata, the integration does not support previews for that message type, or the client did not display the fetched result. | Check the final URL and served HTML, then test through the same client or integration and message type. |
| Card appears without an image | og:image is missing, relative, inaccessible, or points to a non-image response. |
Use an absolute public URL and request the image directly to confirm its response. |
| Old or unexpected image appears | The shared URL redirects or the metadata points to a different image than expected. | Inspect the final page URL and its raw metadata; verify the precise image URL it returns. |
| Image is rejected or omitted | It may exceed the stated size, be too narrow, have an excessive aspect ratio, or require authentication. | Check that it is under 600 KB, at least 300 pixels wide, no more than 4:1 in width-to-height ratio, and publicly retrievable. |
| Puppeteer times out at navigation | The site keeps connections open, responds slowly, or blocks automated browsing. | Use an appropriate readiness condition such as domcontentloaded, then wait for a specific selector with a timeout. Do not assume a screenshot bypasses access controls. |
| Screenshot is blank or incomplete | The capture happened before client rendering, images, or fonts were ready, or the chosen selector did not represent the intended content. | Wait for a meaningful selector or a bounded settling period, and inspect the saved file locally. |
| Element screenshot says element not found | The selector is wrong or the element is inserted only after interaction or delayed rendering. | Inspect the page DOM, wait for the correct selector, and handle the missing-element case explicitly. |
Do not rely on an assumed crawler user agent, cache lifetime, or cache invalidation procedure. The available guidance does not verify those details. If the issue is specific to an integration, preserve the received card or its absence along with the URL, client, sender, and timestamp when escalating.
7. Performance, reliability, and cost
- Browser lifecycle: Reuse a browser process for batches of captures where practical, but create and close pages predictably. Always close the browser in a
finallyblock for one-shot scripts. - Wait strategy: Waiting for a specific content selector can avoid delays caused by unrelated background requests. Keep navigation and selector timeouts bounded and report failures clearly.
- Image size: Select dimensions and format for the actual card design, then inspect the resulting file against the size and aspect-ratio guidance. The guidance gives limits, not a benchmark for capture speed or display.
- Reliability: Treat navigation status, missing selectors, inaccessible image URLs, and metadata omissions as separate failure points. A successful screenshot does not validate delivery or WhatsApp rendering.
- Cost: A self-hosted Puppeteer workflow has no per-shot ScreenshotNeo fee, but it uses your compute, storage, hosting, and maintenance. If browser setup is the costly part, ScreenshotNeo is a managed screenshot API; its listed plans and billing details are below.
8. Or skip the browser setup
[ScreenshotNeo](https://screenshotneo.com) is a website screenshot API and MCP server from Yorker Media. It does not replace the need to publish an image URL and add og:image to your page; it handles the screenshot capture step with one request.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: HTTP ${res.status}`);
await Bun.write('shot.webp', res);
See the ScreenshotNeo API documentation for request options. Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed; responses identify the page verdict and billing status. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Starter is $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free, and every feature is on every plan. Sign up for 1,000 free screenshots a month, with no card.
9. Frequently asked questions
Can Puppeteer generate an Open Graph image?
It can generate the image file by capturing rendered page pixels. Your page’s metadata must still reference that file through og:image; Puppeteer does not control WhatsApp’s card rendering.
Does a screenshot have to be the same image as the page’s social image?
No. Use a screenshot only when it communicates the page well as a thumbnail. A deliberately designed social image may be a better fit than a full-page capture.
Why does a link open correctly but show no preview?
Page navigation and preview rendering are separate. Check the exact returned HTML and image URL, then verify support in the sending integration and message type you use.


