Cloudflare Performance Optimization
Cache headers, Core Web Vitals fixes, and CDN configuration for Arcturus-Prime.com on Cloudflare Pages
Cloudflare Performance Optimization
Arcturus-Prime runs on Cloudflare Pages with a mix of static pre-rendered pages and SSR worker routes. This doc covers the caching strategy, CWV optimizations, and known Cloudflare Pages behaviors.
Cache Strategy
public/_headers File
Cloudflare Pages reads public/_headers to set response headers on static assets. The file uses path patterns with most-specific-match-wins ordering.
Key rules:
| Path Pattern | Browser Cache | Edge Cache | Purpose |
|---|---|---|---|
/_astro/* | immutable, 1 year | — | Fingerprinted JS/CSS/images |
/images/*, /fonts/*, /assets/* | immutable, 1 year | — | Static assets |
/blog/*, /journal/*, /posts/* | 5 min | 1 hour | Content pages |
/ | 1 min | 5 min | Homepage |
/status, /command/*, /telemetry | 1 min | 1 min | Dynamic-ish pages |
/api/*, /auth/* | no-store | — | Never cache |
CDN-Cache-Control
CDN-Cache-Control is a Cloudflare-specific header that sets the edge cache TTL independently from the browser Cache-Control. This lets us have short browser TTLs (users see fresh content) with longer edge TTLs (reduce origin requests).
Cache-Control: public, max-age=300, must-revalidate
CDN-Cache-Control: max-age=3600
Browser caches for 5 minutes, edge caches for 1 hour.
cf-cache-status: DYNAMIC
On Cloudflare Pages, HTML responses always show cf-cache-status: DYNAMIC. This is normal — Pages has its own edge distribution separate from the traditional CDN cache layer. The pages are still served fast from the edge.
What NOT to do
/*.htmlpatterns don’t match Cloudflare Pages clean URLs (/blog/slug/)- You can’t override
Strict-Transport-Securityvia_headers— it’s set in the Cloudflare dashboard - The
/*catch-all should only contain security headers, neverCache-Control(it would override specific rules)
Core Web Vitals
CLS (Cumulative Layout Shift)
All images need explicit width and height attributes. Without them, the browser doesn’t know how much space to reserve, causing layout shifts when images load.
Critical files:
src/pages/blog/[slug].astro— hero imagesrc/pages/posts/[slug].astro— hero imagesrc/pages/configurations/[slug].astro— sidebar imagesrc/pages/projects/[slug].astro— sidebar imagesrc/layouts/BlogPost.astro— hero image + avatar
LCP (Largest Contentful Paint)
Hero images must use loading="eager" and fetchpriority="high". The default loading="lazy" defers the download, which is wrong for above-fold content.
<!-- WRONG -->
<img src={heroImage} loading="lazy" />
<!-- CORRECT -->
<img src={heroImage} width="1024" height="512"
loading="eager" decoding="async" fetchpriority="high" />
Font Optimization
Self-hosted WOFF2 fonts with preload hints in both layouts:
<link rel="preload" href="/fonts/SpaceGrotesk-latin.woff2"
as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/Inter-latin.woff2"
as="font" type="font/woff2" crossorigin />
All @font-face declarations use font-display: swap (in src/styles/fonts.css).
Preconnect Hints
External origins get preconnect hints to save DNS+TLS roundtrips:
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com" />
Added to both BaseLayout.astro and CosmicLayout.astro.
Security Headers
Applied globally via /* in _headers:
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options | nosniff | Prevent MIME sniffing |
X-Frame-Options | DENY | Prevent clickjacking |
Referrer-Policy | strict-origin-when-cross-origin | Limit referrer leaks |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Disable device APIs |
Cross-Origin-Opener-Policy | same-origin | Isolate browsing context |
Cross-Origin-Resource-Policy | same-origin | Prevent cross-origin reads |
Content-Security-Policy | Full policy | XSS prevention |
HSTS is configured in the Cloudflare dashboard (not _headers).
Proxy Route Error Handling
All API proxy routes (/api/gateway/*, /api/proxy/*, /api/orchestrator/*, etc.) must wrap fetch calls in try/catch and return graceful 502 JSON responses when backends are unreachable. Without this, the CF Worker crashes and returns a raw 500 to the client.
Pattern:
try {
const result = await cachedFetch(key, () => fetchOrigin(url));
return new Response(result.body, { status: result.status, headers });
} catch (error) {
return new Response(
JSON.stringify({ error: 'Service unavailable', detail: error.message }),
{ status: 502, headers: { 'Content-Type': 'application/json' } }
);
}
4xx/5xx Error Sources
- 4xx (~22k): Mostly bot/scanner noise — WordPress paths,
.env,xmlrpc.php. All 536 sitemap URLs return 200. Consider a Cloudflare WAF rule to block common scanner paths. - 5xx (~1.7k): Intentional 502/503 from proxy routes when homelab services are offline. After wrapping all routes in try/catch, unhandled 500s should be near zero.