PRD Document · Savoy Signature Hotels — Multi-Site Headless Platform
Version: 1.0 · Date: 2026-03-04
Related docs: 01_General_Architecture.md, 02_Infrastructure_and_Environments.md, 08_API_Contracts.md
This document defines the caching strategy and performance optimization approach for the Savoy Signature platform. It covers the multi-layer cache architecture, Cloudflare configuration, the relationship between edge cache and Next.js use cache, purge mechanics, image optimization, and Core Web Vitals targets.
Layer 2 — Cloudflare Edge
Edge Cache — HTML + Static Assets
Layer What’s Cached TTL Invalidation Browser Static assets (JS, CSS, fonts, images) 1 year (immutable) Hash-based filenames (Turbopack) Cloudflare Edge SSR HTML pages Infinite (s-maxage=31536000) Programmatic purge via API Next.js use cache Expensive computations, shared data (nav structure) Short (5–15 min) Time-based expiry; not primary cache Umbraco API responses, DB queries Internal (managed by Umbraco) Automatic on content publish
[!IMPORTANT]
Cloudflare is the primary cache layer. Next.js use cache is NOT used for page content. All content fetches use cache: 'no-store'. Cloudflare is the single source of truth for cached pages.
Content Type Cache-Control Header Cloudflare Behavior SSR HTML public, max-age=0, s-maxage=31536000, stale-while-revalidate=60Edge cache 1 year (purged via API); browser never caches stale HTML; stale-while-revalidate serves stale during refresh Static assets (JS, CSS)public, max-age=31536000, immutableEdge + browser cache forever; hashed filenames Images (CDN)public, max-age=259200030-day cache at edge + browser API responses private, no-storeNever cached at edge Fonts public, max-age=31536000, immutableEdge + browser cache forever
const nextConfig : NextConfig = {
// SSR pages: cache at edge only (browser must revalidate)
value: ' public, max-age=0, s-maxage=31536000, stale-while-revalidate=60 ' ,
key: ' CDN-Cache-Control ' ,
value: ' max-age=31536000 ' , // Cloudflare-specific edge TTL
// API routes: never cache
value: ' private, no-store ' ,
Rule Match Setting HTML Cache *savoysignature.com/*Cache Level: Cache Everything API Bypass */api/*Cache Level: Bypass Umbraco Backoffice *umbraco*Cache Level: Bypass Storybook (internal) *.wycreative.com/*Cache Level: Standard
Scenario Trigger Purge Type Scope Content Publish Umbraco webhook Selective URL purge Affected pages + dependents Code Deploy CI/CD pipeline Selective by cache-tag or Purge All Pages using changed templates/modules Emergency Manual (Umbraco dashboard) Purge All Entire zone
[!NOTE]
Cloudflare Pro supports cache-tag purge — this feature is available on all plans (Free, Pro, Business, Enterprise) since 2024. Tags are set via the Cache-Tag response header from the origin.
The Next.js application adds Cache-Tag headers to every SSR response, identifying which modules/templates were used to render the page:
// Example: set cache tags on response
export function buildCacheTags ( siteKey : string , modules : string [] ) : string {
// Tags identify: site, page template, every module used
... modules . map ( m => ` module: ${ m } ` ) ,
// In next.config.ts headers or via response:
// Cache-Tag: site:savoy-palace,module:heroSlider,module:cardGrid,module:bookingBar
During a code deploy , the CI/CD pipeline determines which modules changed and calls the Cloudflare API to purge by tag:
# Example: purge all pages that use the heroSlider module
curl -X POST " https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache " \
-H " Authorization: Bearer {api_token} " \
-H " Content-Type: application/json " \
--data ' {"tags":["module:heroSlider"]} '
If the change affects a layout-level component (e.g., header, footer), the scope is too broad and the pipeline falls back to Purge All .
When a content item is published, the webhook must purge not just the item’s URL but also pages that reference it:
Content Published: Room 'Deluxe Ocean'
Purge: /pt/accommodations/rooms/deluxe-ocean
Purge: /en/accommodations/rooms/deluxe-ocean
Purge: /pt/accommodations (rooms list)
Purge: /en/accommodations
Purge: /pt/ (if featured on homepage)
Cloudflare Pro Plan Limit Purge by URL Up to 30 URLs per API call Purge Everything 1 call per zone Rate limit 1,000 purge API calls per 24 hours
For content publishes that affect more than 30 URLs, batch the purge calls or fall back to “Purge Everything.”
After purging, the webhook handler sends warmup requests to re-populate the edge cache:
async function warmup ( urls : string [] ) {
// Limit concurrency to avoid overwhelming origin
const batches = chunk (urls , CONCURRENCY );
for ( const batch of batches) {
await Promise . allSettled (
headers: { ' X-Warmup ' : ' true ' } ,
// Small delay between batches
✅ Use use cache For ❌ Do NOT Cache Navigation tree computation (expensive to build from flat API) Page content (Cloudflare handles this) Site configuration lookup User-specific data (no user auth, but future-proof) Shared labels / dictionary items API responses from Umbraco Third-party API responses (if applicable) Any content that changes via CMS
import { cacheLife } from ' next/cache ' ;
export async function getNavigationTree ( siteKey : string , locale : string ) {
cacheLife ( ' minutes ' ); // Cache for ~5 minutes
const client = new UmbracoClient ();
const nav = await client . getNavigation (siteKey , locale);
return buildNavigationTree (nav ?. items || []);
Concern Approach Format Serve WebP/AVIF via next/image automatic optimization Responsive Desktop + Mobile images (mandatory per responsiveImageComposition) served via <picture> Sizing next/image with sizes prop for responsive srcsetLazy loading Default: lazy. First visible image (LCP candidate): priority={true} CDN Images served via Cloudflare CDN (Blob Storage → Cloudflare) Max file size Desktop: ≤500KB, Mobile: ≤200KB (compressed)
Context Desktop Size Mobile Size Format Hero / Full-width 1920 × 1080 750 × 1000 WebP Card thumbnail 600 × 400 375 × 250 WebP Gallery 1440 × 960 750 × 500 WebP Logo SVG SVG SVG Icons SVG SVG SVG
Feature Setting Polish Lossy (aggressive compression) WebP Enabled (auto-serve WebP to supported browsers) Mirage Enabled (lazy-load images, responsive image placeholders)
Metric Target What It Measures LCP (Largest Contentful Paint)< 2.5s Time until largest visible element renders FID (First Input Delay)< 100ms Time from user interaction to browser response CLS (Cumulative Layout Shift)< 0.1 Visual stability (no layout jumps) INP (Interaction to Next Paint)< 200ms Responsiveness of interactions TTFB (Time to First Byte)< 800ms Server response time FCP (First Contentful Paint)< 1.8s Time until first content painted
Optimization Implementation Impact Edge cache (Cloudflare) TTFB < 50ms for cached pages (global PoPs) TTFB, LCP Server Components Zero client-side JS for static content modules FID, INP, bundle Turbopack 2–5× faster builds, 10× faster HMR DX (developer experience) next/fontZero FOUT, preloaded, subset per theme CLS, LCP next/imageWebP/AVIF, srcset, lazy-load, priority for LCP LCP, CLS Desktop + Mobile images Correct image size per viewport LCP, bandwidth BEM + SASS No runtime CSS-in-JS, structured styles FID, bundle Dynamic imports Lazy-load heavy modules (gallery lightbox, map) Bundle, FID Prefetch <Link prefetch> for likely navigation targetsLCP (subsequent) No layout shifts Explicit width/height on images, reserved space for ads CLS GTM async next/script with afterInteractive strategyFID, LCP
Tool Purpose Cadence Google PageSpeed Insights Lab + field data per URL Weekly + per-deploy Chrome UX Report (CrUX) Real-user field data Monthly review Lighthouse CI Automated lab testing in CI pipeline Every PR openClaw AI QA Performance audit as part of AI QA gate Every PR Cloudflare Analytics Edge cache hit ratio, latency Continuous (dashboard) Azure Application Insights Server-side performance, error rates Continuous (alerts)
Resource Budget Total page weight (first load, gzipped)< 500KB JavaScript bundle (per page, gzipped)< 200KB CSS (all themes, gzipped)< 50KB LCP image < 500KB (desktop), < 200KB (mobile) Third-party scripts (GTM, Navarino)< 100KB Fonts (per site/theme)< 100KB
Next document: 10_MultiLanguage_and_i18n.md