How to Generate Website Thumbnails at Scale
Website thumbnails are everywhere: link previews in chat apps, website directories, social media cards, search results, and portfolio galleries. If you need to generate these at scale -- for hundreds or thousands of URLs -- doing it manually is not an option. This guide covers how to automate website thumbnail generation using APIs and self-hosted solutions.
Why Generate Website Thumbnails?
Thumbnails provide visual context that plain text links cannot. Studies show that links with preview images get 2-3x higher click-through rates. Common use cases include:
- Link previews: Show a visual preview when users paste a URL in your app (like Slack, Discord, or Notion)
- Website directories: Display thumbnail previews in curated lists, comparison pages, or marketplaces
- Social cards: Generate Open Graph images dynamically for SEO and social sharing
- Monitoring dashboards: Visual regression testing and uptime monitoring
- Portfolio sites: Automatically generate previews of client websites
- Search results: Enhance internal search with visual previews
Approach 1: Screenshot API with Resize
The simplest method: capture a full-resolution screenshot and resize it to thumbnail dimensions. Most screenshot APIs support custom viewport sizes, which effectively creates thumbnails.
const API_KEY = process.env.SCREENSHOT_API_KEY;
const BASE_URL = 'https://screenshotapi-api-production.up.railway.app';
async function generateThumbnail(url, width = 400, height = 300) {
// Capture at a standard viewport, the API handles the rest
const params = new URLSearchParams({
url,
width: '1280',
height: '800',
format: 'webp', // WebP for smallest file size
quality: '75', // Good enough for thumbnails
});
const response = await fetch(`${BASE_URL}/v1/screenshot?${params}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
if (!response.ok) throw new Error(`Failed: ${response.statusText}`);
return Buffer.from(await response.arrayBuffer());
}
// Generate thumbnails for a list of URLs
const urls = [
'https://github.com',
'https://vercel.com',
'https://stripe.com',
'https://tailwindcss.com',
];
const thumbnails = await Promise.all(
urls.map(async (url) => ({
url,
image: await generateThumbnail(url),
}))
);
// Save or upload to your CDN
thumbnails.forEach(({ url, image }) => {
const slug = new URL(url).hostname.replace(/\./g, '-');
fs.writeFileSync(`thumbnails/${slug}.webp`, image);
});Approach 2: Batch Processing Pipeline
For large-scale thumbnail generation (thousands of URLs), you need a pipeline that handles rate limiting, retries, and caching. Here is a production-ready example:
const pLimit = require('p-limit');
const fs = require('fs');
const path = require('path');
const API_KEY = process.env.SCREENSHOT_API_KEY;
const BASE_URL = 'https://screenshotapi-api-production.up.railway.app';
const CACHE_DIR = './thumbnail-cache';
// Limit concurrent requests to avoid rate limits
const limit = pLimit(5);
async function generateWithRetry(url, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const params = new URLSearchParams({
url,
width: '1280',
height: '800',
format: 'webp',
quality: '75',
});
const response = await fetch(`${BASE_URL}/v1/screenshot?${params}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
if (response.status === 429) {
// Rate limited - wait and retry
const retryAfter = parseInt(response.headers.get('retry-after') || '5');
console.log(` Rate limited, waiting ${retryAfter}s...`);
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return Buffer.from(await response.arrayBuffer());
} catch (err) {
if (attempt === retries) throw err;
await new Promise(r => setTimeout(r, 1000 * attempt));
}
}
}
async function processBatch(urls) {
fs.mkdirSync(CACHE_DIR, { recursive: true });
const results = await Promise.allSettled(
urls.map(url => limit(async () => {
const slug = new URL(url).hostname.replace(/[^a-z0-9]/gi, '-');
const cachePath = path.join(CACHE_DIR, `${slug}.webp`);
// Skip if cached
if (fs.existsSync(cachePath)) {
console.log(` CACHED: ${url}`);
return { url, status: 'cached', path: cachePath };
}
const image = await generateWithRetry(url);
fs.writeFileSync(cachePath, image);
console.log(` OK: ${url} (${image.length} bytes)`);
return { url, status: 'ok', path: cachePath };
}))
);
const ok = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`\nDone: ${ok} succeeded, ${failed} failed`);
return results;
}
// Usage
const urls = fs.readFileSync('urls.txt', 'utf-8')
.split('\n')
.filter(Boolean);
processBatch(urls);Approach 3: On-Demand with CDN Caching
Instead of pre-generating all thumbnails, generate them on demand when first requested and cache them on a CDN. This works well when you have many URLs but only some are frequently accessed.
// Express endpoint that generates thumbnails on demand
const express = require('express');
const crypto = require('crypto');
const app = express();
app.get('/thumbnail', async (req, res) => {
const { url } = req.query;
if (!url) return res.status(400).json({ error: 'url required' });
// Generate cache key from URL
const cacheKey = crypto
.createHash('md5')
.update(url)
.digest('hex');
// Check memory/Redis cache first
const cached = await cache.get(cacheKey);
if (cached) {
res.set('Content-Type', 'image/webp');
res.set('Cache-Control', 'public, max-age=86400');
return res.send(cached);
}
// Generate thumbnail via ScreenshotAPI
const params = new URLSearchParams({
url, format: 'webp', width: '1280', height: '800', quality: '75',
});
const response = await fetch(
`https://screenshotapi-api-production.up.railway.app/v1/screenshot?${params}`,
{ headers: { 'Authorization': `Bearer ${process.env.API_KEY}` } }
);
const buffer = Buffer.from(await response.arrayBuffer());
// Cache for 24 hours
await cache.set(cacheKey, buffer, 86400);
res.set('Content-Type', 'image/webp');
res.set('Cache-Control', 'public, max-age=86400');
res.send(buffer);
});Choosing the Right Format
| Format | File Size | Quality | Browser Support | Best For |
|---|---|---|---|---|
| WebP | Smallest | Excellent | 97%+ | Default choice for thumbnails |
| JPEG | Small | Good | 100% | Maximum compatibility |
| PNG | Large | Perfect | 100% | Transparency needed |
For thumbnails, WebP at 75% quality gives the best balance of file size and visual quality. A typical 400x300 thumbnail is 15-25KB in WebP versus 40-60KB in JPEG.
Performance Tips
- Use WebP format. 25-35% smaller than JPEG at the same quality. Browser support is over 97% globally.
- Lower quality for thumbnails. At small sizes, quality 70-80 is visually indistinguishable from 100. Save bandwidth and storage.
- Batch with concurrency limits. Run 5-10 screenshots in parallel, not 100. Respect API rate limits.
- Cache aggressively. Website thumbnails rarely change. Cache for 24-48 hours minimum, invalidate only when needed.
- Use a CDN. Serve cached thumbnails from edge locations close to your users. Cloudflare R2 + Workers is a cost-effective option.
Frequently Asked Questions
How many thumbnails can I generate per month?
With ScreenshotAPI's free tier, you get 100 screenshots/month. The Pro plan ($29/mo) gives you 10,000 -- enough for most directory and link preview use cases.
Can I resize thumbnails after capture?
Yes. Capture at a standard viewport (1280x800) and resize client-side or with an image processing service. Alternatively, capture at a smaller viewport for a direct thumbnail.
How do I handle sites that block screenshots?
ScreenshotAPI uses realistic browser fingerprints that work with most sites. For heavily protected pages, try adding a wait parameter (2-5 seconds) to let the page fully render.
Related Articles
How to Capture Website Screenshots with an API
Complete guide with code examples in Node.js, Python, cURL, and PHP.
Automate Website Screenshots with Node.js
Puppeteer vs API approaches compared with working code.
Best Screenshot APIs Compared: 2026 Guide
Feature and pricing comparison of the top screenshot API services.
Generate thumbnails in seconds
Try ScreenshotAPI's interactive playground -- capture your first thumbnail with no signup.
Open Playground