diff --git a/src/build/content/static.ts b/src/build/content/static.ts index 8231ec1fd2..f972e8b8bd 100644 --- a/src/build/content/static.ts +++ b/src/build/content/static.ts @@ -77,6 +77,20 @@ export const copyStaticAssets = async (ctx: PluginContext): Promise => { }) } +export const setHeadersConfig = async (ctx: PluginContext): Promise => { + // https://nextjs.org/docs/app/api-reference/config/next-config-js/headers#cache-control + // Next.js sets the Cache-Control header of public, max-age=31536000, immutable for truly + // immutable assets. It cannot be overridden. These immutable files contain a SHA-hash in + // the file name, so they can be safely cached indefinitely. + const { basePath } = ctx.buildConfig + ctx.netlifyConfig.headers.push({ + for: `${basePath}/_next/static/*`, + values: { + 'Cache-Control': 'public, max-age=31536000, immutable', + }, + }) +} + export const copyStaticExport = async (ctx: PluginContext): Promise => { await tracer.withActiveSpan('copyStaticExport', async () => { if (!ctx.exportDetail?.outDirectory) { diff --git a/src/index.ts b/src/index.ts index 219a554615..ce109ef501 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { copyStaticContent, copyStaticExport, publishStaticDir, + setHeadersConfig, unpublishStaticDir, } from './build/content/static.js' import { clearStaleEdgeHandlers, createEdgeHandlers } from './build/functions/edge.js' @@ -66,7 +67,7 @@ export const onBuild = async (options: NetlifyPluginOptions) => { // static exports only need to be uploaded to the CDN and setup /_next/image handler if (ctx.buildConfig.output === 'export') { - return Promise.all([copyStaticExport(ctx), setImageConfig(ctx)]) + return Promise.all([copyStaticExport(ctx), setHeadersConfig(ctx), setImageConfig(ctx)]) } await verifyAdvancedAPIRoutes(ctx) @@ -78,6 +79,7 @@ export const onBuild = async (options: NetlifyPluginOptions) => { copyPrerenderedContent(ctx), createServerHandler(ctx), createEdgeHandlers(ctx), + setHeadersConfig(ctx), setImageConfig(ctx), ]) }) diff --git a/tests/utils/fixture.ts b/tests/utils/fixture.ts index 9ac5645e6f..59e8b7b09e 100644 --- a/tests/utils/fixture.ts +++ b/tests/utils/fixture.ts @@ -207,6 +207,7 @@ export async function runPluginStep( // INTERNAL_EDGE_FUNCTIONS_SRC: '.netlify/edge-functions', }, netlifyConfig: { + headers: [], redirects: [], }, utils: { diff --git a/tests/utils/playwright-helpers.ts b/tests/utils/playwright-helpers.ts index 8e65624df7..3c05b0c6b2 100644 --- a/tests/utils/playwright-helpers.ts +++ b/tests/utils/playwright-helpers.ts @@ -14,6 +14,7 @@ const makeE2EFixture = ( export const test = base.extend< { + ensureStaticAssetsHaveImmutableCacheControl: void takeScreenshot: void pollUntilHeadersMatch: ( url: string, @@ -91,4 +92,19 @@ export const test = base.extend< }, { auto: true }, ], + ensureStaticAssetsHaveImmutableCacheControl: [ + async ({ page }, use) => { + page.on('response', (response) => { + if (response.url().includes('/_next/static/')) { + expect( + response.headers()['cache-control'], + '_next/static assets should have immutable cache control', + ).toContain('public,max-age=31536000,immutable') + } + }) + + await use() + }, + { auto: true }, + ], })