Skip to content

Commit 5bd68dd

Browse files
authoredFeb 21, 2025
fix: set immutable cache-control for _next/static (#2767)
* test: assert that _next/static have immutable cache control * fix: set immutable cache-control for _next/static
1 parent f4b59b6 commit 5bd68dd

File tree

4 files changed

+34
-1
lines changed

4 files changed

+34
-1
lines changed
 

‎src/build/content/static.ts

+14
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,20 @@ export const copyStaticAssets = async (ctx: PluginContext): Promise<void> => {
7777
})
7878
}
7979

80+
export const setHeadersConfig = async (ctx: PluginContext): Promise<void> => {
81+
// https://nextjs.org/docs/app/api-reference/config/next-config-js/headers#cache-control
82+
// Next.js sets the Cache-Control header of public, max-age=31536000, immutable for truly
83+
// immutable assets. It cannot be overridden. These immutable files contain a SHA-hash in
84+
// the file name, so they can be safely cached indefinitely.
85+
const { basePath } = ctx.buildConfig
86+
ctx.netlifyConfig.headers.push({
87+
for: `${basePath}/_next/static/*`,
88+
values: {
89+
'Cache-Control': 'public, max-age=31536000, immutable',
90+
},
91+
})
92+
}
93+
8094
export const copyStaticExport = async (ctx: PluginContext): Promise<void> => {
8195
await tracer.withActiveSpan('copyStaticExport', async () => {
8296
if (!ctx.exportDetail?.outDirectory) {

‎src/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
copyStaticContent,
1212
copyStaticExport,
1313
publishStaticDir,
14+
setHeadersConfig,
1415
unpublishStaticDir,
1516
} from './build/content/static.js'
1617
import { clearStaleEdgeHandlers, createEdgeHandlers } from './build/functions/edge.js'
@@ -66,7 +67,7 @@ export const onBuild = async (options: NetlifyPluginOptions) => {
6667

6768
// static exports only need to be uploaded to the CDN and setup /_next/image handler
6869
if (ctx.buildConfig.output === 'export') {
69-
return Promise.all([copyStaticExport(ctx), setImageConfig(ctx)])
70+
return Promise.all([copyStaticExport(ctx), setHeadersConfig(ctx), setImageConfig(ctx)])
7071
}
7172

7273
await verifyAdvancedAPIRoutes(ctx)
@@ -78,6 +79,7 @@ export const onBuild = async (options: NetlifyPluginOptions) => {
7879
copyPrerenderedContent(ctx),
7980
createServerHandler(ctx),
8081
createEdgeHandlers(ctx),
82+
setHeadersConfig(ctx),
8183
setImageConfig(ctx),
8284
])
8385
})

‎tests/utils/fixture.ts

+1
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ export async function runPluginStep(
207207
// INTERNAL_EDGE_FUNCTIONS_SRC: '.netlify/edge-functions',
208208
},
209209
netlifyConfig: {
210+
headers: [],
210211
redirects: [],
211212
},
212213
utils: {

‎tests/utils/playwright-helpers.ts

+16
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const makeE2EFixture = (
1414

1515
export const test = base.extend<
1616
{
17+
ensureStaticAssetsHaveImmutableCacheControl: void
1718
takeScreenshot: void
1819
pollUntilHeadersMatch: (
1920
url: string,
@@ -91,4 +92,19 @@ export const test = base.extend<
9192
},
9293
{ auto: true },
9394
],
95+
ensureStaticAssetsHaveImmutableCacheControl: [
96+
async ({ page }, use) => {
97+
page.on('response', (response) => {
98+
if (response.url().includes('/_next/static/')) {
99+
expect(
100+
response.headers()['cache-control'],
101+
'_next/static assets should have immutable cache control',
102+
).toContain('public,max-age=31536000,immutable')
103+
}
104+
})
105+
106+
await use()
107+
},
108+
{ auto: true },
109+
],
94110
})

0 commit comments

Comments
 (0)
Please sign in to comment.