How to Make Website Preview Images for a SaaS Tools Directory
Build consistent SaaS directory previews with Playwright: choose the right page and crop, normalize output, handle failures, and review images before publishing.
To make website preview images for a SaaS tools directory, render each product’s public landing page in a browser at a fixed viewport, wait for its useful content to appear, and save a consistent screenshot for the directory card. Use a viewport capture for a quick first impression, an element capture when a particular product view is more representative, or a full-page capture when the entire page matters. Then normalize the images to a shared aspect ratio and review them at actual card size.
This guide uses Playwright with Node.js. Its screenshot API supports viewport, element, and full-page captures, along with output path, quality, and scale options. See the Playwright Page API and its official screenshot guide.
1. Decide what the directory card should show
Pick the capture scope before automating the directory. A consistent image set starts with a consistent editorial rule.
| Capture | Use it when | Watch for |
|---|---|---|
| Viewport | You want a compact first impression of the public home or product page. | The hero may contain a banner, animation, or content that is not the product itself. |
| Selected element | A specific product preview, interface illustration, or useful hero region best represents the listing. | The selector can change, be absent, or match more than one element. |
| Full page | The complete landing page is useful to readers inspecting the product’s message and features. | A tall page often becomes unreadable when reduced to a directory thumbnail. |
For most cards, start with a viewport capture. Use the product’s public home or product page rather than a login screen unless the listing specifically covers an app that has no public product view. Keep that URL and capture rule with the listing record so later refreshes remain consistent.
2. Set up a repeatable Playwright capture
The following example uses Chromium, a fixed viewport, a fixed device scale factor, and one URL per output PNG. It waits for the document to load and then gives the page a short settling period. Replace the sample URLs with your directory entries.
mkdir saas-previews
cd saas-previews
npm init -y
npm install playwright
npx playwright install chromium
// capture.mjs
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';
const entries = [
{ slug: 'example-tool', url: 'https://example.com' },
];
const outputDir = new URL('./previews/', import.meta.url);
await mkdir(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
colorScheme: 'light',
});
try {
for (const entry of entries) {
const page = await context.newPage();
try {
await page.goto(entry.url, {
waitUntil: 'domcontentloaded',
timeout: 45_000,
});
await page.locator('body').waitFor({ state: 'visible', timeout: 10_000 });
// Allow client-rendered hero content and fonts a moment to settle.
await page.waitForTimeout(1_000);
await page.screenshot({
path: new URL(`${entry.slug}.png`, outputDir).pathname,
type: 'png',
animations: 'disabled',
});
console.log(`Saved ${entry.slug}.png`);
} catch (error) {
console.error(`Could not capture ${entry.url}: ${error.message}`);
} finally {
await page.close();
}
}
} finally {
await context.close();
await browser.close();
}
Run it with node capture.mjs. The sample catches an error per entry so one unavailable website does not prevent later listings from being captured. It logs failures for review rather than quietly treating an error page as a successful preview.
3. Wait for the useful content, not just navigation
A successful navigation does not guarantee that the image, font, or product preview you want is ready. Many pages render important content in client-side code or load it after the initial document. If a stable hero selector is available, wait for it explicitly:
await page.goto(entry.url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
const hero = page.locator('main h1');
await hero.waitFor({ state: 'visible', timeout: 15_000 });
await page.screenshot({ path: 'preview.png', animations: 'disabled' });
Replace main h1 with a selector that exists on the pages in question. For an element capture, waiting for and capturing the same selector is usually clearer:
const preview = page.locator('[data-directory-preview]');
await preview.waitFor({ state: 'visible', timeout: 15_000 });
await preview.screenshot({ path: 'product-preview.png', type: 'png' });
If pages have different structures, maintain a per-listing selector or use the viewport capture as a fallback. Do not make a missing selector silently produce a blank or unrelated image. Record which fallback was used so someone can review it.
4. Capture full pages and control image output
Set fullPage: true when you deliberately want the entire scrollable page. For a regular viewport, omit it or set it to false.
await page.screenshot({
path: 'full-page.png',
fullPage: true,
type: 'png',
animations: 'disabled',
});
Playwright documents screenshot output path, image quality, and scale controls in its Page API. JPEG and WebP support a quality setting; PNG does not use that lossy quality control. Use the browser API’s accepted image type and option combinations, and keep the original capture if you may need to crop it again.
await page.screenshot({ path: 'preview.jpg', type: 'jpeg', quality: 82 });
await page.screenshot({ path: 'preview.webp', type: 'webp', quality: 82 });
Choose one output format and aspect ratio for all cards. Playwright captures the browser content; if the final card needs a different ratio, crop or letterbox afterward with your image pipeline. Apply the same rule to every listing. Avoid stretching an image to fit because that distorts logos and interface details.
5. Make captures consistent across the directory
- Use one browser engine: the example uses Chromium for every listing.
- Fix the viewport and scale: keep width, height, and device scale factor constant unless the directory has intentionally different card types.
- Fix color mode: use the same light or dark preference for all captures, or choose based on a documented directory policy.
- Wait for a meaningful target: prefer a visible hero or product preview over an arbitrary long delay where a stable selector exists.
- Disable animation: Playwright’s screenshot option can disable animations so a capture is less likely to land on a transient frame.
- Keep source details: store the source URL, capture date, viewport, selected scope, and any selector with the image metadata or listing record.
Consistency is an editorial criterion, not a measured performance result: directory cards should have similar framing and remain recognizable and legible when reduced to their displayed size.
6. Handle consent banners, overlays, and unavailable pages
Review captures for cookie banners, newsletter popups, chat widgets, and other overlays. These can obscure the content the card is meant to show. A screenshot should reflect the page a visitor can actually see, so do not hide or dismiss controls in a way that changes the site’s meaning. If you automate a consent choice, follow the site’s presented options and the rules that apply to your use.
Before publishing, reject or replace captures that show a login form by mistake, a bot check, a blank page, a transient error, or a loading state. Retry a temporary load failure with a limited retry policy. If the page remains inaccessible, use a declared favicon as an identifier or mark the preview unavailable rather than presenting the favicon as a screenshot of the product.
A favicon can be declared with rel="icon" (or a supported alternative). Google recommends a square, representative favicon larger than 48 by 48 pixels for good appearance across surfaces; see Google Search Central’s favicon guidance. A favicon identifies a site; it does not show the product experience.
7. Review and publish responsibly
Inspect each image at the size readers will see it. Check that the product is recognizable, text or interface details remain useful, the crop is not misleading, and there is no accidental login, consent overlay, or error state. Review again after a site redesign or when refreshing old directory entries.
Website screenshots may include third-party content and marks. Confirm that your intended use is permitted, especially for commercial or promotional contexts. Google’s guidance about screenshots of Google Search assigns responsibility for relevant third-party approvals and calls for appropriate approval for promotional material. That guidance is specific to Google Search screenshots; it is not a universal legal ruling for screenshots of every website. See the Google Brand Resource Center search guidelines.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Navigation times out | The site is slow, uses long-lived network requests, or is temporarily unavailable. | Use domcontentloaded rather than waiting for every network request to stop; set a finite timeout, retry a small number of times, and inspect the page before publishing. |
| Screenshot is blank or shows a loading skeleton | The app renders after navigation or the capture happened before the target appeared. | Wait for a stable, visible page-specific selector. If none exists, use a modest settling delay and review the resulting image. |
| Element locator times out | The selector is wrong, changed, hidden, or absent on that site. | Inspect the page, update the listing’s selector, or fall back to a viewport capture with the fallback recorded. |
| Cookie banner or chat covers the hero | The page displayed an overlay during capture. | Review whether the site offers a legitimate consent action; capture only after an appropriate choice, or flag the image for manual handling. |
| Images or fonts look incomplete | External assets have not loaded, or the website has blocked or delayed them. | Wait for a specific image or font-dependent region when possible, then recheck. Avoid indefinite waiting for all network traffic to become idle. |
| Output path error | The directory does not exist or the process cannot write there. | Create the destination directory before capture and verify the process has write access. |
| Only some entries were saved | A page-specific failure interrupted its capture. | Keep failures isolated per entry, log the URL and reason, and rerun only those entries after checking their status. |
9. Performance, reliability, and cost
For a directory with many URLs, reuse one browser and browser context as in the example, while creating and closing a page for each entry. This avoids launching a separate browser for every card. Add concurrency only after checking the target sites’ policies and the available CPU and memory; high parallelism can consume resources and place unnecessary load on sites. A simple sequential run is easier to inspect and retry.
Set finite navigation and selector timeouts, isolate errors per URL, and keep a record of failures and capture metadata. Refresh previews on a defined editorial schedule or after meaningful site changes. Browser automation requires a runtime and installed browser, and the costs are your compute, storage, and maintenance; the research sources do not provide a benchmark or cost figure for this workflow.
10. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a screenshot or PDF; its API documentation covers the 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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers indicate the page verdict and billing status. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Should every directory card show the same part of each website?
Use one default capture rule, usually the viewport, and make exceptions only when an element or full page represents a listing more clearly. Record exceptions so cards still follow a deliberate policy.
Is a favicon an acceptable preview image?
It can identify a site when a screenshot is unavailable, but it does not show the product. Label or style it as an icon fallback so readers do not mistake it for a page preview.
Can I use full-page screenshots for thumbnails?
Yes, but long pages often shrink into unreadable cards. Use full-page captures when readers can open or inspect the larger image, and prefer a viewport or selected element for a compact thumbnail.


