Your link directory, portfolio or blogroll used to show a small screenshot next to every site. Now each one is a broken-image icon, or the same grey "thumbnail not available" picture repeated down the page. Nothing changed on your side: the service behind the <img> tag did.
Three services that sites embedded this way have stopped answering as they did. PagePeeker switched off its free endpoint, ShrinkTheWeb took its service offline in 2023 and has not relaunched it, and PageLr shut down in June 2026. Your pages still point at their URLs.
This guide shows what each old URL returns today, measured from a server on 8 November 2026, and then the ways to replace them, cheapest first, with what each one costs you.
Disclosure: this guide is published by Site-Shot, a screenshot API, and it compares other services with ours. We have kept the facts accurate and the trade-offs honest, including where a free keyless service or a cheaper API is the better fit. Every endpoint was checked from a server and every price on the vendor's own page on 8 November 2026; always confirm on the vendor's site before you buy.
Which thumbnail URL your pages use, and what it returns now
Open a page with a broken thumbnail, right-click the image and copy its address, or search your theme, plugin settings and database for the host names below. Each row is one URL pattern sites embedded, requested from a server on 8 November 2026:
| URL pattern in your pages | What it returns today |
|---|---|
http://free.pagepeeker.com/v2/thumbs.php?size=m&url=... |
410 Gone, a 5-byte text body: the browser shows a broken image |
https://api.pagepeeker.com/v2/thumbs.php?size=m&url=... with no code and no registered referer |
200 with a 1,084-byte placeholder GIF; the header x-pp-error-message says "No code and no referer" |
http://pagepeeker.com/thumbs.php?size=m&url=... (the old v1 form) |
403, an HTML challenge page |
https://images.shrinktheweb.com/xino.php?stwembed=1&stwaccesskeyid=...&stwurl=... |
No image: the TLS certificate belongs to another host, so the connection fails; plain http redirects to the same https address |
https://api.pagelr.com/capture?uri=... |
No answer at all; we gave up after 25 seconds |
http://images.thumbshots.com/image.aspx?url=... |
The host name no longer resolves |
http://images.websnapr.com/?size=s&url=... |
404 Not Found |
What the vendors say, as of 8 November 2026:
- PagePeeker still sells its API; only the free, keyless endpoint is gone. We found no announcement. The Internet Archive's last image from
free.pagepeeker.comis dated 16 May 2026, and it has stored only HTML from that host since, so the switch-off was about six months ago. - ShrinkTheWeb went offline in 2023: the home page archived on 24 September 2023 says the service has been taken offline and all billing has stopped. Since at least March 2024 the site has been titled "Shrink The Web is Relaunching Soon!" and says it is under new management. There is no working API to pay for in the meantime.
- PageLr's site says the service "was permanently shut down on June 3, 2026", with its endpoints, accounts and stored data gone, and asks users to move to another screenshot API.
One place the PagePeeker change shows up without anyone choosing PagePeeker: Knight Lab's CDN still serves the old TimelineJS 2 script, which requests api.pagepeeker.com thumbnails for website slides, so those slides now show the placeholder. TimelineJS3 does not use PagePeeker; moving the timeline to it is the fix there.
A few thumbnails, no code
If only a handful of sites need a picture, you do not need a script or a key. Open site-shot.com, paste the address, set the browser window to 1280 × 960 and the format to JPEG, press Shot, and upload the image next to the link. The browser tool is free and needs no signup.
Option 1: a keyless URL that still works
The quickest repair keeps the same shape, a URL in an <img> tag, pointed at a service that still answers without a key. Two of those that did on 8 November 2026:
- WordPress.com mShots.
https://s.wordpress.com/mshots/v1/https%3A%2F%2Fwww.wikipedia.org%2F?w=400returns a 400 × 300 JPEG. The first request for a new page gets a "Generating Preview" placeholder while it captures, and Automattic publishes no terms of use for it. Our mShots guide covers its parameters, the placeholder and the licensing question. - thum.io.
https://image.thum.io/get/width/400/https://www.wikipedia.org/returned a square 400 × 400 PNG, and a page it had not captured yet first got a loading GIF of the same size. Its home page offers "up to 1000 impressions per month completely free, without even signing up" (checked 8 November 2026).
Both are the cheapest fix and both are what PagePeeker's free endpoint was until May: a service you do not pay for and have no agreement with. Neither publishes terms of use that we could find (8 November 2026), so if thumbnails are part of something you sell, ask the operator or use a service with a contract.
Option 2: the paid version of the same service
Of the three, only PagePeeker has a paid API today. Its pricing page lists Basic at $5.99 a month for 100,000 API calls and Advanced at $39.99 for 1,000,000, and says every request counts as a call, including readiness checks (8 November 2026). Per call, that is far cheaper than Site-Shot's paid plans, which start at $5/mo for 2,000.
For a former free.pagepeeker.com user it is also the smallest change. PagePeeker's account docs say a request without a code is matched by the domain in its Referer header against the sites in your account, and that image URLs already in web pages keep working this way (8 November 2026). So change the host in your existing URLs to api.pagepeeker.com and add your domain to a paid account: the <img> tags keep their shape and carry no code. Each image a browser requests counts as a call.
The code is for requests from elsewhere, such as a script on your server; the same docs warn that anyone who copies it "can spend your monthly API calls", so keep it out of public pages. ShrinkTheWeb and PageLr have no such route today.
Option 3: a thumbnail URL on your own server
This keeps the drop-in shape, a URL in an <img> tag, but the URL is on your own site and your API key never leaves the server. A small PHP script, thumb.php, serves each thumbnail from a cache folder and captures it with the Site-Shot API when it is missing or a week old. Your pages call it as /thumb.php?url=...&size=m&sig=..., where sig is a signature your page code makes with a secret only the server knows, so nobody else can make your script spend captures on pages of their choosing. That holds only for addresses you chose: sign submitted links or commenters' websites after you approve them, never on render.
The script
<?php
// thumb.php: website thumbnails from a local cache, captured by the Site-Shot API
// when missing or a week old. Pages call /thumb.php?url=...&size=m&sig=...
// SITESHOT_API_KEY and THUMB_SECRET come from the web server's environment.
const CACHE_DIR = '/var/cache/thumbs'; // writable by PHP, outside the web root
const FRESH = 7 * 86400; // capture again after a week
const RETRY = 86400; // after a failed capture, wait a day
const DAILY_MAX = 20; // at most this many captures a day, whatever is asked
const SIZES = ['t' => 90, 's' => 120, 'm' => 200, 'l' => 400, 'x' => 480];
$in = fn($k, $d = '') => is_string($_GET[$k] ?? null) ? $_GET[$k] : $d; // no arrays
$url = $in('url');
$size = $in('size', 'm');
$scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
if (!isset(SIZES[$size]) || !in_array($scheme, ['http', 'https'], true)
|| !filter_var($url, FILTER_VALIDATE_URL)) {
http_response_code(400);
exit;
}
$secret = (string) getenv('THUMB_SECRET');
$sig = hash_hmac('sha256', "$size $url", $secret);
if ($secret === '' || !hash_equals($sig, $in('sig'))) {
http_response_code(403); // not signed by your pages: no capture
exit;
}
function image_type($bytes) {
if (strncmp($bytes, "\xFF\xD8\xFF", 3) === 0) return 'image/jpeg';
if (strncmp($bytes, "\x89PNG", 4) === 0) return 'image/png';
return null;
}
function age($path) {
return is_file($path) ? time() - filemtime($path) : PHP_INT_MAX;
}
function finish($file) {
if (!is_file($file)) {
http_response_code(404); // nothing cached yet: no image, no API details
header('Cache-Control: no-store'); // so the browser asks again next time
exit;
}
$image = file_get_contents($file);
header('Content-Type: ' . image_type($image));
header('X-Content-Type-Options: nosniff');
header('Cache-Control: public, max-age=86400');
echo $image;
exit;
}
$file = CACHE_DIR . '/' . hash('sha256', "$size $url");
if (age($file) < FRESH || age("$file.fail") < RETRY) {
finish($file);
}
// A miss: one capture at a time, since the free plan answers a parallel one with 429.
// While another capture runs, do not wait: serve the old copy, or a 404 for now.
$lock = fopen(CACHE_DIR . '/.lock', 'c');
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
finish($file);
}
clearstatcache();
$count = CACHE_DIR . '/.captures-today';
$today = is_file($count) && date('Ymd', filemtime($count)) === date('Ymd')
? (int) file_get_contents($count) : 0;
if ($today < DAILY_MAX && age($file) >= FRESH && age("$file.fail") >= RETRY) {
file_put_contents($count, $today + 1);
$ch = curl_init('https://api.site-shot.com/?' . http_build_query([
'url' => $url, 'userkey' => getenv('SITESHOT_API_KEY'), 'format' => 'jpeg',
'width' => 1280, 'height' => 960, 'scaled_width' => SIZES[$size],
]));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 70]);
$image = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status === 200 && is_string($image) && image_type($image)
&& file_put_contents("$file.tmp", $image) === strlen($image)
&& rename("$file.tmp", $file)) {
@unlink("$file.fail");
} else {
touch("$file.fail"); // log the status only, never the API's body
error_log("thumb.php: HTTP $status for $url, keeping the old copy if any");
}
}
flock($lock, LOCK_UN);
finish($file); // the new copy, or the old one after a failure
What it does, in order: it refuses anything but an http or https address with a known size (400), including parameters sent as arrays, and any request whose signature does not match (403, compared with hash_equals(), so timing gives nothing away). A cached file younger than a week is served at once. On a miss it tries a lock, so captures run one at a time; a request that finds a capture already running does not wait for it, and gets the old thumbnail or an empty 404 that the browser does not cache. After DAILY_MAX captures in a day it only serves what it has, which caps the spend whatever happens to your signed addresses. It keeps a capture only when the API answered 200 and the body begins with an image file's signature bytes, and swaps it in with a rename, so no visitor gets half a file; it serves the image with X-Content-Type-Options: nosniff. When a capture fails it keeps serving the old thumbnail, or answers an empty 404 if there was none, and waits a day before trying that page again.
The sizes
The capture uses a 1280 × 960 window, scaled down to the width the size names, which gives a 4:3 thumbnail. SIZES uses PagePeeker's five size letters with the widths PagePeeker lists for them (8 November 2026): t 90 × 68, s 120 × 90, m 200 × 150, l 400 × 300 and x 480 × 360. If you came from ShrinkTheWeb or PageLr, put the pixel widths your pages used into SIZES under names of your choice.
Signing the URL in your pages
Your templates build the address with the same secret:
function thumb_src($url, $size = 'm') {
$sig = hash_hmac('sha256', "$size $url", getenv('THUMB_SECRET'));
return '/thumb.php?' . http_build_query(['url' => $url, 'size' => $size, 'sig' => $sig]);
}
Then the swap in a template is one line:
<!-- was: <img src="http://free.pagepeeker.com/v2/thumbs.php?size=x&url=wikipedia.org"> -->
<img src="<?= htmlspecialchars(thumb_src('https://www.wikipedia.org/', 'x')) ?>"
width="480" height="360" alt="wikipedia.org home page" loading="lazy">
Setting it up
- Get an API key from /start/. A confirmed account's API key comes with the free plan: 100 API captures a month, no card needed; paid plans start at $5/mo for 2,000. Set
DAILY_MAXto what your plan can spend in a day. - Make a long random secret (
openssl rand -hex 32) and give it and the key to PHP asTHUMB_SECRETandSITESHOT_API_KEYthrough the server's environment:SetEnvin Apache, orenv[THUMB_SECRET] = ...in a PHP-FPM pool file. Neither goes into a page. If every thumbnail answers 403, the code that builds the addresses cannot seeTHUMB_SECRET. - Create the cache folder, writable by the PHP user, and put
thumb.phpin your web root.
What we saw when we ran it
On 8 November 2026 we ran the script under PHP 8.3's built-in web server with four workers, unchanged except for the cache folder, against the Site-Shot API:
- The first signed request for
https://www.wikipedia.org/at sizexmade one capture and returned a 480 × 360 JPEG of 31,770 bytes in 4.2 seconds. The second request returned the same bytes from the cache in about a millisecond, with no API call. - With the cached file made eight days old and an invalid key, the call failed; the script served the old file unchanged and did not call again on the next request. A size with no cached copy got an empty 404 marked
no-store.
For the lock and the daily limit, the API address pointed at a slow stand-in that counted calls:
- Six requests at once for six uncached 5-second pages made one call: one image after 5 seconds, five 404s at once. A cached thumbnail requested a second later came back in under a millisecond.
- With an eight-day-old copy on disk and another capture running, the request got the old copy at once.
- Once
DAILY_MAXcaptures had been made that day, a new page got a 404 and no call; after the date changed it was captured. - Forged signatures, and signatures for another size or made with another secret, got 403 and no call; signed
file:,javascript:andftp:addresses got 400.
After the last two edits (parameters sent as arrays refused, the nosniff header), we ran the final script once more against the API: a signed request for https://www.mediawiki.org/ at size m returned a 200 × 150 JPEG in 4.6 seconds with X-Content-Type-Options: nosniff, the repeat came from the cache, and url[]= and sig[]= requests got an empty 400 and 403 even with PHP's error display on.
The live part is two pages on one day; your capture times will differ.
Or fetch the thumbnails ahead of time
If you know your list of links, you can skip the on-request script: a cron job captures each thumbnail into a folder your web server serves, and the pages point at plain files. The mShots guide has that version in PHP and for WordPress, in Your own thumbnails with your own key.
Where the honest limits are
- The free plan has bounds. 100 captures a month, failed ones included, one at a time, from the default location or the US. Every capture counts, so with the seven-day refresh above the free plan covers about 20 thumbnails that are viewed every week (a month can hold five refreshes), and a page that can never be captured costs one capture a day for as long as its signed address is requested. Signed addresses do not expire, so after removing links, change
THUMB_SECRETto revoke the old ones; the cached thumbnails stay valid. - A new thumbnail shows up on a later view. Captures run one at a time, about 4 seconds each in our test and up to the 70-second timeout, and a request that finds one running gets a 404 rather than tie up a PHP worker. So the first visitor to a page with thirty new links sees only a few; after adding links, request each
thumb_src()address once, one after another, withcurlor a script. - No storage on our side. The thumbnails live in your cache folder, which grows with every new link; delete old files from it now and then.
- The facts about other services are a snapshot from requests made on 8 November 2026. Any of them can change, as PagePeeker's free endpoint did.
For larger captures than a thumbnail, see full page vs viewport screenshots; for more about calling the API from PHP, see how to take a website screenshot with PHP.
FAQ
Why do my PagePeeker thumbnails show a broken image?
PagePeeker switched off its free endpoint, free.pagepeeker.com: on 8 November 2026 it answered every request with 410 Gone, and the Internet Archive's last image from it is dated 16 May 2026. The paid PagePeeker API at api.pagepeeker.com still works, from $5.99 a month, but without your account's code or a registered site it returns a placeholder GIF instead of a screenshot.
Is ShrinkTheWeb still working?
Not as of 8 November 2026. ShrinkTheWeb's image host, images.shrinktheweb.com, fails the TLS handshake because its certificate belongs to another server, so thumbnails embedded with xino.php URLs no longer load. The ShrinkTheWeb site has said it is relaunching under new management since at least March 2024; until it does, the thumbnails need another service.
What happened to PageLr?
PageLr's website says its screenshot API was permanently shut down on June 3, 2026, that its API, accounts and stored data are gone, and that users should move to another screenshot API. On 8 November 2026 api.pagelr.com did not answer at all.
Is there a free replacement for website thumbnails with no API key?
Keyless thumbnail URLs that still worked on 8 November 2026 include WordPress.com's mShots at s.wordpress.com/mshots/v1/, and thum.io, whose home page offers up to 1000 impressions per month free without signing up. Neither comes with an agreement, as PagePeeker's free endpoint showed. With a key kept on your server instead, Site-Shot's free plan gives a confirmed account 100 API captures a month, no card needed.
Take a one-off thumbnail free in your browser, no signup, at site-shot.com, or compare API plans on the pricing page.