DEV Community

Cover image for How to Generate a Website Preview Image from a URL
Nico Acosta for Grabbit

Posted on • Originally published at grabbit.live

How to Generate a Website Preview Image from a URL

A website preview image is a rendered picture of a web page, almost always a screenshot of the page captured in a real browser. Link directories, dashboards, "recently added site" feeds, portfolio grids, and monitoring tools all show one so a person can recognize a site at a glance without loading it. To generate a preview from a URL, send the URL to a screenshot API and store the image it returns.

The preview itself is the easy part. The work is rendering the page faithfully, at the right size, for many URLs, on a schedule, without running a browser fleet yourself. This guide covers what a website preview actually is, how it differs from a social link preview, what size to capture, the build-it-yourself path, and the one-call API path.

Website preview vs social link preview

Search results for "website preview" mix two different jobs, so it is worth separating them up front:

  • A website preview image is a screenshot of the page. You generate it for sites you link to, list, or watch, and it refreshes when you recapture. This is a render-the-URL problem.
  • A social link preview is the og:image a site sets for itself, the card that shows when its link is shared on Slack, Discord, X, or LinkedIn. It is a hand-designed image, not a live screenshot, and you control it only for your own pages.

This post is about the first one: turning a URL into a real rendered image you can show. If you are trying to fix the card that appears when your own link is shared, that is an Open Graph problem. See what is an OG image for that path instead.

There is also a third thing people call a "preview": the browser-extension popup that shows a live page when you hover a link. That is an in-browser convenience, not an image you can store, host, or put in a dashboard. For anything programmatic you want a captured image, which is what the rest of this guide produces.

What size should a website preview be

Capture at a desktop viewport width, then display the result scaled down. The two decisions are separate:

  • Capture width sets which layout renders. 1280px is a safe default: it triggers the desktop layout most visitors see. Rendering at a tiny viewport instead pushes the page into its mobile breakpoint, so the preview shows a hamburger menu and a single stacked column, which is not a recognizable preview of the site.
  • Displayed size is a CSS concern. Render the captured image into whatever box your grid or card uses. Downscaling a wide capture keeps text crisp and the layout faithful.

For the most common framing, an above-the-fold preview, capture a fixed height rather than the whole page. A 1280x720 capture gives you a clean 16:9 crop of the top of the page. When you want the entire document (long landing pages, articles), use full_page instead and let the height grow.

The build-it-yourself path

You can generate previews with headless Chrome. The shape in Puppeteer:

// DIY: you own the browser, the scaling, and the storage
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720 });
await page.goto(url, { waitUntil: 'networkidle2' });
const buffer = await page.screenshot({ type: 'png' });
await browser.close();
// then: upload to storage, store the URL, serve the image
Enter fullscreen mode Exit fullscreen mode

That works for one URL on your laptop. The cost shows up at scale:

  • Infrastructure. Each capture needs a Chromium process. Previewing a directory of hundreds of sites means a browser pool, a queue, and memory limits, because one runaway page can take down the box.
  • Reliability. Cookie and consent banners, lazy-loaded images, and pages that block headless browsers all produce blank or broken previews unless you handle each case.
  • Storage. You still have to upload, host, and serve every image yourself.

For a handful of static previews this is fine. For an automated, always-current feed it becomes a service you maintain.

The one-call API path

A screenshot API does the rendering and hosting, so your code is just the request. Send the URL, get back a hosted image:

curl https://api.grabbit.live/v1/grabs \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://stripe.com",
    "width": 1280,
    "height": 720,
    "format": "webp"
  }'
Enter fullscreen mode Exit fullscreen mode

width pins the desktop layout. height of 720 gives a 16:9 above-the-fold crop, the classic preview frame. format of webp keeps the file small, which matters when a feed loads many previews at once. The response includes an image_url pointing at the stored capture. Save that URL and render it in your <img> or as a CSS background-image in the card.

Want the entire page instead of the top crop? Set "full_page": true and drop height. For pages whose content loads after the first paint, add "delay_ms": 800 so lazy images settle before the capture. To preview just one part of the page, such as a hero or a pricing card, pass "selector": "#hero" and only that element is captured.

Generating previews in bulk

A directory or feed is the same request in a loop. There is no separate batch endpoint to learn:

  1. Iterate over your list of URLs.
  2. Call the API once per URL with the same width, height, and format.
  3. Store each returned image_url keyed by the source URL.
  4. Render the stored URLs in your grid or feed.

Because the API owns the browser pool, capturing a hundred previews is the same code as capturing one. If you are building this around a list of sites, how to screenshot a list of URLs covers the loop, concurrency, and error handling in depth.

Keeping previews current

A preview goes stale the moment the source site redesigns. The fix is to recapture, but recapturing on every page load is wasteful. The pattern:

  • Cache the image_url. Store it with the source URL and a captured-at timestamp. Serve the cached image, not a fresh capture, on every request.
  • Refresh on a schedule. Recapture each URL on a cadence that matches how often the sites change. A daily refresh is plenty for most feeds; weekly is fine for stable portfolios.
  • Refresh on demand. Let users trigger a recapture for their own listing so a redesign shows up immediately instead of waiting for the next run.

This keeps the feed current without rendering a browser on every request.

Putting it together

To generate a website preview image from a URL: capture at a desktop width, request a small format, store the returned image URL, and recapture on a schedule. With a hosted API the whole thing is one request per URL plus a cache, with no browser fleet to run.

For the full set of capture options (viewport, format, full-page, selector, delay) see the screenshot API. For running previews across many URLs on a schedule, see automated screenshots. And if you want the top crop versus the whole document, how to generate a website thumbnail from a URL walks through the framing choices in detail.


Originally published on the Grabbit blog.

Top comments (0)