From 56c76af73937d591515b3d2be1031ebf3039e52f Mon Sep 17 00:00:00 2001 From: Arnas Donauskas Date: Fri, 17 Apr 2026 14:34:46 +0300 Subject: [PATCH] fix(deploy): Cache-Control headers so CDN stops serving stale HTML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: Hostinger's CDN was applying s-maxage=31536000 (1 year) to prerendered HTML pages by default. Each deploy produced fresh Turbopack chunk hashes, but the edge kept serving year-old HTML that referenced chunk filenames the new build no longer had on disk — resulting in HTML 200 + every /_next/static/*.{js,css} 404, page rendered unstyled. Private/incognito didn't help because the cache is at the edge, not the browser. Fix: explicit Cache-Control via next.config.ts headers(): - /_next/static/* — public, max-age=31536000, immutable Content-hashed filenames; safe forever. - /api/* — no-store Per-user, must never be shared at the edge. - everything else — public, max-age=0, s-maxage=300, stale-while-revalidate=86400 Edge serves instantly from cache for 5 min, then keeps serving cached content while refreshing in the background for up to 24h. Chunk-hash drift after a deploy self-heals within ~5 min. Verification after deploy: curl -I https:///login | grep -i cache-control Expect: public, max-age=0, s-maxage=300, stale-while-revalidate=86400 (If Hostinger overwrites this, fall back to configuring purge-on-deploy at the platform layer.) Co-Authored-By: Claude Opus 4.7 (1M context) --- next.config.ts | 60 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/next.config.ts b/next.config.ts index e9ffa30..72f70b9 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,65 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + /** + * Cache-Control policy. + * + * Why this exists: + * Hostinger's CDN was applying `s-maxage=31536000` (1 year) to + * prerendered HTML pages by default. When a new deploy shipped + * fresh Turbopack chunk hashes, the edge kept serving year-old + * HTML referencing chunk filenames that no longer existed on + * disk — result: HTML 200, every /_next/static/*.js and .css + * came back 404, the page rendered unstyled. Private/incognito + * did nothing because the cache is server-side. + * + * Strategy: + * - /_next/static/* — immutable for a year. Filenames are + * content-hashed, so a new build produces new filenames; the + * old ones are safe to keep indefinitely in caches. + * - /api/* — no-store. API responses are per-user and + * must never be shared across requests at the edge. + * - Everything else — public, brief s-maxage + generous + * stale-while-revalidate. The edge serves instantly from cache + * for the first 5 min, then returns cached content while + * refreshing in the background for up to 24 h. A deploy's + * chunk-hash drift self-heals within ~5 min with no user- + * visible latency. + * + * Note: dynamic dashboard routes (/inbox, /contacts, /pipelines, + * /broadcasts, etc.) are server-rendered per request — Next.js + * and Supabase auth already prevent them from being served + * from a shared cache. The s-maxage here is a ceiling; Next.js + * and auth middleware still set `private` / `no-store` for + * per-user responses. + */ + async headers() { + return [ + { + source: "/_next/static/:path*", + headers: [ + { + key: "Cache-Control", + value: "public, max-age=31536000, immutable", + }, + ], + }, + { + source: "/api/:path*", + headers: [{ key: "Cache-Control", value: "no-store" }], + }, + { + source: "/:path*", + headers: [ + { + key: "Cache-Control", + value: + "public, max-age=0, s-maxage=300, stale-while-revalidate=86400", + }, + ], + }, + ]; + }, }; export default nextConfig;