A website thumbnail is a small picture of a page's first screen: the preview in a link directory, a "sites we built" grid, a bookmark manager, a CMS field that shows where a link goes. Making one sounds like one screenshot, but three choices decide whether it looks right and loads fast: the window you capture in, the format, and where the shrinking and caching happen. This page goes through each with numbers we measured.
How we measured. On 2026-11-08 we captured the home pages of python.org, w3.org, nodejs.org, debian.org, rust-lang.org and mozilla.org through our screenshot API from our development stack (Free-plan captures, which carry a small badge), in PNG, JPEG and WebP, then shrank them with Pillow 11.1. Each capture took 4.8–8.9 s, most of it the sites' own loading.
1. Capture at desktop width, shrink afterwards
The tempting shortcut is to ask for a 400 px wide screenshot. Websites see a 400 px window as a phone and switch to their mobile layout: python.org captured at 400×250 showed a full-width menu (Close, Python, PSF, Docs, PyPI…) and nothing of its home page. Its responsive layout is doing what the viewport meta tag asks for.
So capture at the width people use the site at — our default viewport is 1280×800 — and scale the picture down. The viewport's height sets the crop: 1280×800 is 16:10; for 4:3 cards send "viewport_height": 960; for a square, 1280×1280.
2. WebP, and how much quality
Sizes at 1280×800, first screen only, cookie banners removed:
| Site | PNG | JPEG (q 85) | WebP (q 85) | WebP (q 60) |
|---|---|---|---|---|
| python.org | 133 KB | 105 KB | 56 KB | 38 KB |
| w3.org | 157 KB | 111 KB | 69 KB | 50 KB |
| nodejs.org | 256 KB | 112 KB | 62 KB | 43 KB |
| debian.org | 593 KB | 158 KB | 109 KB | 69 KB |
| rust-lang.org | 107 KB | 116 KB | 68 KB | 49 KB |
| mozilla.org | 58 KB | 77 KB | 35 KB | 27 KB |
WebP was the smallest for every page, at about half of JPEG. PNG is lossless and swings wildly: flat colours compress well (mozilla.org), photos and gradients do not (debian.org's banner, 593 KB). quality (1–100, default 85) applies to JPEG and WebP; 60 is fine for a picture that will be shown at a third of its size.
3. Shrink once, not in every browser
A 1280×800 picture in <img width="400"> looks right, but every visitor downloads the full 56 KB. Shrunk to 400×250 first, the same WebP was 8.7 KB for python.org, 8.5 KB for nodejs.org and 13.6 KB for debian.org — six times smaller. With Pillow:
from PIL import Image
img = Image.open("python.org.webp") # the 1280×800 capture
img.thumbnail((400, 400)) # keeps the aspect ratio: 400×250
img.save("python.org-400.webp", "WEBP", quality=80)
Do it where your images are processed anyway: when the link is added to your directory, in a build step, or through an image CDN that resizes by URL. For retina screens, keep an 800 px version too and offer both with srcset (MDN: img). Our API does not shrink captures for you: scale goes up (1, 2, 3 for retina), never down. Pillow's thumbnail() is documented in the Pillow reference.
4. Caching: a signed URL in the page
For thumbnails shown straight from our side, put a signed /s/ URL in the <img>. It is a normal GET that anyone can load, with your options in the query and a signature so nobody can change them:
import { createHmac } from 'node:crypto';
const enc = (v) => encodeURIComponent(String(v))
.replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
export function thumbnailUrl(pageUrl) {
const params = { url: pageUrl, block_cookie_banners: '1', viewport_height: '960' };
const path = `/s/${process.env.SHOT_ACCOUNT_ID}.webp`;
const query = Object.keys(params).sort()
.map((name) => `${enc(name)}=${enc(params[name])}`).join('&');
const s = createHmac('sha256', process.env.SHOT_SIGNING_SECRET)
.update(`${path}?${query}`).digest('hex').slice(0, 32);
return `https://shareglint.com${path}?${query}&s=${s}`;
}
// <img src={thumbnailUrl('https://www.w3.org/')} width="400" height="300" loading="lazy" alt="w3.org">
What that URL did for w3.org at 1280×960:
| Request | Answer | Time |
|---|---|---|
| First | 302 to the new capture (81 KB WebP) | 7.1 s |
| Second | 302 to the same file | 0.08 s |
| Third | 302 to the same file | 0.11 s |
viewport_height changed to 961 by hand |
403 | — |
The first visitor waits for the capture. To spare them, request the URL once on your server when the link is added; every later view is a redirect to a stored file. Within 24 hours the same options never capture again, so a page viewed a thousand times a day costs one capture a day. The signature is computed with your signing secret on your server — never in browser code; signed image URLs explains the scheme, and the JavaScript guide shows the same function checked against our verifier.
loading="lazy" keeps a long grid from loading thumbnails nobody scrolls to, and the width/height attributes keep the layout from jumping while they load.
Many URLs at once
A directory with 2,000 links needs 2,000 captures once, then only new links. Send them from a script with a few requests in parallel and webhooks for the results; bulk website screenshots has the script and what concurrency did to the total time. A viewport capture costs 1 render of the quota; repeats within 24 hours are free (pricing).
A thumbnail as a card
For link-preview cards with the site's name and description next to the picture, render the capture into a template instead of showing it raw: site-card takes a title, a description and an image, and browser-frame puts the screenshot in a browser window. The card then has a fixed layout whatever the site looks like.
Limits
- Public pages only. No logins, no intranet, no pages behind a bot wall.
- First screen only in a thumbnail; for the whole page see full-page screenshots.
- A day old at most, not live. A site that changes every hour shows the version captured first that day.
- Some sites show pop-ups other than cookie banners (subscription offers, sign-in boxes);
hide_selectorsremoves them once you know their selector (cookie banners and other pop-ups).