Automate Ghost CDN Cache Purging with BunnyCDN Middleware Scripts
This guide shows how to use BunnyCDN Middleware Scripts to instantly refresh your Ghost CDN cache when you make changes to the website—no extra servers required. A simple solution that automatically detects content changes and keeps your site up-to-date.

Ever struggled with waiting for your Ghost blog changes to appear after you hit publish? If you're using BunnyCDN with your Ghost site, I've found a clever way to make your content updates show up instantly—without the headache of extra servers or complex setups.
The Problem with Waiting
When you update a post or tweak your theme on Ghost, those changes often get stuck in your CDN's cache. This means visitors might still see the old version of your site, even though you've made updates. Ghost offers webhooks to help with this, but as I discovered(as shown below from magicpages), they can be tricky to manage and sometimes miss certain types of changes.

A Better Solution Hidden in Ghost's Code
While digging through Ghost's internals (you can see the code here), I found something interesting: the X-Cache-Invalidate
header. This isn't some obscure feature—it's what Ghost itself uses to signal exactly which content needs refreshing.
Until now, tapping into this header required setting up a separate proxy service (like the ghost-cache-invalidation-proxy). But if you're using BunnyCDN, there's a much simpler approach using their Edge Scripts feature—no extra servers needed.
When Does Ghost Actually Send This Magic Header?
I've confirmed through code analysis that Ghost triggers the X-Cache-Invalidate
header during several key operations:
- For full site refreshes (
/*
), Ghost sends the header when you:- Publish a new post or page
- Change a post's status (like draft to published)
- Update your site settings
- Switch or upload themes
- Modify tags or navigation labels
- Update user profiles
- Perform bulk actions on posts
- For targeted updates, Ghost is smarter—it only refreshes specific paths:
- When comments are added or changed, only the associated post updates
This comprehensive approach catches everything—far better than simple webhooks.
Enter BunnyCDN Middleware Scripts
Here's where BunnyCDN's Edge Scripts feature really shines. Instead of running a separate server, you can write a small piece of JavaScript that runs directly on their edge nodes—like having a tiny helper that lives right where your content is cached.

The key insight? Using the onOriginResponse
trigger, which lets us examine responses from your Ghost server before they get cached or sent to visitors. It's the perfect spot to catch that X-Cache-Invalidate
header and act on it immediately.
The Solution: A Simple Edge Script
Instead of a complex proxy setup, we can handle everything with a concise script. Here's how it works:
- It watches for responses coming from your Ghost server
- If it spots the
x-cache-invalidate
header, it springs into action - It figures out which URLs need refreshing based on the header value
- It calls BunnyCDN's Purge API directly to clear just those specific paths
The whole thing requires just two configuration values:
- Your BunnyCDN API key
- Optionally, your blog's public URL
import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.12.0";
import process from "node:process";
/**
* Parses the invalidation pattern from Ghost's x-cache-invalidate header.
* Mimics the logic needed to correctly interpret paths for purging.
*
* @param {string} pattern - The pattern string (e.g., "/$/", "/post-slug", "/page/*").
* @param {string | undefined} ghostPublicUrl - Optional base public URL from env.
* @returns {{ urls: string[], purgeAll: boolean, pattern: string, timestamp: string }} - Parsed data.
*/
function parseInvalidationPattern(pattern, ghostPublicUrl) {
const purgeAll = pattern === "/$/" || pattern === "/*";
const baseUrls = purgeAll
? [`${ghostPublicUrl || ""}/*`] // Use /* directly if purging all
: pattern.split(",").map((url) => url.trim());
const urls = baseUrls.map((url) => {
if (url === "/*") return ghostPublicUrl ? `${ghostPublicUrl}/*` : "/*";
if (url.startsWith("http")) return url;
return ghostPublicUrl ? `${ghostPublicUrl}${url}` : url; // Return relative if no public URL
});
return {
urls,
purgeAll,
pattern,
timestamp: new Date().toISOString(),
};
}
async function onOriginResponse(context) {
const invalidationHeader =
context.response.headers.get("x-cache-invalidate");
if (invalidationHeader) {
// Log detection only if header is found
console.log(
`EdgeScript: Detected x-cache-invalidate header: ${invalidationHeader}`
);
// --- Configuration from Environment Variables ---
const ghostPublicUrl = process.env.GHOST_PUBLIC_URL;
const apiKey = process.env.BUNNY_API_KEY;
// --- End Configuration ---
if (!apiKey) {
// Log essential error
console.error(
"EdgeScript: BUNNY_API_KEY environment variable is not set. Cannot trigger purge."
);
return context.response; // Return original response
}
try {
const invalidationData = parseInvalidationPattern(
invalidationHeader,
ghostPublicUrl
);
const basePurgeUrl = "https://api.bunny.net/purge";
// Trigger purge for each URL individually
for (const urlToPurge of invalidationData.urls) {
// Use async=false to wait for purge confirmation, change to true if preferred
const apiUrl = `${basePurgeUrl}?url=${encodeURIComponent(
urlToPurge
)}&async=false`;
const options = {
method: "POST",
headers: {
AccessKey: apiKey,
},
};
try {
const purgeResponse = await fetch(apiUrl, options);
if (!purgeResponse.ok) {
const errorText = await purgeResponse.text();
// Log essential failure details
console.error(
`EdgeScript: Purge API request FAILED for ${urlToPurge}: ${purgeResponse.status} ${purgeResponse.statusText} - ${errorText}`
);
} else {
// Log essential success (simplified)
console.log(
`EdgeScript: Purge API request SUCCESSFUL for ${urlToPurge}: ${purgeResponse.status}`
);
}
} catch (fetchError) {
// Log essential network error
console.error(
`EdgeScript: Network error during purge request for ${urlToPurge}: ${fetchError.message}`
);
}
}
} catch (error) {
// Log essential processing error
console.error(
`EdgeScript: Error processing invalidation or calling purge API: ${error.message}`,
error.stack
);
}
}
return context.response;
}
BunnySDK.net.http.servePullZone().onOriginResponse(onOriginResponse);
Why This Approach Beats Running a Separate Proxy
The original ghost-cache-invalidation-proxy
is solid code, but if you're already using BunnyCDN, this Edge Script approach offers several advantages:
- Zero infrastructure overhead: No separate servers to maintain, no Docker containers to update, no SSL certificates to manage.
- Simplified setup: Just copy-paste the script into BunnyCDN's Edge Rules UI, add two environment variables, and you're done.
- Direct integration: The script talks directly to the CDN's API—no middleware, no extra network hops.
- Global distribution: The logic runs on BunnyCDN's global edge network, exactly where your content is being cached.
While the proxy approach does offer more flexibility to trigger different services or use various CDNs, for the common case of "I just want my BunnyCDN cache to stay fresh with minimal hassle," this Edge Script approach is hard to beat.
Setting It Up (It's Easier Than You Think)
Here's how to implement this in your BunnyCDN account:
- Log into BunnyCDN: Go to your BunnyCDN dashboard.
- Navigate to Edge Scripting: Go to "Edge Platform" → "Scripting" in the main navigation.
- Create a new script: Click the "Create Script" button.
- Configure the script:
- Select "Middleware Script" as the type
- Give your script a descriptive name (like "Ghost Cache Refresher")
- Paste the JavaScript code provided above into the editor
- Add environment secrets:
- In the "Environment Variables" section, add:
BUNNY_API_KEY
with your actual BunnyCDN API keyGHOST_PUBLIC_URL
(optional) with your blog's full URL (e.g.,https://yourblog.com
)
- In the "Environment Variables" section, add:
- Link to your Pull Zone: Connect this script to the Pull Zone where your Ghost site is hosted.
- Save and publish: Finalize your changes and make the script active.
That's it! Now whenever Ghost updates content and sends that special header, your Edge Script will automatically detect it and clear exactly the right paths from your cache.
The Bottom Line
I love elegant solutions that reduce complexity, and this is definitely one of them. By leveraging Ghost's built-in cache invalidation signals and BunnyCDN's Edge Scripts, we've eliminated an entire layer of infrastructure while actually improving reliability.
Hat tip to the folks at Magic Pages for their original research and proxy implementation—they identified the importance of this header and paved the way. This approach simply takes their insight and implements it in a way that's tailor-made for BunnyCDN users.
Have you tried this approach? Let me know how it works for you in the comments!
Discussion