ditherkit
Integrations

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-next and apps/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 through next/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 withDitherkit config and same <DitheredImageSSR /> import work for both. Only the route handler subpath differs (/route/app vs /route/pages). Both fully tested in CI.
  • Turbopack and Webpack — both bundlers understand the new URL(..., import.meta.url) worker pattern that @ditherkit/react uses, so no extra bundler config is required.

Quick start

pnpm add @ditherkit/next @ditherkit/react sharp

Setup (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: imported StaticImageData SSR, 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/next depends on Sharp, which has native modules and doesn't run in edge runtimes. Use @ditherkit/react for 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.

On this page