How to Capture SEO Audit Screenshots with Puppeteer
Capture repeatable desktop and mobile screenshots with Puppeteer, choose reliable page waits, and pair visual evidence with the DOM data an SEO audit needs.
Use Puppeteer to set consistent viewport or device conditions before navigation, wait for the page state your audit needs, then save the visible viewport, full page, or a specific element with Page.screenshot() or ElementHandle.screenshot(). A screenshot is visual evidence of one rendered state; pair it with DOM or page data checks for titles, canonicals, robots directives, headings, and structured data.
1. Install Puppeteer and capture a full-page screenshot
Install Puppeteer in a Node.js project. The package normally downloads a compatible browser during installation. If your environment manages Chrome separately, see the deployment notes below and Puppeteer’s official documentation.
npm init -y
npm install puppeteer
Save this as capture-seo.js. Pass the page URL as the first argument. It writes a full-page PNG and a JSON file with a few useful SEO observations. Replace the example URL with a page you are authorized to inspect.
const puppeteer = require('puppeteer');
const path = require('path');
async function main() {
const url = process.argv[2] || 'https://example.com/';
const output = process.argv[3] || 'audit-desktop.png';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1365, height: 900, deviceScaleFactor: 1 });
const response = await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 45000
});
// Capture visual evidence of the entire scrollable page.
await page.screenshot({ path: output, fullPage: true, type: 'png' });
// Inspect DOM values separately: a screenshot cannot validate these.
const audit = await page.evaluate(() => ({
url: location.href,
title: document.title,
description: document.querySelector('meta[name="description"]')?.content || null,
canonical: document.querySelector('link[rel="canonical"]')?.href || null,
robots: document.querySelector('meta[name="robots"]')?.content || null,
h1: [...document.querySelectorAll('h1')].map(el => el.innerText.trim()),
status: null
}));
audit.status = response ? response.status() : null;
const jsonPath = output.replace(/\.(png|jpe?g|webp)$/i, '') + '.json';
require('fs').writeFileSync(jsonPath, JSON.stringify(audit, null, 2));
console.log(`Saved screenshot: ${path.resolve(output)}`);
console.log(`Saved page observations: ${path.resolve(jsonPath)}`);
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
Run it with node capture-seo.js https://example.com/ output.png. The script deliberately records the HTTP response status and selected rendered DOM values alongside the image. Add the checks your audit requires; do not interpret the presence of a title or canonical in this sample as a pass/fail verdict.
2. Choose a stable capture state
Before comparing pages or runs, decide what the screenshot is supposed to show and keep those conditions consistent:
- URL and redirects: record the requested URL and, when relevant, the final
page.url(). - Viewport and device: use the same width, height, device scale, and emulation settings for each comparison.
- Capture scope: choose the initial viewport, the entire scrollable page, or a specific region or element.
- Readiness condition: select a load event, selector, or explicit page condition that matches the content under review.
- Evidence naming: use filenames that identify the page, viewport, run date or audit identifier, and state, such as
product-mobile-2026-10-04-full.png.
Configure viewport or device emulation before navigation. Puppeteer notes that changing the viewport can trigger a reload in some cases, so setting it first makes the capture conditions clearer and more repeatable. See the Page API.
3. Wait for the page state your audit needs
waitUntil: 'networkidle2' is a useful starting point and appears in Puppeteer’s screenshot guide. It is not a guarantee that every client-rendered component, image, or third-party widget has finished. Long polling and analytics can also make network-idle conditions a poor fit.
When an audit depends on a particular component, wait for that component explicitly:
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForSelector('main h1', { visible: true, timeout: 15000 });
await page.screenshot({ path: 'audit-after-content.png', fullPage: true });
For a page that signals completion through an application-specific state, wait for a meaningful condition instead of adding an arbitrary sleep:
await page.waitForFunction(() => {
const root = document.querySelector('[data-page-ready="true"]');
return Boolean(root);
}, { timeout: 15000 });
Only use a selector or condition that reflects the state you want to document. A selector appearing does not prove that every below-the-fold lazy image has loaded. If lazy content matters, inspect the resulting image and use a site-specific scroll-and-wait strategy before capturing. Avoid assuming that a fixed delay makes a capture reliable across different pages or network conditions.
4. Capture a viewport, full page, region, or element
Viewport screenshot
The default is the currently visible viewport:
await page.screenshot({ path: 'audit-viewport.png' });
Full-page screenshot
Set fullPage: true to capture the full scrollable page:
await page.screenshot({ path: 'audit-full-page.png', fullPage: true });
Very tall pages create large images and can take longer to capture and store. If the evidence is about a single module, use an element capture to reduce irrelevant page area.
Clip a region
Use clip when you need a specific rectangle in page coordinates. Supply its x and y position plus width and height:
await page.screenshot({
path: 'audit-header-region.png',
clip: { x: 0, y: 0, width: 1365, height: 260 }
});
Capture a specific element
Use an element handle for evidence about one component. Puppeteer attempts to scroll a hidden element into view for an element screenshot:
const element = await page.$('main article');
if (!element) throw new Error('Audit target was not found');
await element.screenshot({ path: 'audit-article.png' });
Choose a selector that identifies the intended component reliably. A selector that matches multiple elements or changes between releases can produce inconsistent evidence.
5. Capture desktop and mobile conditions
For a mobile capture, set the viewport and mobile behavior before navigation. A viewport alone changes responsive layout dimensions; device emulation can also set a device user agent and other device properties. Puppeteer’s Page API documents viewport and device emulation.
const page = await browser.newPage();
await page.setViewport({
width: 390,
height: 844,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true
});
await page.goto(url, { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'audit-mobile.png', fullPage: true });
If you need a named device profile, use Puppeteer’s device emulation support and apply it before goto():
const devices = puppeteer.KnownDevices;
const device = devices['iPhone 13'];
if (!device) throw new Error('Device profile is unavailable in this Puppeteer version');
await page.emulate(device);
await page.goto(url, { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'audit-iphone.png', fullPage: true });
Keep the emulated device or viewport fixed across the pages you compare. A mobile screenshot documents a rendering condition; it does not establish how search engines crawl or index the page.
6. Pair visual evidence with SEO observations
A screenshot helps reviewers see layout, visible text, and rendering problems. It does not by itself establish indexability, crawlability, canonical correctness, structured-data validity, rankings, or how a search result will appear. Check those properties through the appropriate response, source or rendered DOM, and structured-data validation workflow.
Puppeteer can inspect page content and DOM values. The sample script above collects a few examples. You can also inspect rendered headings or structured data scripts:
const observations = await page.evaluate(() => ({
headings: [...document.querySelectorAll('h1, h2, h3')].map(el => ({
tag: el.tagName.toLowerCase(),
text: el.innerText.trim()
})),
jsonLd: [...document.querySelectorAll('script[type="application/ld+json"]')]
.map(script => script.textContent)
}));
For the search-specific questions around JavaScript SEO and title links, consult Google Search Central’s JavaScript SEO Basics and Influencing Title Links in Google Search. A rendered screenshot is one piece of audit evidence, not a substitute for those checks.
7. Screenshot options and output choices
Puppeteer’s ScreenshotOptions API documents capture settings. The options most relevant to audit evidence are:
| Option | Use | Notes |
|---|---|---|
path |
Save an image to a file | Omit it when you want screenshot bytes returned instead of a saved file. |
type |
Choose PNG, JPEG, or WebP where supported by the API | PNG is the documented default. Check the installed Puppeteer version for current format support. |
quality |
Set JPEG quality | Applies to JPEG output, not PNG. |
fullPage |
Capture the full scrollable page | Defaults to false. |
clip |
Capture a rectangular region | Use page-coordinate x/y and dimensions; do not combine with full-page mode unless your installed API explicitly permits the combination. |
omitBackground |
Request a transparent background where supported | Useful for isolated visuals, usually unnecessary for whole-page audit evidence. |
encoding |
Choose returned bytes as a Buffer or base64 string | Relevant when handling the screenshot in memory rather than writing a file. |
captureBeyondViewport |
Control capture outside the current viewport | Use with care; full-page capture is the clearer choice for a whole document. |
The API evolves, so confirm less common options against the version installed in your project. Keep format and options the same across comparison runs.
8. Repeat captures safely and preserve useful evidence
- Use a fixed viewport or device profile, load condition, and capture scope.
- Record the requested and final URL, timestamp or run identifier, response status, viewport, and wait condition in accompanying metadata.
- Store the screenshot and structured observations together so reviewers can trace visual evidence to the inspected page.
- Close the browser in a
finallyblock, including when navigation or capture throws. - For batch audits, create a fresh page per URL or reset page state deliberately, and limit concurrency to what the host and machine can handle.
When pages vary in load behavior, treat timeouts as per-page results rather than silently reusing a previous image. Log the URL and error so a failed capture cannot be mistaken for evidence.
9. Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
TimeoutError during goto() |
The chosen load event did not occur before the timeout; persistent requests can prevent network idle. | Use a suitable lifecycle event such as domcontentloaded, then wait for the audit-specific selector or condition. Set a considered timeout and report timeout pages as failures. |
| Screenshot is blank or content is missing | Capture ran before client rendering completed, navigation failed, or the target returned an error page. | Check the navigation response and final URL, wait for a meaningful selector, and inspect the page before capture. |
| Lazy-loaded images are absent | Images below the fold may not load until scrolled into view. | Use a deliberate scroll-and-wait strategy for pages where those images are audit evidence, then capture and review the result. |
| Mobile layout looks like desktop | Only a desktop viewport was configured, or emulation happened after navigation. | Set mobile viewport or device emulation before navigation and recapture. |
| Element screenshot fails or targets the wrong area | The selector did not match, matched an unexpected element, or the element was not ready. | Check the handle, use a stable selector, and wait for the intended element to be visible. |
| Browser fails to launch in CI | The runtime lacks required browser dependencies, has an incompatible browser executable, or has restrictive process settings. | Use Puppeteer’s supported installation/runtime setup for the environment, verify the browser and dependencies, and consult Puppeteer’s official troubleshooting guidance. |
| Output is unexpectedly large or capture is slow | A full-page image of a very long page contains many pixels. | Capture the relevant element or region, or use an appropriate compressed format when visual fidelity permits. |
| Before/after screenshots differ without a code change | Viewport, device scale, page state, dynamic content, or third-party assets changed. | Fix comparison conditions and wait state; note unavoidable dynamic content in the audit record. |
10. Performance, reliability, and cost
Each Puppeteer capture runs a browser and loads the target page, so resource use depends on the page, browser runtime, image dimensions, and concurrency. Full-page captures of very long pages can produce large outputs. Capture only the scope needed, reuse a browser process for a controlled batch when appropriate, and always close pages and browsers when finished.
Reliability comes from explicit conditions and honest failure handling: set the viewport before navigation, wait for the state the audit needs, check navigation responses, record errors, and never treat a timeout or blank result as a successful screenshot. Repeatable evidence also requires consistent device, viewport, URL, and capture scope.
Puppeteer is an open-source browser automation library; this workflow has no per-screenshot API charge from Puppeteer itself. You still need to account for the compute, browser installation, storage, and engineering time required to operate the capture system. Any hosting or infrastructure cost depends on your own deployment.
11. Or skip the browser setup
If you need screenshots without installing and maintaining a browser, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. The API accepts parameter names used by other screenshot APIs, which can make switching easier. See the ScreenshotNeo API documentation.
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()));
Cookie and consent banners are accepted like a visitor, and 60+ known consent platforms, newsletter popups, and chat widgets are removed before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are never billed. Responses identify the page verdict and billing status in headers. An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
12. FAQ
Can a screenshot prove that a page is indexed?
No. It records a rendered appearance. Indexing and crawlability require separate checks of the relevant responses, directives, and search systems.
Should I use a full-page image for every audit?
No. Use full-page output when the whole layout or below-the-fold content is evidence. Use a viewport, region, or element capture when that is all the question requires.
Does network idle mean all SEO-relevant content is ready?
No. It is a browser lifecycle condition, not a universal signal that every application component or lazy asset is complete. Wait for the state relevant to the page and audit.
Can I use the image itself to extract title and canonical values?
Not reliably. Read those values from the DOM or other appropriate page data, and retain the screenshot as separate visual evidence.


