diff --git a/next.config.js b/next.config.js index f23eb2c6..7bcc16c3 100644 --- a/next.config.js +++ b/next.config.js @@ -25,10 +25,26 @@ const nextConfig = { transpilePackages: ["jose"], serverExternalPackages: ["@duckdb/node-api"], async headers() { + // ponytail: un-hashed filenames under public/, so `immutable` is unsafe — + // a redeploy has to be able to replace these in place. An hour of freshness + // drops the per-navigation revalidation round-trip without stranding a + // stale asset for long. + const staticAssets = [ + { + source: "/:dir(img|logo)/:path*", + headers: [ + { + key: "Cache-Control", + value: "public, max-age=3600, stale-while-revalidate=604800", + }, + ], + }, + ]; if (process.env.STAGE === "prod") { - return []; + return staticAssets; } return [ + ...staticAssets, { source: "/:path*", headers: [ diff --git a/next.config.test.ts b/next.config.test.ts index f6ae8a34..5e5de366 100644 --- a/next.config.test.ts +++ b/next.config.test.ts @@ -1,5 +1,20 @@ import nextConfig from "./next.config.js"; +const STATIC_ASSET_RULE = { + source: "/:dir(img|logo)/:path*", + headers: [ + { + key: "Cache-Control", + value: "public, max-age=3600, stale-while-revalidate=604800", + }, + ], +}; + +const NOINDEX_RULE = { + source: "/:path*", + headers: [{ key: "X-Robots-Tag", value: "noindex, nofollow" }], +}; + describe("next.config headers()", () => { const originalStage = process.env.STAGE; @@ -14,28 +29,26 @@ describe("next.config headers()", () => { test("non-prod stage emits noindex header on all paths", async () => { process.env.STAGE = "dev"; const result = await nextConfig.headers!(); - expect(result).toEqual([ - { - source: "/:path*", - headers: [{ key: "X-Robots-Tag", value: "noindex, nofollow" }], - }, - ]); + expect(result).toEqual([STATIC_ASSET_RULE, NOINDEX_RULE]); }); - test("prod stage emits no header overrides", async () => { + test("prod stage emits no noindex override", async () => { process.env.STAGE = "prod"; const result = await nextConfig.headers!(); - expect(result).toEqual([]); + expect(result).toEqual([STATIC_ASSET_RULE]); }); test("missing STAGE defaults to noindex", async () => { delete process.env.STAGE; const result = await nextConfig.headers!(); - expect(result).toEqual([ - { - source: "/:path*", - headers: [{ key: "X-Robots-Tag", value: "noindex, nofollow" }], - }, - ]); + expect(result).toEqual([STATIC_ASSET_RULE, NOINDEX_RULE]); + }); + + test("static assets stay cacheable in every stage", async () => { + for (const stage of ["prod", "dev"]) { + process.env.STAGE = stage; + const result = await nextConfig.headers!(); + expect(result).toContainEqual(STATIC_ASSET_RULE); + } }); });