How to Integrate Cloudinary with Your Headless CMS
Connect Cloudinary to structured content so product pages, campaigns, and apps can publish resized, tagged, CDN-delivered media without copy-pasting URLs.
What is Cloudinary?
Cloudinary is a cloud-based media platform for uploading, transforming, organizing, and delivering images and videos through a CDN. Teams use it as a DAM, media API, and delivery layer for ecommerce, publishing, SaaS, and marketing sites. Its core strength is programmatic media handling, including transformations, responsive delivery, metadata, folders, tags, and signed uploads.
Why integrate Cloudinary with a headless CMS?
Media gets messy fast when your content and assets live in separate systems. A merchandiser updates a product title in one place, a designer uploads a new hero image in another, and the frontend still points at last month’s Cloudinary URL. At 50 assets, that’s annoying. At 50,000 assets across product pages, landing pages, emails, and apps, it turns into broken alt text, stale campaign images, and duplicate uploads.
Connecting Cloudinary to a headless CMS lets content changes trigger media work automatically. When a product, article, or campaign is published, the integration can send the right image URL, alt text, caption, tags, folder path, and product category to Cloudinary. Cloudinary can then create the derived assets your frontend needs, such as WebP, AVIF, cropped thumbnails, social cards, and responsive breakpoints.
The difference is structure. With Sanity’s AI Content Operating System, content in the Content Lake is typed JSON, not page HTML or opaque blobs. GROQ can select exactly the fields Cloudinary needs, webhooks can fire on publish events, and Functions can run the server-side sync code without a separate queue worker. The trade-off is that you’ll need to design the schema and sync rules up front. That’s extra setup, but it prevents the long-term pain of manual uploads, copied URLs, and metadata that drifts out of date.
Architecture overview
A typical Sanity and Cloudinary integration starts with structured content in the Content Lake. For example, a product document might include a title, slug, category reference, Sanity image asset, alt text, locale, and a Cloudinary field for the returned public ID and secure URL. On publish, a Sanity webhook can use a GROQ filter such as `_type == "product" && defined(mainImage.asset)` so only relevant mutations trigger the integration. The webhook sends a small payload, often just `_id`, to a Sanity Function or a server endpoint. That server-side code uses `@sanity/client` to fetch the full document with GROQ, including joins like `category->title` and `mainImage.asset->url`. The function then calls Cloudinary’s Node SDK, usually `cloudinary.uploader.upload()` for a Sanity-hosted image URL or `cloudinary.uploader.explicit()` if the asset already exists in Cloudinary and only needs updated metadata. The request can set `folder`, `public_id`, `tags`, `context`, `overwrite`, and transformation settings. Cloudinary returns fields such as `public_id`, `secure_url`, `version`, `width`, `height`, and `format`. You can write those values back to Sanity, render them directly on the frontend, or store only the Cloudinary public ID and build delivery URLs at request time. From there, the end user gets media from Cloudinary’s CDN, while the page, app, or AI agent gets structured content from Sanity. When an editor changes the product title, alt text, campaign tag, or source image, the webhook runs again and Cloudinary receives the update without polling.
Common use cases
Product media sync
Send product images, alt text, SKU tags, and category metadata from Sanity to Cloudinary when a product is published.
Campaign image variants
Create Cloudinary transformations for hero banners, email headers, social cards, and mobile crops from one approved campaign asset.
Localized media delivery
Use Sanity locale fields to tag Cloudinary assets by market, language, region, or legal usage rights.
Video metadata publishing
Keep Cloudinary video posters, captions, titles, and taxonomy aligned with the structured content your editors publish.
Step-by-step integration
- 1
Set up Cloudinary
Create a Cloudinary account, copy your cloud name, API key, and API secret from the dashboard, then install the Node SDK with `npm install cloudinary`.
- 2
Model media fields in Sanity Studio
Add fields for source image, alt text, caption, rights, locale, tags, and a `cloudinary` object that can store `publicId`, `secureUrl`, `version`, `width`, and `height`.
- 3
Create a publish trigger
Add a Sanity webhook with a GROQ filter like `_type == "product" && defined(mainImage.asset)` or run the same logic in a Sanity Function triggered by content mutations.
- 4
Fetch only the fields Cloudinary needs
Use GROQ to fetch the document ID, slug, title, category, image URL, alt text, and any tags or locale fields that should become Cloudinary metadata.
- 5
Call Cloudinary’s Upload API
Use `cloudinary.uploader.upload(imageUrl, { folder, public_id, tags, context, overwrite: true })` to register or replace the media asset and attach structured metadata.
- 6
Test delivery in your frontend
Render Cloudinary `secure_url` values or build transformation URLs with Cloudinary’s SDK, then test publish, update, delete, and rollback cases before turning on the webhook for all content.
Code example
import { createClient } from '@sanity/client'
import { v2 as cloudinary } from 'cloudinary'
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-01-01',
token: process.env.SANITY_WRITE_TOKEN!,
useCdn: false
})
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
api_key: process.env.CLOUDINARY_API_KEY!,
api_secret: process.env.CLOUDINARY_API_SECRET!
})
export async function POST(req: Request) {
const { _id } = await req.json()
const product = await sanity.fetch(`*[_id == $_id][0]{
_id,
title,
"slug": slug.current,
"category": category->title,
"imageUrl": mainImage.asset->url,
"alt": mainImage.alt
}`, { _id })
if (!product?.imageUrl) {
return Response.json({ skipped: true })
}
const uploaded = await cloudinary.uploader.upload(product.imageUrl, {
folder: 'products',
public_id: product.slug,
overwrite: true,
tags: ['sanity', product.category].filter(Boolean),
context: {
alt: product.alt || '',
title: product.title,
sanity_id: product._id
}
})
await sanity.patch(product._id).set({
cloudinary: {
publicId: uploaded.public_id,
secureUrl: uploaded.secure_url,
version: uploaded.version,
width: uploaded.width,
height: uploaded.height
}
}).commit()
return Response.json({ publicId: uploaded.public_id })
}How Sanity + Cloudinary works
Build your Cloudinary integration on Sanity
Sanity gives you the structured content foundation, real-time event system, and flexible APIs to connect Cloudinary with your media workflow.
Start building free →CMS approaches to Cloudinary
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Media metadata shape | Media fields are often tied to page templates, so syncing alt text, rights, and taxonomy to Cloudinary usually requires custom parsing. | Typed JSON in the Content Lake keeps media, alt text, references, locale, and Cloudinary IDs in one queryable structure. |
| Publish-triggered sync | Plugins or cron jobs often push assets after a save, which can miss edge cases like scheduled publishing or rollbacks. | GROQ-filtered webhooks and Functions can run Cloudinary sync logic on specific content mutations without polling. |
| Field-level query control | Integrations may receive full page payloads, then discard fields Cloudinary doesn’t need. | GROQ can project only the Cloudinary payload, including joined references, computed fields, and fallback values. |
| Editorial workflow for assets | Editors often paste Cloudinary URLs into rich text or page fields, which makes validation difficult. | Sanity Studio can add validation for alt text, rights, required crops, Cloudinary public IDs, and market-specific metadata. |
| Multi-channel delivery | The website is usually the primary target, so apps, feeds, and agents may need separate export logic. | One structured source can feed websites, apps, Cloudinary, marketing automation, and Agent Context for AI agents. |
Keep building
Explore related integrations to complete your content stack.
Sanity + Imgix
Serve responsive image transformations from structured Sanity image fields with Imgix delivery URLs.
Sanity + Mux
Connect structured video metadata in Sanity with Mux playback, thumbnails, captions, and publishing workflows.
Sanity + Bynder
Pair Sanity content schemas with Bynder assets, approvals, brand metadata, and reuse across channels.