How to Integrate Bunny.net with Your Headless CMS
Connect Bunny.net to your headless CMS so published media, metadata, and cache updates move from structured content to Bunny’s edge automatically.
What is Bunny.net?
Bunny.net is a global edge platform for CDN delivery, edge storage, image resizing, video streaming, DNS, and security. Teams use it to serve images, downloads, and video close to users through Bunny Storage Zones, Pull Zones, Stream, and its API. It competes in the CDN and media delivery market with self-serve setup, usage-based pricing, and a developer-facing API.
Why integrate Bunny.net with a headless CMS?
Media gets messy when editorial content and delivery infrastructure live in different places. An editor publishes a product page with a new hero image, but the image still needs to be uploaded to Bunny Storage, attached to the right Pull Zone URL, purged from cache, and referenced correctly on the frontend. If that work is manual, you get stale images, broken URLs, and last-minute Slack messages asking which file is actually live.
Connecting Bunny.net to a headless CMS category tool solves that by treating media delivery as part of the publishing flow. With Sanity, content is structured as typed JSON in the Content Lake, so a webhook or Function can read exactly which image, video, slug, locale, or campaign changed. GROQ selects the fields Bunny.net needs, and the sync logic can upload files to Bunny Storage, purge a CDN URL, or update derived media fields without polling every few minutes.
The trade-off is that you need to define the rules up front. For example, should every image publish to Bunny Storage, or only images tagged for a marketing site? Should deletes remove files from Bunny, or keep them for rollback? Those decisions take a little planning, but the payoff is clear: editors keep working in Sanity Studio, developers keep media delivery on Bunny.net, and the frontend receives fast, predictable URLs.
Architecture overview
A typical Sanity and Bunny.net integration starts when an editor publishes or updates a document in Sanity Studio. The content lands in the Content Lake as structured JSON, with references to Sanity assets, captions, alt text, slugs, locales, and any delivery settings you model in your schema. A Sanity webhook fires on the publish event. You can filter the webhook with GROQ so it only triggers for documents that include media fields or a specific flag like syncToBunny == true. The webhook sends the document ID to a server endpoint, or you can run the same logic in Functions so the sync happens server-side without standing up separate infrastructure. The sync handler uses @sanity/client and GROQ to fetch the current document shape, including joined asset fields such as image.asset->url, file.asset->mimeType, slug.current, and any Bunny path you’ve modeled. It downloads the source asset, calls Bunny.net’s Storage API with a PUT request to https://storage.bunnycdn.com/{storageZoneName}/{path}, and authenticates with the Storage Zone access key in the AccessKey header. If the file is already cached through a Pull Zone, the handler can call Bunny.net’s purge API at https://api.bunny.net/purge?url={cdnUrl} using the account API key. The end user never touches Sanity directly. Your website, app, or agent queries Sanity for the content structure and uses the Bunny CDN URL for the media asset. Bunny.net handles edge delivery, while Sanity remains the source for titles, alt text, captions, routing, localization, references, and publish state.
Common use cases
Publish-ready image delivery
Sync hero images, campaign graphics, and product photos from Sanity publish events into Bunny Storage, then serve them through a Pull Zone URL.
Video landing pages
Model video pages in Sanity, store titles, transcripts, CTAs, and publish state there, and deliver the media through Bunny Stream or Bunny CDN.
Localized media paths
Use Sanity fields like locale, market, and slug to create Bunny paths such as /en-us/summer-sale/hero.jpg and /fr-ca/summer-sale/hero.jpg.
Cache purge on publish
Trigger Bunny.net cache purges when an editor replaces a file, updates a campaign image, or republishes a media-heavy page.
Step-by-step integration
- 1
Set up Bunny.net delivery
Create a Bunny.net account, then create a Storage Zone for uploaded media. Connect it to a Pull Zone, note the Storage Zone name, storage hostname, CDN hostname, Storage Zone access key, and account API key. If you’re serving large videos, set up Bunny Stream separately and decide whether Sanity will hold Stream video IDs, embed URLs, or both.
- 2
Install the Sanity client
In your webhook handler, Function, or middleware project, install @sanity/client. Bunny.net’s Storage API works over HTTP, so a standard fetch call is enough for basic uploads, deletes, and cache purges.
- 3
Model Bunny delivery fields in Sanity Studio
Add fields such as heroImage, altText, syncToBunny, bunnyPath, bunnyCdnUrl, locale, and slug. Keep editor-facing fields clear. For example, editors should choose whether an asset syncs to Bunny, but your code should generate the final path to avoid typos.
- 4
Create the sync trigger
Create a Sanity webhook that fires on publish for the document types that need Bunny delivery. Use a GROQ filter such as _type == 'article' && syncToBunny == true. Point it at your webhook listener, or use Functions if you want the server-side logic to run from Sanity events without a separate service.
- 5
Upload to Bunny.net and purge cache
Fetch the latest document from the Content Lake with GROQ, download the Sanity asset URL, and PUT the bytes to Bunny Storage at /{storageZoneName}/{path}. After upload, call Bunny.net’s purge API for the CDN URL so visitors don’t receive the previous file.
- 6
Test the frontend path
Publish a draft, confirm the file exists in Bunny Storage, confirm the Pull Zone URL returns a 200, and render the Bunny CDN URL in your frontend. Also test a replacement image, an unpublished document, and a failed upload so your team knows what happens when the sync doesn’t finish.
Code example
import {createClient} from '@sanity/client';
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-01-01',
token: process.env.SANITY_READ_TOKEN,
useCdn: false,
});
export async function POST(req: Request) {
const {id} = await req.json();
const doc = await sanity.fetch(
`*[_id == $id][0]{
title,
"slug": slug.current,
"imageUrl": heroImage.asset->url
}`,
{id}
);
if (!doc?.imageUrl || !doc?.slug) {
return Response.json({skipped: true});
}
const image = await fetch(doc.imageUrl);
const bytes = await image.arrayBuffer();
const path = `articles/${doc.slug}/hero.jpg`;
const storageHost = process.env.BUNNY_STORAGE_HOST || 'storage.bunnycdn.com';
const zone = process.env.BUNNY_STORAGE_ZONE!;
const upload = await fetch(`https://${storageHost}/${zone}/${path}`, {
method: 'PUT',
headers: {
AccessKey: process.env.BUNNY_STORAGE_KEY!,
'Content-Type': 'application/octet-stream',
},
body: bytes,
});
if (!upload.ok) {
throw new Error(`Bunny upload failed: ${upload.status}`);
}
const cdnUrl = `https://${process.env.BUNNY_CDN_HOST}/${path}`;
await fetch(`https://api.bunny.net/purge?url=${encodeURIComponent(cdnUrl)}`, {
method: 'POST',
headers: {AccessKey: process.env.BUNNY_API_KEY!},
});
return Response.json({synced: true, cdnUrl});
}How Sanity + Bunny.net works
Build your Bunny.net integration on Sanity
Sanity gives you the structured content foundation, real-time event system, and flexible APIs to connect Bunny.net to your publishing workflow.
Start building free →CMS approaches to Bunny.net
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Media-to-content mapping | Media is often attached inside page fields or plugins, which can make automated Bunny paths hard to generate consistently. | Schemas define the exact fields Bunny needs, including asset references, slugs, locales, alt text, and sync flags. |
| Publish-time Bunny sync | A plugin or manual upload often handles the transfer, and failures may not match the editorial publish flow. | Webhooks or Functions can run sync logic on publish, including upload to Bunny Storage and cache purge calls. |
| Field-level query control | Integrations may read rendered HTML, full pages, or plugin-specific payloads. | GROQ fetches the exact fields and references needed for Bunny.net in one query. |
| Cache invalidation | Cache purge logic is often tied to page publish events, not specific media replacements. | Schema fields and GROQ projections can generate the exact Bunny CDN URL to purge when an asset changes. |
| Multi-channel media delivery | Web pages are usually the primary target, and reuse across apps or agents can take extra work. | The same structured content can feed web, mobile, Bunny.net, and production AI agents through Agent Context. |
Keep building
Explore related integrations to complete your content stack.
Sanity + Cloudinary
Connect structured content in Sanity with Cloudinary transformations, delivery URLs, and asset workflows.
Sanity + Mux
Model video pages, metadata, transcripts, and publish state in Sanity while Mux handles encoding and playback.
Sanity + Bynder
Use approved brand assets from Bynder inside structured Sanity content for campaigns, product pages, and regional sites.