Every time you share a link on Slack, Twitter, or iMessage, you see a tiny preview image of the destination page. That thumbnail does a lot of heavy lifting: it tells people what they're about to click before they click it. If you're building a product that displays URLs (a bookmarking app, a web directory, a CMS, a search engine), generating those thumbnails yourself is surprisingly hard.
This guide walks through using a website thumbnail generator API to create preview images from any URL, covering the common use cases, code in three languages, and the gotchas that will save you hours of debugging.
You can spin up a headless browser, navigate to a URL, and call page.screenshot(). It works. But in production, you'll quickly run into:
A thumbnail API handles all of this behind one HTTP call. You send a URL, you get an image back.
The simplest approach: hit the GrabShot API with a URL and specify a small viewport or output size.
curl "https://grabshot.dev/api/screenshot?url=https://github.com&width=1280&height=800&output=jpeg&quality=80" \
-H "x-api-key: YOUR_API_KEY" \
--output github-thumb.jpg
This captures a 1280x800 viewport and returns a compressed JPEG. For actual thumbnails, you'll usually want to resize the output further.
import fs from 'fs';
const params = new URLSearchParams({
url: 'https://github.com',
width: '1280',
height: '800',
output: 'jpeg',
quality: '75',
});
const res = await fetch(`https://grabshot.dev/api/screenshot?${params}`, {
headers: { 'x-api-key': process.env.GRABSHOT_KEY },
});
const buffer = Buffer.from(await res.arrayBuffer());
fs.writeFileSync('github-thumb.jpg', buffer);
console.log(`Thumbnail saved: ${buffer.length} bytes`);
import requests
import os
resp = requests.get("https://grabshot.dev/api/screenshot", params={
"url": "https://github.com",
"width": 1280,
"height": 800,
"output": "jpeg",
"quality": 75,
}, headers={
"x-api-key": os.environ["GRABSHOT_KEY"]
})
with open("github-thumb.jpg", "wb") as f:
f.write(resp.content)
print(f"Saved {len(resp.content)} bytes")
A 1280x800 screenshot is not a thumbnail. For most use cases (cards, grids, directories), you want something like 400x250 or 600x400. There are two approaches:
If your API supports a resize or thumbnail parameter, use it. With GrabShot, you can use the AI cleanup feature to get a polished result, or simply capture at the target resolution:
# Capture at a smaller viewport - fast and lightweight
curl "https://grabshot.dev/api/screenshot?url=https://stripe.com&width=800&height=600&output=webp&quality=70" \
-H "x-api-key: YOUR_API_KEY" \
--output stripe-thumb.webp
Capture at 1280 or 1440 width (so the page renders normally), then use Sharp, Pillow, or your image CDN to resize:
import sharp from 'sharp';
// Assume `screenshotBuffer` is from the API
const thumbnail = await sharp(screenshotBuffer)
.resize(400, 250, { fit: 'cover' })
.webp({ quality: 75 })
.toBuffer();
// ~15-30 KB per thumbnail
Option 2 looks better because the page renders at a realistic viewport width. Text is readable, layouts don't collapse. Option 1 is faster and cheaper if you're generating thousands.
Bookmarking apps like Raindrop.io and Pocket show a thumbnail alongside each saved link. When a user saves a URL, fire off an async thumbnail request and store the result in your CDN. Display a placeholder until the thumbnail is ready.
// On bookmark save (background job)
async function generateLinkPreview(url, bookmarkId) {
const params = new URLSearchParams({
url,
width: '1280',
height: '800',
output: 'webp',
quality: '70',
});
const res = await fetch(
`https://grabshot.dev/api/screenshot?${params}`,
{ headers: { 'x-api-key': process.env.GRABSHOT_KEY } }
);
const buffer = Buffer.from(await res.arrayBuffer());
// Resize to card size
const thumb = await sharp(buffer)
.resize(480, 300, { fit: 'cover' })
.toBuffer();
// Upload to S3/R2/your CDN
await uploadToCDN(`thumbnails/${bookmarkId}.webp`, thumb);
}
If you run a directory of tools, startups, or resources, thumbnails make your listings dramatically more engaging. Generate them on submission, regenerate monthly to catch redesigns.
Web designers and agencies often showcase client sites. Instead of manually taking screenshots, automate it:
sites = [
"https://client-one.com",
"https://client-two.com",
"https://client-three.com",
]
for site in sites:
slug = site.split("//")[1].replace(".", "-").rstrip("/")
resp = requests.get("https://grabshot.dev/api/screenshot", params={
"url": site,
"width": 1440,
"height": 900,
"output": "png",
}, headers={"x-api-key": os.environ["GRABSHOT_KEY"]})
with open(f"portfolio/{slug}.png", "wb") as f:
f.write(resp.content)
print(f"Captured {slug}")
Internal search tools (for intranets, documentation sites, or custom search engines) can show a visual preview alongside each result. Users find the right page faster when they can see it.
Thumbnails don't need to be real-time. Most sites don't change their layout daily. A sensible caching strategy saves you API calls and speeds up your app:
| Content type | Cache duration | Why |
|---|---|---|
| Static sites / blogs | 7-30 days | Rarely changes |
| SaaS landing pages | 3-7 days | Occasional updates |
| News sites | 1-4 hours | Content rotates frequently |
| Social media profiles | 1-7 days | Profile pages are stable |
| User-submitted URLs | 24 hours initially, then 7 days | Capture fresh, then relax |
Use the URL as a cache key (normalized: lowercase, strip trailing slash, sort query params). Store thumbnails in S3, R2, or any object store with a CDN in front.
Real URLs in the wild are messy. Here's what to watch for:
If you're generating thousands of thumbnails (directory with 10K listings, or a bookmarking app with active users), a few optimizations matter:
25 free screenshots per month. No credit card required. JPEG, PNG, and WebP output with custom viewports.
Try It Free →A common question: should you use the site's existing Open Graph image instead of generating a thumbnail?
OG images are fast to fetch (just parse the HTML for the og:image meta tag), but they have limitations. Many sites don't have one. Those that do often use a generic brand image that doesn't represent the specific page. And OG images are chosen by the site owner, not by you, so quality varies wildly.
A good strategy: try the OG image first (using a meta tag extractor). If it exists and looks reasonable, use it. If not, fall back to a generated thumbnail. Best of both worlds.
Website thumbnails are one of those features that seems simple until you try to build it yourself. Between browser rendering, image processing, caching, and edge cases, there's a lot of hidden complexity. An API abstracts all of that into a single HTTP call.
The key decisions are: what size to capture at, how to cache, and how to handle the inevitable weird URLs. Get those right and you'll have reliable thumbnails powering your link previews, directories, or portfolio pages with minimal ongoing effort.