04 — Webhooks and Cache Purge
Dev Guide — Savoy Signature Hotels
PRD refs:08_API_Contracts.md,09_Cache_and_Performance.md,01_General_Architecture.md
1. Overview
Section titled “1. Overview”The caching strategy uses Cloudflare Edge as the primary cache layer with infinite TTL. Content is invalidated by programmatic purge triggered by Umbraco publish webhooks. This guide covers the webhook contract, purge flow, dependency graph, and cache warmup.
2. Cache Architecture (4 Layers)
Section titled “2. Cache Architecture (4 Layers)”Key principle: Cloudflare is the single source of truth for cached HTML. Next.js does NOT cache page content. All page requests use cache: 'no-store' at the fetch level.
3. Cache-Control Headers
Section titled “3. Cache-Control Headers”| Content Type | Header Value |
|---|---|
| SSR HTML pages | public, max-age=0, s-maxage=31536000 + CDN-Cache-Control: max-age=31536000 |
| Static assets (JS/CSS) | public, max-age=31536000, immutable |
| Images (CDN) | public, max-age=2592000 (30 days) |
| API responses | private, no-store |
| Fonts | public, max-age=31536000, immutable |
4. Webhook Contract
Section titled “4. Webhook Contract”4.1 Endpoint
Section titled “4.1 Endpoint”POST /api/webhooks/umbraco4.2 Security
Section titled “4.2 Security”| Mechanism | Implementation |
|---|---|
| HMAC Signature | X-Webhook-Signature header with SHA-256 of payload using REVALIDATE_SECRET |
| IP Allowlist | Restrict to Umbraco Azure App Service IP |
4.3 Payload
Section titled “4.3 Payload”{ "event": "ContentPublished", "timestamp": "2026-03-05T14:30:00Z", "content": { "id": "guid-here", "contentType": "roomDetailPage", "cultures": ["pt", "en"], "urls": [ "/pt/alojamento/quarto-deluxe", "/en/accommodation/deluxe-room" ] }, "dependencies": [ "/pt/alojamento", "/en/accommodation", "/pt/", "/en/" ]}Supported events:
| Event | Trigger |
|---|---|
ContentPublished | Content node published |
ContentUnpublished | Content node unpublished |
MediaSaved | Media item saved/replaced |
4.4 Handler Implementation
Section titled “4.4 Handler Implementation”export async function POST(request: Request) { // 1. Validate webhook signature const signature = request.headers.get('X-Webhook-Signature'); const body = await request.text(); const expectedSignature = createHmac('sha256', process.env.REVALIDATE_SECRET!) .update(body) .digest('hex');
if (signature !== expectedSignature) { return Response.json({ error: 'Invalid signature' }, { status: 401 }); }
// 2. Parse payload const payload = JSON.parse(body);
// 3. Collect all URLs to purge (content + dependencies) const urlsToPurge = [ ...payload.content.urls, ...(payload.dependencies || []), ].map(path => `https://${getDomainForSite(payload)}${path}`);
// 4. Purge Cloudflare cache await purgeCloudflareCache(urlsToPurge);
// 5. Warm up purged URLs await warmupUrls(urlsToPurge);
return Response.json({ success: true, purged: urlsToPurge.length });}5. Cloudflare Purge API
Section titled “5. Cloudflare Purge API”5.1 Selective URL Purge
Section titled “5.1 Selective URL Purge”POST https://api.cloudflare.com/client/v4/zones/{CLOUDFLARE_ZONE_ID}/purge_cacheAuthorization: Bearer {CLOUDFLARE_API_TOKEN}Content-Type: application/json
{ "files": [ "https://www.savoysignature.com/pt/alojamento/quarto-deluxe", "https://www.savoysignature.com/en/accommodation/deluxe-room" ]}5.2 Cache-Tag Purge (Cloudflare Pro)
Section titled “5.2 Cache-Tag Purge (Cloudflare Pro)”Next.js adds Cache-Tag header to SSR responses:
Cache-Tag: site:savoy-palace,module:heroSlider,page:room-detailPurge by tag (useful for code deploys):
POST /zones/{zone}/purge_cache{ "tags": ["module:heroSlider"] }5.3 Purge Everything (Emergency)
Section titled “5.3 Purge Everything (Emergency)”Available via custom Umbraco dashboard button with confirmation dialog.
POST /zones/{zone}/purge_cache{ "purge_everything": true }5.4 API Limits (Cloudflare Pro)
Section titled “5.4 API Limits (Cloudflare Pro)”| Limit | Value |
|---|---|
| URLs per purge call | 30 |
| Purge API calls per 24h | 1,000 |
Batch strategy: If more than 30 URLs, split into multiple calls. If exceeding daily limit, fall back to “Purge Everything”.
6. Dependency Graph
Section titled “6. Dependency Graph”When a content node is published, the webhook must include all pages that reference or display that content.
Common dependencies:
| Published Content | Affected Pages |
|---|---|
| Room Detail page | Room Detail + Rooms List + Homepage (if featured) |
| Dining Detail page | Dining Detail + Dining List |
| Shared Content (footer) | ALL pages across ALL 8 sites |
| siteRoot (header/nav) | ALL pages for that site |
| News article | News Detail + News List |
| Special Offer | Offer Detail + Offers List + Homepage (if featured) |
Implementation: Build the dependency resolver in Umbraco as a custom webhook handler that:
- Identifies the published node
- Finds all Block List references to that node
- Finds parent list pages
- Checks if the node appears on homepage featured sections
- Adds all affected URLs to the webhook payload
7. Cache Warmup
Section titled “7. Cache Warmup”After purging, warm up critical pages to ensure visitors hit a cached response.
async function warmupUrls(urls: string[]) { const CONCURRENCY = 5; const DELAY_MS = 200;
for (let i = 0; i < urls.length; i += CONCURRENCY) { const batch = urls.slice(i, i + CONCURRENCY); await Promise.all( batch.map(url => fetch(url, { headers: { 'X-Warmup': 'true' } }) ) ); await new Promise(resolve => setTimeout(resolve, DELAY_MS)); }}Rules:
- Concurrency limit: 5 parallel requests
- 200ms delay between batches
- Send
X-Warmup: trueheader (for logging/analytics exclusion)
8. Three Purge Scenarios
Section titled “8. Three Purge Scenarios”8.1 Content Publish (Automated)
Section titled “8.1 Content Publish (Automated)”8.2 Code Deploy (CI/CD)
Section titled “8.2 Code Deploy (CI/CD)”# CI/CD purge by tagcurl -X POST \ "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache" \ -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ -H "Content-Type: application/json" \ --data '{"tags":["module:heroSlider"]}'8.3 Emergency (Manual)
Section titled “8.3 Emergency (Manual)”Via Umbraco custom dashboard:
- Admin clicks “Purge All Cache” button
- Confirmation dialog: “This will purge ALL cached pages across ALL 8 sites. Continue?”
- Calls Cloudflare
purge_everything: true - Warmup runs for all 8 site homepages
9. use cache Directive (Next.js)
Section titled “9. use cache Directive (Next.js)”Only for expensive computations that don’t change on every publish:
'use cache';import { cacheLife } from 'next/cache';
export async function getNavigationTree(siteKey: string, locale: string) { cacheLife('minutes'); // ~5 minutes const client = new UmbracoClient(); return client.getNavigation(siteKey, locale);}When to use use cache:
- Navigation tree (~5 min TTL)
- Dictionary/labels (~15 min TTL)
- Shared content lookups
When NOT to use use cache:
- Page content (Cloudflare caches the full HTML)
- API responses (use
cache: 'no-store')
10. Cloudflare Page Rules
Section titled “10. Cloudflare Page Rules”| Pattern | Rule |
|---|---|
*savoysignature.com/* | Cache Everything |
*/api/* | Bypass Cache |
*umbraco* | Bypass Cache |
*.wycreative.com/* | Standard (dev/stage) |
11. Monitoring & Alerts
Section titled “11. Monitoring & Alerts”| Metric | Threshold | Alert Level |
|---|---|---|
| Cache Hit Rate | < 80% | Warning |
| Purge API calls/day | > 800 | Warning |
| Warmup failures | > 5% | Warning |
| Webhook processing time | > 10s | Warning |
12. Common Pitfalls
Section titled “12. Common Pitfalls”| Pitfall | Solution |
|---|---|
| Forgot dependency graph | Always include parent list pages and homepage when purging detail pages |
| Shared Content purge misses sites | Shared Content changes MUST trigger purge across all 8 sites |
| Exceeded Cloudflare purge limits | Batch URLs (max 30/call) or use “Purge Everything” for large publishes |
| Warmup not running | Warmup is critical — without it, first visitor gets a cache miss (slow) |
| Caching API responses in Next.js | Use cache: 'no-store' for all Umbraco API fetches |
| Missing webhook signature validation | Always validate HMAC signature before processing |