How to Build an Asset Pipeline With Sanity's Image API
Your marketing team drops a 6000-pixel hero image straight from a designer's export into the CMS, and three weeks later someone notices the mobile bounce rate on the campaign landing page.
Your marketing team drops a 6000-pixel hero image straight from a designer's export into the CMS, and three weeks later someone notices the mobile bounce rate on the campaign landing page. The culprit is a 4MB PNG being served, unresized, to a phone on a train. Multiply that across a few hundred editors and a decade of content, and your asset layer becomes the single biggest drag on your Core Web Vitals, your CDN bill, and your build times. Most teams patch this with a tangle of build-time image scripts, a separate DAM, and a lot of hope.
Sanity treats assets as first-class, queryable content rather than files bolted onto a folder. Its Content Operating System stores every upload in Content Lake with structured metadata, and its Image API lets you request exactly the crop, format, and dimensions you need at request time, from a URL. That reframes the asset pipeline from a batch job you run and maintain into a transformation you compose on demand.
This guide walks through building that pipeline: how uploads and metadata work, how to derive responsive variants without a build step, how to wire it into a frontend, and how it holds up operationally against the alternatives you might otherwise stitch together yourself.
The failure mode: assets as files, not content
The default mental model for images in a CMS is a filing cabinet. You upload a file, it lands in a folder or an S3 bucket, and the CMS hands your frontend a URL to that raw file. Everything downstream, resizing, format negotiation, cropping for different aspect ratios, art direction on mobile versus desktop, becomes your problem to solve somewhere else. Teams typically solve it in one of three painful ways: a build-time pipeline (sharp scripts, gulp tasks, Next.js Image on a build server) that recomputes derivatives on every deploy and balloons build times; a bolt-on image CDN (Cloudinary, imgix) that adds a vendor, a bill, and a second source of truth for the same asset; or manual discipline, asking editors to export the right sizes, which fails the moment a deadline hits.
The deeper problem is that a file has no idea what it is. It does not know its dominant color, its dimensions, whether it contains a face, or where the meaningful subject sits in the frame. So every consumer of that image has to re-derive that context, or go without it and ship layout shift and bad crops. When the same hero image is reused across a landing page, an email, a native app, and a partner syndication feed, four different systems each reinvent the same handling.
An asset pipeline worth building inverts this. The asset carries its own structured metadata, the transformations are described declaratively at the point of use, and there is exactly one canonical original that every channel derives from. That is the difference between managing files and modeling content, and it is the first pillar of how Sanity approaches the problem: model your business, including the shape of your media, so that everything downstream can query rather than guess.
How uploads and metadata work in Content Lake
When you upload an image to Sanity, it does not just get stashed as a blob. It becomes an asset document of type `sanity.imageAsset` inside Content Lake, the same queryable store that holds the rest of your content. That document carries structured metadata computed at ingest: the original dimensions and aspect ratio, the file size and MIME type, a dominant-color palette, a low-quality image placeholder (the LQIP, a tiny base64 blur you can render instantly), and a perceptual hash for deduplication. Because it is a real document, you can query it with GROQ alongside everything else.
That matters operationally. You can ask Content Lake, in one query, for every image over a certain file size, every asset not referenced by any document (orphans you can safely garbage-collect), or every product photo missing alt text. A GROQ projection like `*[_type == "sanity.imageAsset" && size > 2000000]{ _id, originalFilename, size, "dims": metadata.dimensions }` turns an audit that would otherwise be a scripting exercise against a storage bucket into a single round trip. Deduplication is automatic: upload the same file twice and Content Lake stores one asset and points both references at it.
The reference model is the other half. A document does not embed a copy of the image; it holds a reference to the asset, plus per-usage crop and hotspot data. So the same original can be cropped as a wide banner in one place and a square thumbnail in another, with the editor setting the hotspot once in Sanity Studio's image input. The hotspot marks the part of the image that must survive any crop, which is exactly the context a bare file throws away. Everything the Image API does downstream reads from this structured foundation rather than reprocessing pixels blindly.
Deriving responsive variants with the Image API
The Image API is a URL contract. Every asset has a canonical CDN URL, and you shape the delivered image by appending query parameters to it. Want a 800-pixel-wide WebP at 80 quality, cropped to the editor-defined hotspot? You express that in the URL: `?w=800&fm=webp&q=80&fit=crop`. The transformation runs at the edge, the result is cached, and no build step recomputes anything. Add a new breakpoint to your design six months later and you simply request a new width; there is no pipeline to re-run and no derivatives to regenerate and store.
In practice you do not hand-write these URLs. The `@sanity/image-url` helper builds them from the asset reference and applies the hotspot and crop the editor set in the Studio, so `urlFor(image).width(800).format('webp').quality(80).url()` produces a correct, hotspot-aware URL every time. To ship a genuinely responsive image you generate a `srcSet` across your breakpoints, let `fm=auto` negotiate AVIF or WebP based on the browser's Accept header, and render the LQIP from metadata as a blurred placeholder so there is no layout shift while the real image loads.
The operational win is that variants are ephemeral and free to add. A build-time pipeline forces you to decide your full matrix of sizes and formats up front, because each one is an artifact you compute and store; changing it means a full rebuild. With request-time transformation, the matrix lives in your frontend code as parameters, the CDN caches whatever gets requested, and unused variants simply cost nothing because they are never generated. You trade a maintained batch job for a cache that fills itself on demand, which is the operational posture you actually want for a media library that grows every day.
Wiring it into a modern frontend
On the frontend, the goal is to feed the browser a correct `srcSet`, `sizes`, width, height, and a placeholder, so the layout is stable before a single pixel of the real image arrives. Because Sanity stores original dimensions in metadata, you always know the intrinsic aspect ratio, which lets you set explicit `width` and `height` attributes and eliminate cumulative layout shift, the CLS that quietly tanks Core Web Vitals. You query the asset reference and its `metadata.dimensions` and `metadata.lqip` in the same GROQ query that fetches the surrounding content, so there is no second request to size an image.
In a Next.js app you can either drive a plain `<img>` with a generated `srcSet` from `@sanity/image-url`, or use a custom loader so `next/image` delegates resizing to Sanity's edge instead of the Next image optimizer, which keeps optimization off your serverless function and on infrastructure built for it. The Astro, Remix, and Next.js starters that Sanity publishes wire this up out of the box. Because the transformation is a URL, it works identically across frameworks; there is no framework-specific plugin to keep in sync.
The reason this holds together is that the frontend is querying content, not files. GROQ hands you the reference, the dimensions, the palette, and the placeholder in one round trip, and the Image API turns the reference into whatever concrete rendition each viewport needs. When an editor re-crops the hero in the Studio, the hotspot changes in Content Lake, every derived URL reflects it on the next request, and Visual Editing lets that editor see the result against the live layout without leaving the editorial context. The pipeline has no separate build to trigger and no cache to manually bust.
Operating the pipeline at scale: governance and cleanup
A pipeline that works for one landing page has to survive years of accumulation, and that is where the file-cabinet model quietly rots. Orphaned assets pile up, nobody knows which images carry usage rights, and the same logo exists in nine slightly different exports. Because Sanity assets are documents, the operational hygiene becomes queryable rather than archaeological. A scheduled Function can run a GROQ query for assets with zero incoming references and flag or delete them, turning orphan cleanup into an automated job instead of a quarterly panic.
Governance rides on the same foundation. Roles & Permissions control who can upload and who can publish the documents that reference assets, Audit logs record who changed what, and Content Releases let a batch of asset-and-content changes ship together and roll back together, so a rebranded image set does not go live half-swapped. Alt-text policy stops being a wish: a GROQ query surfaces every image reference missing an accessibility description, and the App SDK plus Functions can enforce or auto-suggest it in the Studio rather than in a spreadsheet.
On compliance, Sanity runs on infrastructure covered by SOC 2 Type II, supports GDPR obligations, offers regional hosting for data residency, and publishes its sub-processor list, so the media you store is governed under the same terms as the rest of your content operation. This is what modeling assets as content buys you at scale: the metadata, the queries, the permissions, and the automation are all part of one shared foundation rather than four disconnected systems, which is precisely the end-to-end posture a Content Operating System is meant to provide versus a CMS that stops at handing you a file URL.
Request-time asset transformation approaches compared
| Feature | Sanity | Contentful | Strapi | Cloudinary (bolt-on CDN) |
|---|---|---|---|---|
| Asset as queryable content | Every upload is a `sanity.imageAsset` document in Content Lake, filterable by size, references, and metadata in one GROQ query. | Assets are entries with fields and can be queried via the Content Delivery API, though palette and LQIP are not computed natively. | Media Library entries stored via an upload provider; queryable through REST or GraphQL against the entry, not a rich metadata document. | Assets live in a separate DAM outside your CMS, so querying media alongside content means reconciling two systems. |
| Ingest metadata | Auto-computes dimensions, aspect ratio, dominant-color palette, LQIP blur placeholder, and a dedup hash at upload time. | Stores dimensions and file metadata; color palette and blur placeholder are typically derived client-side or by an add-on. | Stores basic file metadata and optional format variants configured in the upload provider; no palette or LQIP by default. | Rich metadata including color and face detection, but as a parallel source of truth from your CMS content. |
| Responsive derivatives | URL parameters (`w`, `fm=auto`, `q`, `fit=crop`) transform at the edge with no build step; `@sanity/image-url` builds hotspot-aware srcSets. | Images API supports width, format, quality, and fit params on delivery URLs, negotiated at request time. | Responsive formats generated at upload as fixed breakpoints, or offloaded to an image CDN provider you add. | Strong request-time transformations via URL, which is the whole product, but on assets stored apart from your content model. |
| Editor-defined crop context | Hotspot and crop set once in Sanity Studio's image input; every derived rendition honors the subject the editor marked. | Focus area can be set per asset and applied via the API, though the editing surface is fixed rather than customizable. | Crop and focal point depend on the media library and provider; no unified hotspot carried into every transform. | Gravity and focal point are set in the DAM UI, separate from where editors author the surrounding content. |
| Orphan and usage auditing | GROQ finds assets with zero incoming references or missing alt text; a scheduled Function can clean up automatically. | Unused assets can be found via API scripting against references; automation is a job you build outside the platform. | Requires custom queries against the database or provider to find unreferenced media; no native orphan report. | Usage reports exist in the DAM, but mapping them back to CMS references is a manual reconciliation. |
| Governance on the same foundation | Roles & Permissions, Audit logs, and Content Releases govern asset and content changes together, shipping and rolling back as one. | Roles and scheduled publishing exist, with releases-style bundling available on higher tiers. | Role-based access and draft-and-publish are self-managed; audit depth depends on your hosting and add-ons. | Access controls apply to the DAM, but they are separate from your CMS editorial permissions and workflow. |