How to Create Website Thumbnails with Puppeteer
Create website thumbnails with Puppeteer using a fixed viewport, reliable readiness checks, and the right screenshot options for your output.
Use Puppeteer’s page.screenshot() to capture a website thumbnail. Set the viewport before navigating, wait for the page or a page-specific visual target to be ready, and save the screenshot to a file. Use fullPage for the whole document, clip for a defined region, or an element handle’s screenshot() method for a single element. See Puppeteer’s screenshots guide and Page.screenshot() API.
1. Install Puppeteer and create a thumbnail
In a new Node.js project, install Puppeteer:
npm install puppeteer
Save this as thumbnail.mjs. It opens a page at a fixed desktop viewport, waits for the navigation lifecycle event, and writes a PNG to the current working directory:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'thumbnail.png' });
} finally {
await browser.close();
}
Run it with node thumbnail.mjs. Change the URL and viewport to fit your thumbnail layout. The dimensions here are an example, not a universal standard. Puppeteer recommends choosing the viewport before navigation because some sites do not expect it to change after the page loads; see Page.setViewport().
2. Choose the viewport and device scale
A thumbnail usually represents the page as it appears within a particular window, so decide its dimensions before loading the URL. Viewport width affects responsive breakpoints, while height determines how much of the page is initially visible. A smaller viewport can switch a site to its mobile layout; a taller one can show more content in a viewport-only capture.
await page.setViewport({
width: 1280,
height: 720,
deviceScaleFactor: 1
});
deviceScaleFactor controls pixel density. Use 1 for output at the viewport’s CSS-pixel dimensions. A higher scale factor produces a denser image and can increase its pixel dimensions and file size. Set this before navigation along with the viewport.
If you need a specific device profile, Puppeteer also offers device emulation APIs. Choose the emulated viewport and user agent deliberately; the resulting page may use different responsive markup than a desktop capture. For a repeatable thumbnail pipeline, keep the viewport, scale, and any emulation settings consistent between runs.
3. Wait for the page to be visually ready
page.goto() accepts a waitUntil option. It defaults to load; Puppeteer also supports lifecycle events such as domcontentloaded, networkidle0, and networkidle2. The screenshots guide uses networkidle2. See the WaitForOptions API.
These events describe browser navigation or network activity; they do not guarantee that every site’s visual content is finished. A page can load content later through client-side code, lazy loading, timers, or user interaction. When you know what must appear in the thumbnail, wait for a selector that represents that content:
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-thumbnail-ready]', { timeout: 15000 });
await page.screenshot({ path: 'thumbnail.png' });
For a known animation or delayed transition, a short fixed delay can help, but it adds the same wait to every capture whether or not the page needs it. Prefer a meaningful selector or application readiness signal when the site provides one. Avoid assuming that a generic network-idle wait will work on pages with long-polling or continuously active requests.
4. Choose what to capture
Viewport screenshot
By default, page.screenshot() captures the current viewport. This is the usual choice for a compact website thumbnail:
await page.screenshot({ path: 'thumbnail.png' });
Full-page screenshot
Set fullPage: true to capture the page beyond the visible viewport:
await page.screenshot({ path: 'full-page.png', fullPage: true });
A full-page capture can be much taller than a thumbnail and may include sections that would not appear in a preview. Long pages also create larger images and may take longer to render and store.
Clipped region
Use clip to capture a specific rectangle. Its coordinates and dimensions are in CSS pixels:
await page.screenshot({
path: 'region.png',
clip: { x: 100, y: 80, width: 800, height: 450 }
});
Make sure the requested region has positive width and height and fits the intended page area. Screenshot options document captureBeyondViewport behavior in relation to whether a clip is supplied; consult the ScreenshotOptions API when relying on that setting.
One element
To capture a card, hero, chart, or other element, locate it and call its screenshot method:
const element = await page.waitForSelector('.hero-card', { timeout: 15000 });
if (!element) throw new Error('Hero card was not found');
await element.screenshot({ path: 'hero-card.png' });
Puppeteer scrolls the element into view if needed. If the element is detached from the document before capture, the call throws. See ElementHandle.screenshot(). Waiting for the selector first also gives a clearer failure when the page never renders the target.
5. Select the image format and output
The default output is PNG. You can choose JPEG or WebP and, for formats that support it, set quality from 0 to 100. Quality does not apply to PNG:
await page.screenshot({
path: 'thumbnail.webp',
type: 'webp',
quality: 80
});
JPEG and WebP can reduce output size depending on the content and quality setting; inspect the result for artifacts if small text or sharp edges matter. Use PNG when you need lossless output. The exact supported options are documented in ScreenshotOptions.
A relative path is resolved from the process’s current working directory. If you omit path, Puppeteer returns image data instead of saving a file:
const image = await page.screenshot();
// image is screenshot data; write it to storage or return it from your application.
You can also request binary or base64 output using the documented encoding options. Be mindful that base64 adds size when embedding or transmitting an image. To capture a transparent background where the page permits it, use omitBackground: true:
await page.screenshot({ path: 'transparent.png', omitBackground: true });
6. Make a reusable capture function
For a batch or application endpoint, put browser cleanup in a finally block so it also runs after navigation or capture errors. This example takes its URL from the command line and accepts an optional output filename:
import puppeteer from 'puppeteer';
const url = process.argv[2];
const output = process.argv[3] ?? 'thumbnail.png';
if (!url) throw new Error('Usage: node thumbnail.mjs <url> [output-file]');
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 1 });
const response = await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 30000
});
if (response && !response.ok()) {
throw new Error(`Navigation returned HTTP ${response.status()}`);
}
await page.screenshot({ path: output, type: 'png' });
} finally {
await browser.close();
}
This checks the main document response, but a successful response does not prove that every image or client-rendered component succeeded. Add a page-specific readiness check for the content your thumbnail requires. To capture many pages efficiently, consider reusing a browser process and creating a fresh page per job, while ensuring one failed job does not prevent cleanup or contaminate the next capture.
7. Or skip the browser setup
If you want a screenshot without installing or operating a browser, ScreenshotNeo provides a website screenshot API. The parameter names used by other screenshot APIs also work, which can make switching easier. Read the ScreenshotNeo API documentation for the available parameters.
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 import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo accepts cookie or consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots a month without a card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo and get 1,000 free screenshots a month, no card required.
8. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Navigation times out | The page keeps making requests, is slow, or cannot be reached. | Check the URL and network access. Choose a lifecycle event suitable for the page, set an explicit timeout, and wait separately for the content your capture needs. |
| Screenshot is blank or missing content | The capture ran before client-rendered content appeared, or the site returned an error or bot check. | Wait for a page-specific selector and inspect the main document response. For a remote page, confirm it is accessible from the machine running Chromium. |
| Selector wait times out | The selector is wrong, the element is inside a frame or shadow root, or the page did not render it. | Verify the selector in the correct document context, account for frames or shadow DOM, and confirm the element appears under the same viewport and URL conditions. |
| Element screenshot reports a detached element | The page replaced or removed the element between lookup and capture. | Wait for the final element state and reacquire the handle immediately before calling screenshot(). |
| Clipped capture fails or crops unexpectedly | The clip has invalid dimensions or its coordinates do not match the intended CSS-pixel region. | Use positive width and height, check x/y against the page layout, and review captureBeyondViewport behavior in the version’s API reference. |
| Output file cannot be found | A relative path was resolved from a different working directory, or no path was supplied. | Use an absolute path or log the current working directory; provide path to save directly, or explicitly write the returned screenshot data. |
| JPEG/WebP quality has no effect | quality is not applicable to PNG. |
Select a supported lossy format such as JPEG or WebP, then set a quality value from 0 to 100. |
| Browser process remains after an error | Cleanup did not run on every code path. | Close the browser in a finally block, including when navigation or screenshot capture throws. |
9. Performance, reliability, and cost
- Reuse deliberately: Starting a browser for every URL adds setup work. For a service processing multiple URLs, reuse a browser process and isolate jobs with separate pages or contexts. Close pages and the browser when no longer needed.
- Keep readiness targeted: Waiting for a page-specific selector can avoid arbitrary extra delays. Network-idle conditions can be unsuitable for pages with ongoing requests. Set a timeout and handle its failure explicitly.
- Control output size: Viewport dimensions, device scale, full-page height, format, and quality all affect the amount of image data produced. Choose the smallest dimensions and format that meet the thumbnail’s use.
- Plan for changing pages: Remote websites can change their layout, availability, scripts, and anti-automation behavior. Use bounded timeouts, log the URL and failure stage, and treat a successful navigation response as separate from visual completeness.
- Account for your own infrastructure: A Puppeteer workflow requires a Node.js runtime and a compatible Chromium browser environment. The dossier provides no benchmark or universal per-capture cost; measure resource use and latency in your own workload.
- Consider managed capture costs: ScreenshotNeo’s stated plans are Free: 1,000 shots per month, Starter: $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 available on every plan. Compare these prices with the cost of operating your own browser workers for your workload.
10. FAQ
Does Puppeteer take the screenshot itself?
Puppeteer controls a browser page and exposes screenshot methods. The page method is page.screenshot(); individual elements have their own screenshot method.
Should a thumbnail use a full-page screenshot?
Usually a compact preview uses the viewport. Choose full-page output when the preview must represent all page sections, and account for the taller image.
Can I return the image without creating a file?
Yes. Omit path and handle the returned image data in memory or write it to your own storage.
Why does the same URL produce a different thumbnail later?
The site may have changed content, responsive layout, scripts, or loading behavior. Keep capture settings stable and wait for a meaningful page-specific readiness signal.


