my-library
Cover images
Three thousand covers, none of them repeating — which is what broke Vercel's image optimizer, and why the app now bypasses it.
Where covers come from
OpenLibrary publishes cover art on its own CDN at covers.openlibrary.org, in three sizes. The import stores the URL for the right one per context rather than a single source:
| Column | Variant | Used by |
|---|---|---|
thumbnailUrl | -M.jpg | The shelf — 24 tiles per page, ~180px wide. |
coverUrl | -L.jpg | One book's detail page, at 240px and worth the extra pixels. |
Both are derived from the same cover_i the search response returns, so storing two URLs costs nothing but a string and removes any need to rewrite URLs at render time.
The optimizer ran out
The site originally rendered covers through next/image with Vercel’s optimizer, which is the default and normally the right answer. Covers started failing to load. Nothing was broken — the free tier’s 5,000 image transformations had simply been consumed.
Why caching never saved it
The optimizer bills per unique image and width. Every one of the 3,367 books has a cover no other book shares, plus a second larger one on its detail page. Because nothing repeats, a cache hit is only ever possible on a cover already seen — so browsing the shelf accrues new transformations more or less monotonically until the allowance is gone. Paginating at 24 a page slows the burn; it cannot stop it.
The work was redundant anyway
This is the part that made the decision easy rather than merely necessary. The optimizer’s job is to resize an oversized original and convert it to a modern format. But OpenLibrary already publishes-S/-M/-L variants, and the import already stores the correct one for each context — so the optimizer was re-resizing images that arrived pre-sized, and paying per book to do it.
images: {
unoptimized: true,
// Kept although unoptimized bypasses the check: it is the record of which
// hosts a cover may come from, and what makes optimization safe to re-enable.
remotePatterns: [
{ protocol: "https", hostname: "covers.openlibrary.org", pathname: "/b/id/**" },
],
}remotePatterns stays deliberately. With unoptimized set it does nothing, but it documents the only host a cover is permitted to come from, and it means turning optimization back on is a one-line change rather than a rediscovery.
What this gives up
Automatic WebP/AVIF conversion, and responsive srcsetgeneration. For small JPEGs already sitting behind a CDN, that is a fair trade against a cost that grows with the size of the library — the one thing here guaranteed to keep growing.
next/image is still the component in use, so the layout benefits survive: fill inside a fixed 2:3 box means the grid reserves space before anything loads and never reflows as covers stream in. With 24 tiles that would otherwise be very visible.
<div className="aspect-cover relative w-full overflow-hidden bg-raised">
<Image src={src} alt="" fill sizes="…" priority={priority} className="object-cover" />
</div>Books with no cover
About 10% of the library has no artwork. Those tiles render a spine-and-title placeholder rather than a broken-image icon or an empty box — at a glance the shelf still reads as books, and the title is legible, which is the information the cover would have carried anyway.
If this is ever revisited
Self-hosting the covers would remove the dependency on OpenLibrary’s CDN and make optimization affordable again, since the transformation count would be bounded by the library rather than by browsing. That is a storage bill and an import step in exchange for a rendering bill — worth it only if OpenLibrary becomes unreliable.
The import that produces these URLs is covered in OpenLibrary import.