ScreenshotNeo

BlogHow-to

How to Capture Website Thumbnails for a Directory Built with Next.js

Capture consistent previews of directory listings with Playwright, store and serve them in Next.js, and choose when a generated Open Graph image fits better.

By the ScreenshotNeo team4 October 20268 min read

To show a thumbnail of each listed website as it actually renders, run a browser capture job with Playwright: navigate to the listing URL, wait for a useful readiness condition, take a viewport screenshot, store the resulting image, and render that stored asset from your Next.js directory. Use a fixed viewport for consistent cards. Use an element screenshot only when a stable selector is available, and full-page capture only when a tall page image is useful.

Next.js Image displays and optimizes supplied image assets; it does not capture arbitrary websites. For a branded tile generated from your directory data, use Next.js ImageResponse instead. Those are two different image jobs.

1. Choose what the thumbnail represents

Need Use What it produces
Show the real listed website Playwright screenshot A browser-rendered capture of the target URL.
Show a consistent branded card with a site name, category, or directory data Next.js ImageResponse and an opengraph-image convention A designed image generated from your own data, not a screenshot of the external site.

For a directory card, viewport screenshots are usually the simplest starting point: every asset has a predictable shape. Playwright also supports element and full-page screenshots. A full-page result can be very tall, and an element capture depends on a selector or locator that works for the target website. See the Playwright screenshot guide and Page API.

2. Build a Playwright capture job

The following Node.js example captures a URL using Chromium, waits for the page’s load event, and saves a PNG. It accepts a URL and output path from the command line, so you can run it as a small worker or adapt the function for a queue. Install Playwright and its Chromium browser in the runtime where this script will run:

npm install playwright
npx playwright install chromium
// capture.mjs
import { chromium } from 'playwright';

const [targetUrl, outputPath = 'thumbnail.png'] = process.argv.slice(2);
if (!targetUrl) {
  throw new Error('Usage: node capture.mjs <url> [output.png]');
}

const browser = await chromium.launch({ headless: true });
try {
  const page = await browser.newPage({
    viewport: { width: 1280, height: 800 },
    deviceScaleFactor: 1,
    colorScheme: 'light',
  });
  await page.goto(targetUrl, { waitUntil: 'load', timeout: 30_000 });
  await page.screenshot({ path: outputPath, type: 'png' });
  console.log(`Saved ${outputPath}`);
} finally {
  await browser.close();
}

Run it with a trusted directory URL:

node capture.mjs https://example.com ./public/thumbnails/example.png

The viewport, color scheme, scale, and wait condition are application choices, not universal settings. Some sites continue loading content after the load event; some never become network-idle because they keep connections open. For pages with a known readiness signal, wait for a selector or a deliberate delay instead of assuming one condition fits all targets. Review Playwright’s screenshot options for path, buffer, full-page, and element capture.

Return bytes for upload or processing

When your storage layer accepts bytes, omit path. The screenshot call returns a buffer:

const imageBytes = await page.screenshot({ type: 'webp' });
// Pass imageBytes to your storage adapter or image-processing step.

Choose the output format based on your delivery needs. PNG is lossless and broadly supported; JPEG and WebP are also available through Playwright’s screenshot options. Confirm that your image pipeline and consumers support the format you choose.

Capture an element or the full page

For a target whose layout exposes a stable selector, capture the matching element:

const cardRegion = page.locator('main');
await cardRegion.screenshot({ path: 'region.png' });

For a full-page image, use fullPage: true:

await page.screenshot({ path: 'full-page.png', fullPage: true });

Element selectors vary between websites, so a selector that works for one listing may fail for another. Full-page images are often unsuitable for compact directory cards because their heights vary with page length. Playwright documents these capture modes in its screenshot guide.

3. Connect captures to your directory data

Run captures in a background or scheduled job when you have many URLs, need retries, or refresh thumbnails. This keeps browser work out of the interactive page-rendering path. Associate the source URL, capture status, and stored image key with a stable directory record. On a failed refresh, keep serving the last successful image and show a placeholder only when no successful capture exists. These are application design choices; Playwright and Next.js do not prescribe a queue, retry policy, storage provider, or refresh schedule.

A practical job flow is:

  1. Validate and normalize the directory record’s URL.
  2. Launch or reuse a browser according to your workload and runtime limits.
  3. Navigate with a bounded timeout and wait for the readiness condition chosen for your target sites.
  4. Capture a fixed viewport (or a deliberate element/full-page scope).
  5. Store the image durably and update the record only after storage succeeds.
  6. Record the outcome and retain the previous good image when the new attempt fails.

Use a concurrency limit that your worker and hosting environment can support. Browser processes use more resources than a simple HTTP request, and target sites can be slow or unavailable. Measure the workload in your own deployment before choosing worker size, concurrency, or refresh frequency; there is no universal throughput figure.

4. Serve the saved image with Next.js

If the capture worker writes to a public object store, save its image URL with the directory entry and render it with next/image. For remote sources, configure the allowed host and path patterns in next.config.js or your equivalent Next.js config:

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
        pathname: '/directory/**',
      },
    ],
  },
};
// app/components/DirectoryCard.jsx
import Image from 'next/image';

export function DirectoryCard({ listing }) {
  return (
    <article>
      <Image
        src={listing.thumbnailUrl}
        alt={`Preview of ${listing.name}`}
        width={640}
        height={400}
        sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 320px"
      />
      <h2>{listing.name}</h2>
    </article>
  );
}

Set dimensions and sizes to match the card layout so the browser can reserve space and Next.js can select an appropriate image size. For local files under public, use a path such as /thumbnails/example.png. See the Next.js Images guide for local and remote sources and remote URL configuration.

5. Generate a branded image instead of capturing a site

When the goal is a consistent share image or designed directory tile, Next.js supports Open Graph image file conventions and dynamic image generation with ImageResponse. Its documentation shows a 1200 by 630 example and describes a supported subset of CSS. Build the graphic from your own route or listing data; this does not browse to and screenshot the listed website. See Metadata and OG images and the generateMetadata reference.

6. Or skip the browser setup

If you need the actual rendered website preview without installing and operating a browser worker, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a screenshot. 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}`);

Cookie banners, popups, and chat widgets are removed before the capture, and each of those steps can be turned off. Bot checks, blank pages, and failed loads are never billed; response headers report the page verdict and billing status. 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 with no card; paid plans start at $5 for 3,000. Every feature is available on every plan. For a directory pipeline, store the returned image with the matching listing record and check the response status and headers before treating it as a successful capture.

Start free with 1,000 screenshots a month and no card.

7. Troubleshooting

Symptom Likely cause Fix
Navigation times out The target is slow, unavailable, or keeps network activity open. Use a bounded timeout and a readiness condition suited to the site, such as a known selector. Keep the previous thumbnail if refresh fails.
Screenshot is blank or incomplete The page’s visible content was not ready when capture ran, or content is loaded dynamically. Wait for a meaningful page signal or a deliberate delay, and inspect the captured output before updating the stored asset.
Element capture fails The selector does not exist or is not unique on that target. Use a verified locator for that site, or fall back to viewport capture.
Remote image is rejected by Next.js The source host or path is not allowed by the configured remote patterns. Add the exact HTTPS hostname and required path pattern to the image configuration, then redeploy.
Image dimensions distort the card Capture viewport or display dimensions differ from the card’s intended aspect ratio. Standardize the capture viewport and set display dimensions and sizes for the card layout.
Worker runs out of resources Too many browser pages or processes run concurrently for the available worker capacity. Lower concurrency, close pages and browsers in cleanup paths, and size workers from observed workload.

8. Performance, reliability, and cost

  • Performance: Browser startup, site navigation, and image storage all add work compared with serving an existing thumbnail. Reuse browser processes where appropriate, bound concurrency, and avoid recapturing unchanged records unnecessarily.
  • Reliability: External sites can change, block automation, or fail to load. Treat capture as a fallible job, preserve the last good asset, and keep a placeholder for records with no successful image.
  • Storage and delivery: Persist captures outside ephemeral worker storage when they must survive restarts. Choose PNG, JPEG, or WebP based on visual needs, compatibility, and bandwidth; the image pipeline should be able to serve the chosen type.
  • Cost: A self-hosted Playwright flow has no per-screenshot API price in this implementation, but it consumes your compute, storage, and delivery capacity. A managed screenshot API trades browser operations for provider pricing. Compare based on your capture volume and operational needs; no universal cost or speed comparison is implied.

FAQ

Does Next.js Image take the screenshot?

No. It renders an image asset. A browser capture job such as Playwright must create the screenshot separately.

Should a directory thumbnail use full-page capture?

Usually not for compact cards: a full-page image can be very tall. Use it when readers need to inspect the entire page as one image.

Can I use the same image as the Open Graph image?

You can choose to reuse a captured asset, but a generated Open Graph image is a separate design decision. Use ImageResponse when you want a consistent branded image based on route data.

How often should thumbnails refresh?

There is no universal cadence. Choose one based on how often directory listings change, how fresh previews need to be, and the capture resources available to your application.