payload-images
Upload a picture once, then request any size, crop, or format from a URL — rendered on demand and cached forever.
@pro-laico/payload-images optimizes images on demand. Upload a picture once, then ask for any
size, crop, or format with a URL. The plugin renders each variant the first time it's requested,
then caches it forever.
It's the Sanity-style image pipeline for Payload:
art-directed focal cropping, AVIF/WebP negotiation, instant placeholders, and a durable cache — all
driven from your CMS. Unlike Payload's built-in imageSizes, you never pre-declare a fixed set of
sizes; any size is available the moment a layout asks for it.
pnpm add @pro-laico/payload-imagesRequirements
Next.js 15+, plus Payload ^3 and React 19. This plugin isn't framework-agnostic and the next
peer isn't optional: the transform endpoint, prewarming, and preset generation all persist their
work with Next's after() (next/server), which is what lets a request return the bytes and store
the variant afterwards. Without it there's no way to cache a generated variant off the response
path — the endpoint is the plugin.
What's included
- Never bigger than it needs to be: every layout requests the exact size, crop, and format it needs; the endpoint renders it once on demand, then caches it forever.
- Focal-point cropping, subject-aware by default: uploads get a saliency-detected focal point when the analysis finds a subject and the editor hasn't set one; refine it once and every crop, at any ratio, stays centered on it.
- Modern formats, negotiated: AVIF / WebP served to browsers that accept them, with a graceful fallback.
- One component, one helper:
imageForfetches a render-ready doc,<ResponsiveImage>paints it — srcset, placeholder, and crop included, zero client JS. - Instant placeholders, cropped like the image: a ladder of quality tiers stored at upload; every read gets a finished placeholder focal-cropped to the exact ratio being rendered —
smby default,.blur(tier)to pick,blur: falseto opt out. - Self-busting cache, smart prewarming, instant fallbacks: URLs refresh themselves on change, prewarming generates what your site actually serves before anyone asks, and a cold request serves a nearby stand-in instantly.
Sample images rendered in the frontend by the images-sandbox example repo
Quickstart
Add the plugin
This registers an images upload collection with the transform pipeline attached.
import { buildConfig } from 'payload'
import sharp from 'sharp'
import { imagesPlugin } from '@pro-laico/payload-images'
const serverURL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000'
export default buildConfig({
sharp, // required: the endpoint resizes/crops with Sharp
serverURL, // used to read originals from cloud/relative storage
plugins: [imagesPlugin()],
})Want it called media? Pass
imagesPlugin({ collections: { images: { slug: 'media' } } }) to rename the collection the plugin
registers — and delete your own, since this one is a full upload collection. Add your fields in the
same override. See
Renaming a collection.
Keep Sharp out of the bundle
Sharp ships a native binary that breaks when bundled, so tell Next not to (Turbopack and webpack both need this):
// next.config.ts
const nextConfig = { serverExternalPackages: ['sharp'] }
export default nextConfigGenerate the admin import map
collections.images.options.focalUI is on by default, so the plugin registers custom admin
components — the focal-point picker and the Preset manager panel. Regenerate the import map so
they load:
pnpm payload generate:importmapUpload an image
Start the app, open the images collection in the admin, and upload a picture. Copy its document
id — the next step renders from it.
Render your first image
// src/lib/imageFor.ts — seed the getter once
import config from '@payload-config'
import { getPayload } from 'payload'
import { createImageFor } from '@pro-laico/payload-images'
export const imageFor = createImageFor(getPayload({ config }))// any server component
import { ResponsiveImage } from '@pro-laico/payload-images/components/image'
import { imageFor } from '@/lib/imageFor'
const img = await imageFor(id).aspectRatio('16:9').fetch()
return img && <ResponsiveImage {...img} sizes="(max-width: 768px) 100vw, 50vw" />That's a focal-cropped, responsive, placeholder-backed <img>. Rendering images
covers everything it just did.
Explore
Rendering images
Fetch a render-ready doc with imageFor and paint it with <ResponsiveImage> — plus raw URLs for OG tags and email.
Image URLs
The /api/img endpoint: URL anatomy, every transform parameter, and format negotiation.
Image metadata
What upload-time analysis stores on every doc: placeholder tiers, color palette, alpha flags, and the focal layers.
Caching
Content-addressed caching, self-revalidation, abuse limits, and the nearby-quality fallback.
Prewarming
Strategies that pre-generate the renders your site actually serves — before the first visitor asks.
Collections
The collections it registers, extending your own upload collection, and seeding.
Reference
Plugin options, environment variables, CLI commands, endpoints, and exports.
Troubleshooting
Images not optimizing, missing admin panels, over-fetching, wrong crops, stale renders — symptom-first fixes.
Plugin options
Zero-config by default — every option has a sensible default. See Reference for options, environment variables, CLI commands, endpoints, and exports.
Troubleshooting
Fix a stuck seed fast — the seed button that won't show, the destructive reset, the Node 24 CLI crash, and stale typed refs, each symptom-first.
Rendering images
Fetch a render-ready image doc with imageFor and paint it with <ResponsiveImage> — srcset, placeholder, and focal crop included.