Zac Braddy
Contract Software Engineer

Decisions

Image loader, no backend

ADR 0014

Context

The static-export decision bought a deploy with no server. It also took something away: static export disables Next's built-in image optimiser. That forces a sharp question. Do I add a backend to my deploy purely to serve optimised images, or do I keep the deploy completely static and let managed infrastructure do the optimising?

Adding image optimisation back the "normal" way means bundling an image function into the deploy, which is a backend I'd own and maintain solely to load pictures. That directly contradicts the zero-server promise I'd just made.

Decision

No image backend. Point next/image at Netlify's managed Image CDN with a URL. The optimisation still happens, resizing, re-encoding, and caching at request time, it just runs on managed infrastructure I don't own, deploy, or maintain. My loader isn't an optimiser at all; it's a URL builder:

const netlifyImageLoader = ({ src, width, quality }) => {
  const params = new URLSearchParams({
    url: src,
    w: String(width),
    q: String(quality || 75),
  });
  return `/.netlify/images?${params}`;
};

export default netlifyImageLoader;

It's wired in via images: { loader: 'custom', loaderFile: './src/image-loader.ts' }. Because a custom loader replaces the default optimiser, this is also what makes next/image legal under output: 'export' at all, so there's no images.unoptimized flag fighting it.

The fact that this is about five lines of stable glue is a welcome bonus, not the reason. The reason is keeping the deploy purely static with no backend of my own to keep alive.

Consequences

next/image works under static export with images routed through the Image CDN, verified by the built output emitting /.netlify/images?... URLs and no serverless functions. One detail mattered for local development: the optimiser endpoint only exists on Netlify, so the loader short-circuits under NODE_ENV=development to return the plain public/ path, which lets real images render under next dev while production builds stay unchanged.

Rejected alternatives

images.unoptimized: true was rejected: it ships unoptimised images and throws away the Image CDN, and it's redundant once you have a custom loader anyway.

Next's built-in optimisation via the Netlify Next runtime was rejected on principle: it serves images by bundling an image function into the deploy, reintroducing exactly the backend the static-export decision exists to avoid. It isn't a dependency I declined to save maintenance; it's architecturally incompatible with a zero-functions static export.

Status / trail

Accepted and in force. Images serve through the managed CDN with no functions in the deploy, confirmed on a Netlify preview before cutover.