How I configure Sanity image URL auto=format with next/image to serve WebP and AVIF

Jun 02, 2026 · 6 min read

The question of who converts the image — Sanity's CDN or Next.js's image optimiser — comes up on almost every project. Get it wrong and you are double-encoding a JPEG to WebP twice, paying bandwidth twice, and caching the result under two different keys. The phrase sanity image url webp avif automatic format nextjs is exactly the decision point: one of them should own format negotiation, not both.

What auto=format actually does on the Sanity CDN

When you append ?auto=format to a Sanity image URL, Sanity's CDN reads the Accept header from the incoming request and returns the best format the browser supports — WebP or AVIF if available, JPEG/PNG as a fallback. The transformation is done at Imgix (which powers Sanity's asset pipeline), cached at the edge, and served with a long Cache-Control header keyed to the full URL including query parameters.

This is fast and costs nothing extra on Sanity's free or Growth plans. The CDN URL looks like this:

// lib/sanity-image-url.ts
import imageUrlBuilder from '@sanity/image-url'
import { client } from './sanity-client'
 
const builder = imageUrlBuilder(client)
 
export function urlFor(source: SanityImageSource) {
  return builder.image(source)
}
 
// Usage — note: NO auto=format yet
const src = urlFor(post.coverImage)
  .width(1200)
  .height(630)
  .fit('crop')
  .url()
// → https://cdn.sanity.io/images/<projectId>/<dataset>/...jpg?w=1200&h=630&fit=crop

The Accept header is what triggers format selection. A direct <img> or a fetch() from a server component will carry Accept: image/avif,image/webp,*/* in modern browsers and Chromium-based crawlers. The CDN responds with AVIF or WebP accordingly.

Why you should not combine auto=format with next/image as an external image

next/image works by proxying images through /_next/image?url=...&w=...&q=.... When Next.js receives a request to that endpoint it:

  1. Fetches the origin URL (your Sanity CDN URL)
  2. Runs Sharp locally (or on the Vercel image optimiser) to resize and re-encode
  3. Serves the result with its own cache headers

If your Sanity URL already has auto=format, the /_next/image proxy fetches a WebP from Sanity, then Sharp re-encodes that WebP into another WebP (or AVIF). You have now paid for two encode passes and the quality is slightly degraded because lossy-to-lossy re-encoding accumulates artefacts. The cache key at /_next/image is the full origin URL string, so ?auto=format in that URL means every format+width combination gets its own cache entry — which is fine but pointless if Next.js is doing the format work anyway.

The fix is to pick a lane:

  • Lane A – let Sanity CDN do it: Use a raw <img> tag (or next/image with unoptimized), append auto=format and q=80 directly on the Sanity URL, and skip Next.js image optimisation entirely.
  • Lane B – let next/image do it: Strip auto=format from the Sanity URL, configure remotePatterns for cdn.sanity.io, and let Next.js handle format negotiation and resizing.

For most content sites I default to Lane B because it gives me sizes, fill, and priority props without extra plumbing, and Vercel's image cache is already warm on the same edge network. The trade-off is that every unique w value in sizes produces a separate cached variant at /_next/image.

Lane B setup: next/image without double-encoding

// next.config.ts
import type { NextConfig } from 'next'
 
const config: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.sanity.io',
        pathname: '/images/**',
      },
    ],
    // Vercel supports avif by default; on self-hosted you need Sharp >= 0.33
    formats: ['image/avif', 'image/webp'],
    // Match Sanity's default quality to avoid surprise
    quality: 80,
  },
}
 
export default config
// components/sanity-image.tsx
import Image from 'next/image'
import { urlFor } from '@/lib/sanity-image-url'
import type { SanityImageSource } from '@sanity/image-url/lib/types/types'
 
interface Props {
  image: SanityImageSource & { alt?: string }
  width: number
  height: number
  priority?: boolean
  className?: string
}
 
export function SanityImage({ image, width, height, priority, className }: Props) {
  // Build the Sanity URL WITHOUT auto=format and WITHOUT width/height
  // next/image owns resizing; Sanity CDN just serves the original at base quality
  const src = urlFor(image)
    .fit('max') // avoid upscaling on Sanity side
    .url()      // no .width() call here — next/image handles that
 
  return (
    <Image
      src={src}
      alt={image.alt ?? ''}
      width={width}
      height={height}
      priority={priority}
      className={className}
      // Sanity metadata already carries real dimensions; pass them
      // so next/image does not need to probe the origin
    />
  )
}

Notice that I am not calling .width() on the builder when passing to next/image. If you set ?w=1200 on the Sanity URL and then next/image requests ?url=...%3Fw%3D1200&w=800, Sharp will first decode a 1200-wide image and resize down to 800 — wasted decode work. Omitting the width on the Sanity side means the CDN serves the original (or near-original) and Sharp receives clean pixels.

When to use Lane A instead

Lane A (Sanity CDN owns everything, unoptimized on next/image or plain <img>) makes more sense when:

  • You are self-hosting Next.js without Sharp or a paid image service
  • The images are served from a CMS preview route where you cannot add remotePatterns (draft content domains sometimes differ)
  • You need auto=format&dpr=2 for high-DPI but do not want to enumerate 2x srcset entries manually

In that case, build the full URL with quality and format on the Sanity side and pass unoptimized to avoid the proxy:

// Lane A — Sanity CDN handles format, quality, and DPR
const src = urlFor(image)
  .width(800)
  .auto('format')
  .quality(80)
  .url()
 
// next/image with unoptimized bypasses /_next/image entirely
<Image src={src} alt={alt} width={800} height={533} unoptimized />

The Accept header is still sent because Vercel's edge (or any modern CDN in front of your Next.js origin) forwards it, and Sanity's CDN will serve AVIF or WebP as appropriate.

Cache key hygiene

One operational gotcha: if you switch between Lane A and Lane B mid-deployment, your old /_next/image cache entries (keyed on URLs with auto=format) and your new ones (keyed on URLs without it) coexist under different keys. On Vercel you can flush the image cache from the project settings. On self-hosted deployments the cache lives in .next/cache/images and is wiped by a full redeploy anyway. Make the lane decision once per project and commit it to an ADR so the next developer does not mix them.

The short rule: auto=format and next/image optimisation are both format negotiation — they are additive overhead, not complementary. Pick one, strip the other, and your payloads stay small without any double-encoding tax.

Related posts

All posts →