How to Bulk Screenshot Ecommerce Category Pages for a Visual Audit
Capture ecommerce category pages in a repeatable batch with Playwright. Choose viewport, full-page, or element shots and keep results comparable.
To bulk screenshot ecommerce category pages for a visual audit, keep a reviewed list of URLs, capture them with the same browser and viewport, and save each image with a stable page identifier and capture metadata. For a recurring audit or a list large enough to make manual work tedious, Playwright can automate navigation and save viewport, full-page, or selected-element screenshots. Use viewport shots to compare what shoppers first see, full-page shots to inspect below-the-fold merchandising, and element shots to focus on a product grid or filter panel.
This guide uses Node.js and Playwright. It includes a runnable batch script, ways to handle long and lazy-loaded pages, a review checklist, and a managed API option.
1. Prepare the category URL list
Start with a reviewed list rather than discovering URLs while capturing. Give each page a short, stable ID that stays the same across audit runs. Avoid using the URL itself as a filename: URLs can contain query strings, slashes, or characters that are awkward in paths.
[
{ "id": "womens-coats", "url": "https://shop.example/collections/womens-coats" },
{ "id": "mens-shoes", "url": "https://shop.example/collections/mens-shoes" }
]
Keep the list in a JSON file named categories.json. Record which pages are included and why. If filters or sorting create separate states that matter to the audit, represent each state as a separate item with its own ID and URL.
2. Choose what each screenshot should show
| Capture type | Use it for | Tradeoff |
|---|---|---|
| Viewport | Comparing navigation, introductory content, first product row, and above-the-fold merchandising. | Does not show content farther down the page. |
| Full page | Reviewing the page hierarchy, product rows, promotional blocks, and footer in one image. | Long images can be hard to inspect at natural size; lazy-loaded sections may need preparation. |
| Element | Focused comparison of a product grid, filter panel, or another stable region. | Hides the surrounding context and depends on a selector that exists on each page. |
For a useful category-page audit, consider saving both a viewport and a full-page image. Add an element capture only when the audit has a specific question about a region. Playwright documents viewport, selected-element, and full-page screenshots, with PNG, JPEG, and WebP output options. See the Playwright screenshot tools documentation and Playwright screenshot API documentation.
3. Install Playwright and run a batch capture
Use a fixed browser runtime and viewport for every URL in a run. The following Node.js script reads categories.json, saves a full-page PNG for each category, and writes a JSON index with the source URL, capture timestamp, viewport, and browser version. It is runnable after installing the dependencies.
npm init -y
npm install playwright
npx playwright install chromium
Save this as capture-categories.js next to categories.json:
const fs = require('node:fs/promises');
const path = require('node:path');
const { chromium } = require('playwright');
const pages = require('./categories.json');
const outputDir = path.join(__dirname, 'category-audit');
const viewport = { width: 1440, height: 1000 };
const navigationTimeoutMs = 45_000;
function safeId(value) {
return String(value).replace(/[^a-zA-Z0-9_-]/g, '-');
}
async function main() {
await fs.mkdir(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport });
const index = [];
try {
for (const item of pages) {
if (!item.id || !item.url) {
index.push({ id: item.id ?? null, url: item.url ?? null, status: 'skipped', error: 'Each entry needs id and url.' });
continue;
}
const page = await context.newPage();
page.setDefaultNavigationTimeout(navigationTimeoutMs);
const capturedAt = new Date().toISOString();
const filename = `${safeId(item.id)}__full-page__${capturedAt.slice(0, 10)}.png`;
const filepath = path.join(outputDir, filename);
try {
const response = await page.goto(item.url, { waitUntil: 'domcontentloaded' });
if (!response) {
throw new Error('Navigation returned no main-document response.');
}
if (!response.ok()) {
throw new Error(`Main document returned HTTP ${response.status()}.`);
}
// Replace this with a stable page-specific marker when the site provides one.
await page.locator('body').waitFor({ state: 'visible' });
await page.screenshot({ path: filepath, fullPage: true, animations: 'disabled' });
index.push({
id: item.id,
url: item.url,
capturedAt,
viewport,
browserVersion: browser.version(),
screenshot: filename,
status: 'captured'
});
} catch (error) {
index.push({
id: item.id,
url: item.url,
capturedAt,
viewport,
browserVersion: browser.version(),
screenshot: null,
status: 'failed',
error: error.message
});
} finally {
await page.close();
}
}
} finally {
await context.close();
await browser.close();
}
await fs.writeFile(path.join(outputDir, 'index.json'), JSON.stringify(index, null, 2));
const failed = index.filter((entry) => entry.status === 'failed' || entry.status === 'skipped');
console.log(`Finished: ${index.length - failed.length} captured, ${failed.length} failed or skipped. See ${outputDir}/index.json.`);
if (failed.length) process.exitCode = 1;
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Run it with node capture-categories.js. The script processes one URL at a time, which makes failures easier to attribute and avoids opening many pages against a production store at once. It records failed URLs in the index and continues through the list.
The example checks the main-document HTTP response, but it does not prove every product image or client-rendered widget finished loading. For pages with a reliable product-grid marker, replace the generic body wait with a locator wait such as await page.locator('[data-testid="product-grid"]').waitFor({ state: 'visible' }). Use a selector that actually exists on the store being captured.
4. Adapt the capture for viewport and element shots
To create a first-screen comparison, change the screenshot call to await page.screenshot({ path: filepath, fullPage: false }). You can store viewport and full-page images separately by adding the capture type to the filename, for example womens-coats__viewport__2026-10-04.png and womens-coats__full-page__2026-10-04.png.
To capture a specific region, use a locator screenshot. Confirm the selector on every page because category templates may differ:
const grid = page.locator('[data-testid="product-grid"]');
await grid.waitFor({ state: 'visible' });
await grid.screenshot({ path: filepath });
For a targeted comparison, keep the selected region and its surrounding state consistent. A grid can shift when a filter panel opens, a banner appears, or the number of columns changes.
5. Handle lazy-loaded content and page state
A full-page screenshot captures the rendered page, but some stores load product images or rows only after scrolling. If all below-the-fold items matter, scroll through the page before capture, then wait for the relevant images or page marker. The scrolling behavior is site-specific; a generic scroll loop can trigger endless loading on pages that append content as you move down. Set a maximum number of scrolls and verify a sample output before running the full batch.
async function scrollToLoad(page, maxSteps = 12) {
let previousHeight = 0;
for (let step = 0; step < maxSteps; step += 1) {
const height = await page.evaluate(() => document.documentElement.scrollHeight);
if (height === previousHeight) break;
previousHeight = height;
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
await page.waitForTimeout(400);
}
await page.evaluate(() => window.scrollTo(0, 0));
}
Call await scrollToLoad(page) after navigation and before the screenshot when that page needs it. Tune the short pause to the site and verify that images are loaded; a fixed delay alone is not proof that all network or image work has completed.
Also decide whether the audit should include a consent banner, login state, or a particular storefront region. Currency, inventory, promotions, personalization, and consent can change the page. Use a consistent state across URLs and runs, and record any relevant setup alongside the index. Do not assume that a screenshot represents every shopper’s experience: it records one rendered state at one time.
6. Make the visual audit reproducible
- Keep capture conditions fixed. Use the same viewport, browser, runtime version, device scale, and headless setting for comparable runs.
- Keep page state fixed. Record region, currency, login, consent, selected filters, sort order, and other inputs that change the rendered category.
- Use deterministic names. Include the stable category ID, capture type, and date or run ID. Keep the index mapping filenames back to URLs and notes.
- Validate representative pages. Check a short, long, image-heavy, and structurally unusual page before capturing the entire list.
- Review anomalies manually. A visual difference can come from the page, content changes, browser rendering, or changed capture conditions. A screenshot alone does not identify the cause.
Playwright notes that rendering can vary with the host operating system, browser version, settings, hardware, power source, and headless mode. It recommends using the same environment as the baseline for consistent comparisons. See Playwright’s visual comparison guidance. If the review is collaborative, individual images are easier to annotate or process; a PDF can be more convenient for sequential review.
7. Choose a capture method for the job
For recurring audits, larger URL sets, custom naming, or page-specific preparation, Playwright offers a scriptable workflow with control over viewport, full-page, and element capture. It requires a maintained browser runtime and decisions about waits, state, and failures.
For a small fixed list where coding is undesirable, a batch browser extension may be quicker. FullPage Capture states that its batch feature can process up to 200 pages per run and save separate files or a combined PDF; the vendor page says batch capture is part of Pro. These are vendor-stated details, so confirm current limits, plan requirements, and browser compatibility before depending on them: FullPage Capture batch capture.
A hosted screenshot platform can fit teams that want a managed capture pipeline. Check current limits, data handling, authentication support, geographic and browser options, retention, and price directly. Pageshot documents browser capture and bulk batches, but the available research does not establish its current limits or price: Pageshot documentation.
Compare methods on URL volume, repeatability, authentication, required interactions, viewport coverage, output naming, collaboration, data handling, and current cost. A browser extension or hosted service is not required: Playwright supports a software-only scripted approach.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. It can return a PNG, JPEG, WebP, or PDF from one request. For a quick one-URL capture, use the API; for batch category auditing, its bulk capture option accepts up to 100 URLs per call. See the ScreenshotNeo API documentation for request options.
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}`);
Replace the example URL with a category URL. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks and 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 for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Navigation times out | The site is slow, keeps connections open, or needs a longer navigation window. | Increase the navigation timeout for that site, use an appropriate readiness condition, and record the failure instead of silently dropping the URL. |
| Screenshot is blank or incomplete | The page content has not rendered, the selected wait condition was too early, or a client-side error occurred. | Wait for a stable page-specific marker, inspect the page manually, and capture a representative page before rerunning the batch. |
| Product images are missing lower down | Images or rows are lazy-loaded only as the page scrolls. | Scroll through the page with a bounded loop, wait for relevant images, and inspect the full-page result. |
| An element screenshot fails | The selector is absent, hidden, or different on some category templates. | Check selector presence per page, report that entry as a failure, or use a page-specific selector. |
| HTTP error response | The URL may redirect, be unavailable, or reject the request. | Check the URL and response status. For authenticated pages, establish the intended session state and follow the site’s access rules. |
| Images differ between runs without a design change | Browser, operating system, viewport, consent, inventory, currency, promotions, or personalization changed. | Compare the saved metadata and restore matching capture conditions before treating the difference as a page change. |
| Batch stops after a single bad page | An unhandled navigation or screenshot exception ended the script. | Catch errors per URL, write the outcome to the index, and continue with the remaining pages, as in the example. |
Performance, reliability, and cost
A serial run is slower than concurrent capture, but places less load on the store and makes failures easier to isolate. Start serially, measure the run on a representative subset, then increase concurrency only when the store and audit environment can handle it. Keep browser contexts and page lifetimes controlled, and do not retry a failing URL indefinitely. A bounded retry for transient navigation errors can help, provided the index records attempts and final status.
Full-page screenshots can use more time and memory than viewport captures, especially for long listing pages. Use viewport captures when the first screen is the only audit target and element captures when a focused region answers the question. Store images and the metadata index together; retain originals if the images will be reviewed or compared later.
With Playwright, the principal cost is the time and infrastructure needed to run and maintain the browser process; the dossier does not provide a comparable price figure. Extensions and hosted services have their own plan and service terms, which should be checked directly. For ScreenshotNeo, the stated options are free 1,000 shots monthly with no card, then Starter at $5 for 3,000, Growth at $15 for 15,000, Pro at $39 for 60,000, Scale at $99 for 250,000, and Business at $249 for 1,000,000; yearly billing gives two months free. Every feature is on every plan.
FAQ
Should I capture every filter and sort combination?
Only if those states are part of the audit question. Otherwise, keep one representative state per category and document the chosen sort and filters.
Can screenshots prove that a layout change caused a conversion change?
No. They show rendered appearance, not user behavior or causation. Use them to identify visual differences for further investigation.
Should the audit use a PDF or separate image files?
Use separate images for annotation, processing, or page-level follow-up. A combined PDF can make sequential review easier.
How often should category pages be captured?
Set the schedule around the audit purpose, such as a release review or merchandising check. Keep run metadata so comparisons remain interpretable.


