How imported images get deterministic, cacheable URLs from their webpack hash.
<DitheredImageSSR src={importedImage} /> is the most ergonomic way
to use @ditherkit/next. You import an image file the normal Next.js
way, pass it as src, and everything else — cache keys, route
URLs, invalidation — happens automatically. This page explains how.
Next.js (via webpack or Turbopack) understands image imports:
import portrait from '@/assets/portrait.jpg'// `portrait` is a StaticImageData object:// {// src: '/_next/static/media/portrait.a1b2c3d4.jpg',// width: 1200,// height: 1800,// blurDataURL: 'data:image/...',// }
The bundler copies the file to .next/static/media/ with a
content hash in the filename (portrait.a1b2c3d4.jpg). That
hash changes whenever the source file changes, and that's the hook
we use for cache invalidation.
Take the webpack src path from portrait.src. For
/_next/static/media/portrait.a1b2c3d4.jpg that's the entire
string. The webpack content hash is embedded in the filename, so
the path itself changes whenever the source changes.
Serialise the dither parameters into a URL-safe string:
fs_th128_pl000000ffffff_brp0_ctp0_w800_h600_px1_gsa.
Each underscore-separated chunk encodes one parameter (algorithm
slug, threshold, palette hex, brightness, contrast, width, height,
pixelSize, grayscale).
Combine src and params: src + '|' + paramString.
MD5 the combined string and take the first 12 hex chars.
That's the cacheKey.
The URL is deterministic — the same source file with the same
dither parameters always produces the same URL. That's what makes
ISR caching work: once a URL is cached, every subsequent request
with the same parameters hits the cache instead of re-running Sharp.
The route handler also recomputes the cacheKey at request time and
rejects mismatches with a 400. This binds the path-segment cacheKey
to the ?src= query so an attacker can't mint distinct cache
entries by varying the path independently of the query.
On Vercel, _next/static/ files are deployed exclusively as CDN
assets — they are not bundled into the serverless function
container. The on-disk readFileSync always fails, and the route
handler falls back to fetching the image over HTTP from the CDN.
This HTTP fallback is the primary read path on Vercel, not a
dev-mode workaround.
This is automatic and requires no extra configuration beyond
calling withDitherkit() in your next.config.
On preview deployments with
deployment protection
enabled, the internal HTTP fetch returns 401 because the CDN
requires authentication. To fix this:
Go to your Vercel project → Settings → Deployment Protection
Enable Protection Bypass for Automation
Redeploy
This makes VERCEL_AUTOMATION_BYPASS_SECRET available as a system
environment variable. The route handler automatically sends it as
an x-vercel-protection-bypass header on the internal fetch,
bypassing protection for that request only. Production deployments
are unaffected.
The source file can live anywhere. The import path is what
matters for the developer, but the bundler output path is what
goes into the URL. Webpack/Turbopack copies every imported image
into the same flat .next/static/media/ directory regardless of
where it came from, so:
import hero from '@/assets/marketing/hero.jpg'import avatar from '@/components/profile/avatar.png'import screenshot from '../../fixtures/test-image.jpg'
All three end up in .next/static/media/, all three get the same
treatment, all three work the same way with <DitheredImageSSR />.
No configuration needed, no "registry" to maintain.
The route handler does take the webpack src path as a query
parameter — it's how the handler knows which file to read. But the
cache key in the URL path is deliberately separate because:
Cache keys are short (12 hex chars) whereas webpack src paths
are long (/_next/static/media/portrait.a1b2c3d4.jpg).
The cache key sits in the URL path (which CDNs key their cache
on), not the query string (which some CDNs ignore).
Encoding the src path in the path would produce ugly URLs full
of %2F escape sequences.
So the cache key is a content-addressed handle that's cheap to
match against a CDN cache, and the full src path rides along in
the query string for the server to actually find the file on disk.
The server-side cacheKey-vs-src binding check ensures the two
always agree.