Image Optimization Pipelines for CMS Assets

A headless CMS ships structured content well but rarely ships production-ready media: its default endpoints hand you original-resolution files with arbitrary query strings. A deterministic pipeline intercepts those payloads, transforms formats, enforces responsive breakpoints, and caches at the edge — closing the gap between editorial uploads and Core Web Vitals targets.

The stages below form one deterministic path from editorial upload to edge delivery:

From editorial upload to edge deliveryA CMS publish yields an asset reference whose URL is normalized, transformed by a framework or CDN optimizer into AVIF, WebP or JPEG, given reserved dimensions and a placeholder, cached immutably at the edge and delivered per locale; editor updates purge or version the affected URLs.CMS publishasset referenceNormalizeURLTransformAVIF / WebP / JPEGReserve spacewidth, height, LQIPEdge cacheimmutableLocalizeddeliveryEditor updatesassetnew version or purge
One deterministic path from upload to pixels, with invalidation built in.
Where to transform imagesTransformation options compared: the CMS's own image API, the framework's optimizer, a CDN image service and build-time processing, on control, cost, latency and caching.OptionControlOperational costCache behaviourCMS image APIURL parametersnoneCMS CDN, versioned URLsFramework optimizerfullcompute per variantapp-level cacheCDN image servicefullper-request pricingedge cacheBuild-time processingfullbuild time growsstatic files
Most sites use the CMS image API or a CDN service; build-time processing suits small static sites.

Integration Contract

Images need a small, strict contract between CMS and frontend. Identity and versioning: every asset URL identifies one version of the file; when the file changes, the URL changes, which most CMS asset CDNs already guarantee through ids or version tokens in the path. Metadata: width, height, alt text and, where cropping matters, a focal point, all delivered with the asset reference. Transformation: one place that turns a reference and a requested size into a URL, used by every component. Budgets: maximum source dimensions and file sizes enforced on upload, so a 40-megapixel photograph never becomes the source of a thumbnail.

Bash
# .env: image pipeline
IMAGE_LOADER=cms            # cms | next | cdn
IMAGE_WIDTHS=320,480,640,768,1024,1280,1600,1920
IMAGE_DEFAULT_QUALITY=70
IMAGE_MAX_SOURCE_PX=6000
IMAGE_CDN_ZONE=example-images

Normalize Payloads at Ingestion

Ingestion starts at the CMS webhook or GraphQL resolver. On publish, extract media references and resolve signed URLs. Contentful, Sanity, and Strapi all expose url fields carrying query parameters, but resizing client-side adds latency and inflates the initial payload. Route through a dedicated image proxy or a framework-native optimizer instead, and normalize the URL before it reaches the render layer:

TypeScript
interface CMSAsset {
  url: string;
  alt: string;
  width: number;
  height: number;
  locale?: string;
}

export function normalizeAssetPayload(asset: CMSAsset): string {
  const { url } = asset;
  const cleanUrl = new URL(url);
  // Strip sizing parameters the optimizer will set itself; keep anything else (such as version tokens).
  for (const key of ['w', 'h', 'fit', 'q', 'fm', 'auto']) cleanUrl.searchParams.delete(key);
  return cleanUrl.toString();
}

Stripping inherited query strings gives downstream components predictable, cache-friendly URLs and keeps transformation parameters and cache headers under your control rather than the CMS’s.

Delegate Transformation to the Framework

Configured correctly, modern frameworks handle format negotiation for you. In Next.js, next/image reads headless sources through remotePatterns, generating srcset and converting formats without manual string-building. See Automating Next.js image optimization with headless CMS for custom loader patterns.

JavaScript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.sanity.io', pathname: '/images/**' },
      { protocol: 'https', hostname: 'images.ctfassets.net', pathname: '/**' }
    ],
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [320, 480, 768, 1024, 1280, 1600],
    minimumCacheTTL: 31536000
  }
};
export default nextConfig;

The built-in loader appends width, quality, and format based on the client Accept header. Paired with @sanity/image-url or contentful-image, you can also pass crop coordinates and focal points. For content-type routing and capability detection, see Handling image format negotiation in headless pipelines.

Cache at the Edge, Invalidate on Publish

Framework optimizers emit Cache-Control: public, max-age=31536000, immutable for transformed assets. That’s ideal until an editor updates an asset, at which point the stale variant persists across every edge node. Wire a webhook to purge the affected paths through the CDN API.

TypeScript
// Edge function for cache invalidation
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const { assetId, locale } = await req.json();
  const purgeUrl = `https://api.cloudflare.com/client/v4/zones/${process.env.CF_ZONE_ID}/purge_cache`;
  
  await fetch(purgeUrl, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.CF_API_TOKEN}` },
    body: JSON.stringify({ files: [`/assets/${locale}/${assetId}-optimized.avif`] })
  });

  return NextResponse.json({ status: 'cache_purged' });
}

Pair immutable caching with stale-while-revalidate to serve cached variants during background refresh. As MDN’s HTTP caching reference notes, long-lived directives plus revalidation cut origin load without serving stale content.

Prefer versioned URLs over purges

Purging transformed variants works, but it is fragile: every width and format of every asset is a separate cached object, and a purge that misses one leaves a stale variant somewhere. Versioned source URLs avoid the problem. When the CMS includes a version or upload id in the asset URL, as most do, a replaced image gets a new URL, every transformed variant gets a new cache key automatically, and nothing needs purging. The old variants simply expire. Where your pipeline controls the URLs, include a content hash or the asset’s revision number in them. Reserve purges for the rare case of removing an image that must disappear immediately, such as a legal takedown, and purge by tag so every variant goes at once.

Localization and Deterministic Routing

Localization adds asset duplication: multilingual sites need region-specific imagery and culturally adapted crops. Resolve the locale tag from the CMS payload, map it to the localized variant, and degrade to a default asset that preserves layout when a variant is missing — the same Content Fallback & Routing logic you apply to content.

A consistent URL structure like /assets/{locale}/{slug}.{ext} yields deterministic cache keys and simpler CDN config, and lets Next.js and Nuxt pre-render localized references at build time. For path structure, see Route Mapping for Multilingual Sites.

Layout Stability

Unsized images are a leading cause of Cumulative Layout Shift (CLS). Enforce explicit width and height plus responsive sizing, and reserve space before the optimized asset loads. Generate placeholders at build time or during payload resolution; CSS aspect-ratio boxes and low-quality image placeholders (LQIP) hold the layout steady across network conditions. See Reducing CLS with headless CMS image placeholders.

Choosing Sizes and Quality

Responsive images only save bytes if the sizes offered match how images are displayed. Pick a width ladder that covers common layout widths at one and two times device pixel ratio, for example 320 to 1920 pixels in eight steps, and give every image a sizes attribute that describes its displayed width at each breakpoint. Without sizes, browsers assume the image fills the viewport and download far larger files than needed for thumbnails and cards. Quality settings between 60 and 75 are usually indistinguishable from higher values in AVIF and WebP; test with your own imagery, since photos, illustrations and screenshots behave differently. Screenshots and diagrams with text often look better as lossless WebP or PNG than as lossy formats at any quality.

Bytes for one article's images on mobileTotal image bytes downloaded for a typical article on a mobile device with original CMS files, with responsive widths but no sizes attribute, and with responsive widths, sizes and modern formats.Original files4800 KBsrcset without sizes1900 KBsrcset + sizes + AVIF520 KB
The sizes attribute alone cut transfer by more than half; formats did the rest.

Art Direction and Focal Points

Responsive sizing scales one image; art direction chooses different crops for different layouts. A wide hero photo that works on desktop can lose its subject on a narrow phone screen. Store a focal point, or hotspot and crop, with each asset in the CMS, and let the transformation step crop around it for each aspect ratio the design uses. Sanity’s hotspot and crop fields, Contentful’s focus areas and Storyblok’s focal point all serve this purpose. When the difference is more than a crop, for instance a different photo for mobile, model it as an optional second image on the block and render a picture element with media queries. Keep art direction in the model rather than in code, so editors can see and adjust how each crop looks in preview.

TSX
// components/hero-image.tsx
import { imageUrl } from "@/lib/images"; // builds CMS image API URLs with width, height, format and focal point

export function HeroImage({ asset, mobileAsset, alt }: { asset: CmsImage; mobileAsset?: CmsImage; alt: string }) {
  const wide = (w: number) => imageUrl(asset, { width: w, aspect: 16 / 7 });
  const tall = (w: number) => imageUrl(mobileAsset ?? asset, { width: w, aspect: 4 / 5 });
  return (
    <picture>
      <source media="(max-width: 767px)" srcSet={[480, 768, 1024].map((w) => `${tall(w)} ${w}w`).join(", ")} sizes="100vw" />
      <img
        src={wide(1280)}
        srcSet={[768, 1024, 1280, 1600, 1920].map((w) => `${wide(w)} ${w}w`).join(", ")}
        sizes="100vw"
        width={1600}
        height={700}
        alt={alt}
        fetchPriority="high"
      />
    </picture>
  );
}

The width and height attributes reserve the desktop aspect ratio; for the mobile crop, add CSS that sets aspect-ratio: 4 / 5 at the same breakpoint, so the reserved box matches the image actually loaded.

Worked Example

A travel magazine served original CMS files, often 4000 pixels wide and several megabytes, through a simple img tag. Mobile LCP on articles was over four seconds, and image bytes made up 85 percent of page weight. The team moved to the CMS’s image API through one URL builder, added a width ladder with accurate sizes attributes, AVIF with WebP and JPEG fallbacks, focal-point crops for heroes, and upload limits in the CMS. Image bytes per article view dropped by about 89 percent, and mobile LCP fell to 2.2 seconds, with no visible loss of quality in side-by-side reviews by the photo desk.

Accessibility and Alt Text

Performance work on images often overlooks the one property that matters to readers who cannot see them. Alt text should come from the CMS, localized per locale, and describe the image’s purpose in context; decorative images get an empty alt attribute so screen readers skip them. Model this explicitly: an image field with a localized alt text and a “decorative” flag, where validation requires either alt text or the flag. Avoid falling back to file names or titles, which produce noise such as “IMG_4821.jpg”. For images reused across pages, allow the alt text to be overridden where the image is used, since the same photo can serve different purposes in different articles. Check alt coverage in the SEO audit, reporting images without alt text and without the decorative flag per content type and locale.

Cost and Capacity

On-demand transformation has a cost model worth understanding before traffic grows. Each unique combination of source, width, format and quality is transformed once and then cached; costs scale with the number of distinct variants, not with page views, as long as caching works. The main risks are unbounded widths, where arbitrary width parameters from clients create unlimited variants, and cache misses from unstable URLs. Restrict widths to the ladder, reject other values, and keep URLs deterministic. Watch the transformation count per day alongside the cache hit rate; a hit rate above 95 percent is typical for a healthy pipeline on a content site. Build-time processing avoids per-request costs entirely but lengthens builds as the media library grows, which is why most larger sites move to on-demand transformation eventually.

Preview & Draft Images

Draft assets are often served from a different host or require the preview token, and optimizers configured only for the delivery host will refuse or fail to fetch them. Allow the preview asset host in the optimizer configuration, or bypass optimization in draft mode and serve the CMS’s own resized URLs, which is simpler and good enough for preview. Make sure preview never writes draft images into the production optimizer cache under URLs that could later be requested publicly.

Error Handling & Resilience

Images fail in ways pages do not: the source host is slow, a transformation times out for an unusually large file, or a format is unsupported. Serve a neutral placeholder of the right dimensions rather than a broken image icon, so layout stays intact. Set timeouts on transformation requests and fall back to the original URL when the optimizer fails, which is slower but visible. Log failures with the asset id so editors can fix problematic uploads, such as CMYK JPEGs or enormous PNGs, at the source.

Testing & Observability

Check the pipeline at three levels. Unit-test the URL builder for widths, formats, crops and versioning. Run a lab check on each template that lists every image request with its transferred size, format and whether its rendered width is much smaller than its intrinsic width, which reveals missing or wrong sizes attributes. In the field, track image bytes per page view and LCP by template, and watch the optimizer’s cache hit rate and transformation errors. A drop in hit rate often means URLs started varying unintentionally, for example through an added query parameter.

Ownership Between Editors and Engineers

Image quality is shared work. Engineers own the pipeline: URL building, formats, widths, caching, placeholders and the budgets enforced in the CMS. Editors and photo desks own the sources: choosing images that suit each role, setting focal points, writing alt text and replacing problematic uploads. The pipeline should make the right choice the easy one. Show editors, in the CMS or in preview, how an image will be cropped at each aspect ratio, warn when a source is too small for its role, such as a 600-pixel image used as a full-width hero, and flag missing alt text before publishing. In return, editors should not need to know about formats or quality settings at all. A short guide listing image roles, their target dimensions and aspect ratios, kept in the CMS itself as help text on each image field, is worth more than any amount of documentation elsewhere.

Rolling Out a Pipeline Change

Changing how every image on a site is served is risky, because a mistake affects every page at once. Roll out by template: switch one template, such as articles, to the new URL builder and component, compare image bytes, LCP and visual quality for a week, then move to the next. Keep the old and new paths behind a flag so a problem can be reverted without a deploy. Before switching, generate a side-by-side review page that renders a sample of real images through both paths at common widths, and have someone with an eye for image quality, often from the photo or design team, approve the settings. That review catches issues numbers miss, such as banding in gradients at low quality or over-sharpened text in screenshots.

Frequently Asked Questions

Should we use the CMS’s image API or Next.js image optimization?

If the CMS image API supports resizing, formats and focal points, use it: it runs on the CMS’s CDN at no compute cost to you. Use the framework optimizer when you need processing the CMS cannot do or when assets come from several sources.

Is AVIF always better than WebP?

Usually smaller at the same visual quality, but slower to encode. For on-demand transformation, cache aggressively; for very large images, compare both.

How do we stop editors from uploading huge files?

Validate dimensions and file size on upload in the CMS, and document target sizes per image role, such as hero, card and inline.

Do we need LQIP placeholders?

Reserved dimensions prevent layout shift; placeholders only improve perceived loading. Use a dominant colour or tiny blurred preview for large hero images, and nothing for small ones.

Should SVG images go through the pipeline?

No. Serve SVGs as they are, with their own long cache lifetime, after sanitizing uploads to remove scripts and event handlers, since resizing and format conversion do not apply to vector graphics.

How do we handle GIF animations?

Convert them to short muted looping videos, which are far smaller, or to animated WebP, and keep a static poster frame for reduced-motion users. Large GIFs are among the heaviest assets on content sites.

Do image CDNs work with static exports?

Yes. With a static export there is no framework optimizer, so point image URLs at the CMS image API or a CDN image service directly through the URL builder, which then plays the role the optimizer would otherwise play.