Next.js
First-class support. App Router and Pages Router. Server-side with @ditherkit/next, client-side with @ditherkit/react.
Status: ✅ Supported. App Router and Pages Router both first-class. Mirror example apps at
apps/example-nextandapps/example-next-pages.
Next.js is the most polished path. Two packages, both designed to
cohabit cleanly. @ditherkit/next uses a singleton pattern: one
withDitherkit call in next.config writes a build-time env bridge
that the component, the route handler, and Next.js itself all read
from. No factory call, no lib/ files for the common case.
What works
<DitheredImageSSR />from@ditherkit/next— server-rendered, ISR-cached dithered images that flow throughnext/image. Use this for above-the-fold content, SEO-indexable images, and anything that's the same for every user.<DitheredImage />from@ditherkit/react— client-side Web Worker rendering for interactive slider adjustments, live previews, and user uploads.- Both together in one app — the two packages coexist cleanly. See the example apps for exactly this pattern.
- App Router and Pages Router — same
withDitherkitconfig and same<DitheredImageSSR />import work for both. Only the route handler subpath differs (/route/appvs/route/pages). Both fully tested in CI. - Turbopack and Webpack — both bundlers understand the
new URL(..., import.meta.url)worker pattern that@ditherkit/reactuses, so no extra bundler config is required.
Quick start
pnpm add @ditherkit/next @ditherkit/react sharpSetup (App Router or Pages Router)
Three steps. No lib/ files, no factory call.
1. next.config.ts — configure once
import type { NextConfig } from 'next'
import { withDitherkit } from '@ditherkit/next/config'
const config: NextConfig = {
// your existing config
}
export default withDitherkit(config, {
routeBase: '/api/dithered',
// Default is deny-all; opt in to allow external URLs:
// externalImages: { inheritRemotePatterns: true },
})2. Your page — render the component
// app/page.tsx (App Router) — or pages/index.tsx (Pages Router)
import { DitheredImageSSR } from '@ditherkit/next'
import portrait from '@/assets/portrait.jpg'
export default function Page() {
return (
<DitheredImageSSR
src={portrait}
alt="A portrait, dithered on the server"
algorithm="Atkinson"
priority
/>
)
}The component reads its routeBase and the externalImages allowlist
from the env bridge that withDitherkit writes — no factory call,
no shared lib module to thread between files.
getDitheredImageUrl(props) is also exported from @ditherkit/next
as the URL-only sibling of the component — same dimension resolution,
same cache key, useful for OG images, <picture>/srcset pipelines,
and CSS background-image. See Building URLs without the
component.
3. The route handler — one re-export
// app/api/dithered/[cacheKey]/[...params]/route.ts (App Router)
export { GET } from '@ditherkit/next/route/app'// pages/api/dithered/[cacheKey]/[...params].ts (Pages Router)
export { default } from '@ditherkit/next/route/pages'One file handles both StaticImageData and string sources.
The component, URL serialisation, cache headers, and Sharp pipeline
are all identical between the two routers — the only thing that
differs is which /route/{app,pages} subpath the route file
re-exports from.
How configuration flows without a lib file
@ditherkit/next exposes the route handler subpaths
(/route/app, /route/pages) from a separate module than the
component (/) because the handler statically imports Sharp, which
uses Node built-ins and can't be bundled for the browser. The
component, the URL helper, and the withDitherkit config helper
are all client-safe.
Configuration flows from next.config.ts to runtime via the env
bridge: withDitherkit writes resolved routeBase, externalImages,
and limits values into nextConfig.env, Webpack's DefinePlugin
inlines them at build time, and both the component and the route
handler read them via direct process.env.NAME access. The
inlining means there's no runtime cost and no shared JavaScript
module is needed to thread configuration between files.
For multi-instance use cases (two distinct route bases in one app),
the legacy createDitherkit factory pattern is still available
from @ditherkit/next/app and @ditherkit/next/pages. See
Route setup § Multi-instance.
Example apps
apps/example-next— App Router. Every integration point exercised on a single page: importedStaticImageDataSSR, external URL SSR, and a client-side interactive demo via@ditherkit/react.apps/example-next-pages— Pages Router, feature-for-feature mirror of the App Router example.
Both apps use the singleton pattern with zero lib/ files — see
Route setup for the full story.
What doesn't work (yet)
- Partial Pre-Rendering (PPR). Untested with Next.js 16's PPR
mode.
<DitheredImageSSR />is a server component that hits a route handler, so it should work fine in the PPR static shell, but we haven't actually verified. - Edge runtime.
@ditherkit/nextdepends on Sharp, which has native modules and doesn't run in edge runtimes. Use@ditherkit/reactfor client-side rendering at the edge, or render at build time with@ditherkit/core+ sharp in a Node build script. - Parallel routes / intercepting routes. Should work (the route handlers are just regular route handlers) but untested.