Skip to content

Commit 9135d10

Browse files
committed
fix: only set permament caching header when reading html file when its not during server initialization AND when read html is Next produced fully static html
1 parent 9fe6763 commit 9135d10

File tree

7 files changed

+161
-56
lines changed

7 files changed

+161
-56
lines changed

src/build/content/static.test.ts

Lines changed: 95 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { beforeEach, describe, expect, Mock, test, vi } from 'vitest'
1010
import { decodeBlobKey, encodeBlobKey, mockFileSystem } from '../../../tests/index.js'
1111
import { type FixtureTestContext } from '../../../tests/utils/contexts.js'
1212
import { createFsFixture } from '../../../tests/utils/fixture.js'
13+
import { HtmlBlob } from '../../shared/blob-types.cjs'
1314
import { PluginContext, RequiredServerFilesManifest } from '../plugin-context.js'
1415

1516
import { copyStaticAssets, copyStaticContent } from './static.js'
@@ -22,18 +23,19 @@ type Context = FixtureTestContext & {
2223
const createFsFixtureWithBasePath = (
2324
fixture: Record<string, string>,
2425
ctx: Omit<Context, 'pluginContext'>,
25-
2626
{
2727
basePath = '',
2828
// eslint-disable-next-line unicorn/no-useless-undefined
2929
i18n = undefined,
3030
dynamicRoutes = {},
31+
pagesManifest = {},
3132
}: {
3233
basePath?: string
3334
i18n?: Pick<NonNullable<RequiredServerFilesManifest['config']['i18n']>, 'locales'>
3435
dynamicRoutes?: {
3536
[route: string]: Pick<PrerenderManifest['dynamicRoutes'][''], 'fallback'>
3637
}
38+
pagesManifest?: Record<string, string>
3739
} = {},
3840
) => {
3941
return createFsFixture(
@@ -49,6 +51,7 @@ const createFsFixtureWithBasePath = (
4951
},
5052
} as Pick<RequiredServerFilesManifest, 'relativeAppDir' | 'appDir'>),
5153
[join(ctx.publishDir, 'prerender-manifest.json')]: JSON.stringify({ dynamicRoutes }),
54+
[join(ctx.publishDir, 'server', 'pages-manifest.json')]: JSON.stringify(pagesManifest),
5255
},
5356
ctx,
5457
)
@@ -62,10 +65,7 @@ async function readDirRecursive(dir: string) {
6265
return paths
6366
}
6467

65-
let failBuildMock: Mock<
66-
Parameters<PluginContext['utils']['build']['failBuild']>,
67-
ReturnType<PluginContext['utils']['build']['failBuild']>
68-
>
68+
let failBuildMock: Mock<PluginContext['utils']['build']['failBuild']>
6969

7070
const dontFailTest: PluginContext['utils']['build']['failBuild'] = () => {
7171
return undefined as never
@@ -197,12 +197,13 @@ describe('Regular Repository layout', () => {
197197
)
198198
})
199199

200-
describe('should copy the static pages to the publish directory if there are no corresponding JSON files and mark wether html file is a fallback', () => {
200+
describe('should copy the static pages to the publish directory if there are no corresponding JSON files and mark wether html file is a fully static pages router page', () => {
201201
test<Context>('no i18n', async ({ pluginContext, ...ctx }) => {
202202
await createFsFixtureWithBasePath(
203203
{
204204
'.next/server/pages/test.html': '',
205205
'.next/server/pages/test2.html': '',
206+
'.next/server/pages/test3.html': '',
206207
'.next/server/pages/test3.json': '',
207208
'.next/server/pages/blog/[slug].html': '',
208209
},
@@ -213,27 +214,36 @@ describe('Regular Repository layout', () => {
213214
fallback: '/blog/[slug].html',
214215
},
215216
},
217+
pagesManifest: {
218+
'/blog/[slug]': 'pages/blog/[slug].js',
219+
'/test': 'pages/test.html',
220+
'/test2': 'pages/test2.html',
221+
'/test3': 'pages/test3.js',
222+
},
216223
},
217224
)
218225

219226
await copyStaticContent(pluginContext)
220227
const files = await glob('**/*', { cwd: pluginContext.blobDir, dot: true })
221228

222-
const expectedStaticPages = ['blog/[slug].html', 'test.html', 'test2.html']
223-
const expectedFallbacks = new Set(['blog/[slug].html'])
229+
const expectedHtmlBlobs = ['blog/[slug].html', 'test.html', 'test2.html']
230+
const expectedFullyStaticPages = new Set(['test.html', 'test2.html'])
224231

225-
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedStaticPages)
232+
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedHtmlBlobs)
226233

227-
for (const page of expectedStaticPages) {
228-
const expectedIsFallback = expectedFallbacks.has(page)
234+
for (const page of expectedHtmlBlobs) {
235+
const expectedIsFullyStaticPage = expectedFullyStaticPages.has(page)
229236

230237
const blob = JSON.parse(
231238
await readFile(join(pluginContext.blobDir, await encodeBlobKey(page)), 'utf-8'),
232-
)
239+
) as HtmlBlob
233240

234-
expect(blob, `${page} should ${expectedIsFallback ? '' : 'not '}be a fallback`).toEqual({
241+
expect(
242+
blob,
243+
`${page} should ${expectedIsFullyStaticPage ? '' : 'not '}be a fully static Page`,
244+
).toEqual({
235245
html: '',
236-
isFallback: expectedIsFallback,
246+
isFullyStaticPage: expectedIsFullyStaticPage,
237247
})
238248
}
239249
})
@@ -243,10 +253,12 @@ describe('Regular Repository layout', () => {
243253
{
244254
'.next/server/pages/de/test.html': '',
245255
'.next/server/pages/de/test2.html': '',
256+
'.next/server/pages/de/test3.html': '',
246257
'.next/server/pages/de/test3.json': '',
247258
'.next/server/pages/de/blog/[slug].html': '',
248259
'.next/server/pages/en/test.html': '',
249260
'.next/server/pages/en/test2.html': '',
261+
'.next/server/pages/en/test3.html': '',
250262
'.next/server/pages/en/test3.json': '',
251263
'.next/server/pages/en/blog/[slug].html': '',
252264
},
@@ -260,34 +272,50 @@ describe('Regular Repository layout', () => {
260272
i18n: {
261273
locales: ['en', 'de'],
262274
},
275+
pagesManifest: {
276+
'/blog/[slug]': 'pages/blog/[slug].js',
277+
'/en/test': 'pages/en/test.html',
278+
'/de/test': 'pages/de/test.html',
279+
'/en/test2': 'pages/en/test2.html',
280+
'/de/test2': 'pages/de/test2.html',
281+
'/test3': 'pages/test3.js',
282+
},
263283
},
264284
)
265285

266286
await copyStaticContent(pluginContext)
267287
const files = await glob('**/*', { cwd: pluginContext.blobDir, dot: true })
268288

269-
const expectedStaticPages = [
289+
const expectedHtmlBlobs = [
270290
'de/blog/[slug].html',
271291
'de/test.html',
272292
'de/test2.html',
273293
'en/blog/[slug].html',
274294
'en/test.html',
275295
'en/test2.html',
276296
]
277-
const expectedFallbacks = new Set(['en/blog/[slug].html', 'de/blog/[slug].html'])
297+
const expectedFullyStaticPages = new Set([
298+
'en/test.html',
299+
'de/test.html',
300+
'en/test2.html',
301+
'de/test2.html',
302+
])
278303

279-
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedStaticPages)
304+
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedHtmlBlobs)
280305

281-
for (const page of expectedStaticPages) {
282-
const expectedIsFallback = expectedFallbacks.has(page)
306+
for (const page of expectedHtmlBlobs) {
307+
const expectedIsFullyStaticPage = expectedFullyStaticPages.has(page)
283308

284309
const blob = JSON.parse(
285310
await readFile(join(pluginContext.blobDir, await encodeBlobKey(page)), 'utf-8'),
286-
)
311+
) as HtmlBlob
287312

288-
expect(blob, `${page} should ${expectedIsFallback ? '' : 'not '}be a fallback`).toEqual({
313+
expect(
314+
blob,
315+
`${page} should ${expectedIsFullyStaticPage ? '' : 'not '}be a fully static Page`,
316+
).toEqual({
289317
html: '',
290-
isFallback: expectedIsFallback,
318+
isFullyStaticPage: expectedIsFullyStaticPage,
291319
})
292320
}
293321
})
@@ -419,12 +447,13 @@ describe('Mono Repository', () => {
419447
)
420448
})
421449

422-
describe('should copy the static pages to the publish directory if there are no corresponding JSON files and mark wether html file is a fallback', () => {
450+
describe('should copy the static pages to the publish directory if there are no corresponding JSON files and mark wether html file is a fully static pages router page', () => {
423451
test<Context>('no i18n', async ({ pluginContext, ...ctx }) => {
424452
await createFsFixtureWithBasePath(
425453
{
426454
'apps/app-1/.next/server/pages/test.html': '',
427455
'apps/app-1/.next/server/pages/test2.html': '',
456+
'apps/app-1/.next/server/pages/test3.html': '',
428457
'apps/app-1/.next/server/pages/test3.json': '',
429458
'apps/app-1/.next/server/pages/blog/[slug].html': '',
430459
},
@@ -435,27 +464,36 @@ describe('Mono Repository', () => {
435464
fallback: '/blog/[slug].html',
436465
},
437466
},
467+
pagesManifest: {
468+
'/blog/[slug]': 'pages/blog/[slug].js',
469+
'/test': 'pages/test.html',
470+
'/test2': 'pages/test2.html',
471+
'/test3': 'pages/test3.js',
472+
},
438473
},
439474
)
440475

441476
await copyStaticContent(pluginContext)
442477
const files = await glob('**/*', { cwd: pluginContext.blobDir, dot: true })
443478

444-
const expectedStaticPages = ['blog/[slug].html', 'test.html', 'test2.html']
445-
const expectedFallbacks = new Set(['blog/[slug].html'])
479+
const expectedHtmlBlobs = ['blog/[slug].html', 'test.html', 'test2.html']
480+
const expectedFullyStaticPages = new Set(['test.html', 'test2.html'])
446481

447-
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedStaticPages)
482+
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedHtmlBlobs)
448483

449-
for (const page of expectedStaticPages) {
450-
const expectedIsFallback = expectedFallbacks.has(page)
484+
for (const page of expectedHtmlBlobs) {
485+
const expectedIsFullyStaticPage = expectedFullyStaticPages.has(page)
451486

452487
const blob = JSON.parse(
453488
await readFile(join(pluginContext.blobDir, await encodeBlobKey(page)), 'utf-8'),
454-
)
489+
) as HtmlBlob
455490

456-
expect(blob, `${page} should ${expectedIsFallback ? '' : 'not '}be a fallback`).toEqual({
491+
expect(
492+
blob,
493+
`${page} should ${expectedIsFullyStaticPage ? '' : 'not '}be a fully static Page`,
494+
).toEqual({
457495
html: '',
458-
isFallback: expectedIsFallback,
496+
isFullyStaticPage: expectedIsFullyStaticPage,
459497
})
460498
}
461499
})
@@ -465,10 +503,12 @@ describe('Mono Repository', () => {
465503
{
466504
'apps/app-1/.next/server/pages/de/test.html': '',
467505
'apps/app-1/.next/server/pages/de/test2.html': '',
506+
'apps/app-1/.next/server/pages/de/test3.html': '',
468507
'apps/app-1/.next/server/pages/de/test3.json': '',
469508
'apps/app-1/.next/server/pages/de/blog/[slug].html': '',
470509
'apps/app-1/.next/server/pages/en/test.html': '',
471510
'apps/app-1/.next/server/pages/en/test2.html': '',
511+
'apps/app-1/.next/server/pages/en/test3.html': '',
472512
'apps/app-1/.next/server/pages/en/test3.json': '',
473513
'apps/app-1/.next/server/pages/en/blog/[slug].html': '',
474514
},
@@ -482,34 +522,50 @@ describe('Mono Repository', () => {
482522
i18n: {
483523
locales: ['en', 'de'],
484524
},
525+
pagesManifest: {
526+
'/blog/[slug]': 'pages/blog/[slug].js',
527+
'/en/test': 'pages/en/test.html',
528+
'/de/test': 'pages/de/test.html',
529+
'/en/test2': 'pages/en/test2.html',
530+
'/de/test2': 'pages/de/test2.html',
531+
'/test3': 'pages/test3.js',
532+
},
485533
},
486534
)
487535

488536
await copyStaticContent(pluginContext)
489537
const files = await glob('**/*', { cwd: pluginContext.blobDir, dot: true })
490538

491-
const expectedStaticPages = [
539+
const expectedHtmlBlobs = [
492540
'de/blog/[slug].html',
493541
'de/test.html',
494542
'de/test2.html',
495543
'en/blog/[slug].html',
496544
'en/test.html',
497545
'en/test2.html',
498546
]
499-
const expectedFallbacks = new Set(['en/blog/[slug].html', 'de/blog/[slug].html'])
547+
const expectedFullyStaticPages = new Set([
548+
'en/test.html',
549+
'de/test.html',
550+
'en/test2.html',
551+
'de/test2.html',
552+
])
500553

501-
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedStaticPages)
554+
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedHtmlBlobs)
502555

503-
for (const page of expectedStaticPages) {
504-
const expectedIsFallback = expectedFallbacks.has(page)
556+
for (const page of expectedHtmlBlobs) {
557+
const expectedIsFullyStaticPage = expectedFullyStaticPages.has(page)
505558

506559
const blob = JSON.parse(
507560
await readFile(join(pluginContext.blobDir, await encodeBlobKey(page)), 'utf-8'),
508-
)
561+
) as HtmlBlob
509562

510-
expect(blob, `${page} should ${expectedIsFallback ? '' : 'not '}be a fallback`).toEqual({
563+
expect(
564+
blob,
565+
`${page} should ${expectedIsFullyStaticPage ? '' : 'not '}be a fully static Page`,
566+
).toEqual({
511567
html: '',
512-
isFallback: expectedIsFallback,
568+
isFullyStaticPage: expectedIsFullyStaticPage,
513569
})
514570
}
515571
})

src/build/content/static.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,23 @@ export const copyStaticContent = async (ctx: PluginContext): Promise<void> => {
2727
})
2828

2929
const fallbacks = ctx.getFallbacks(await ctx.getPrerenderManifest())
30+
const fullyStaticPages = await ctx.getFullyStaticHtmlPages()
3031

3132
try {
3233
await mkdir(destDir, { recursive: true })
3334
await Promise.all(
3435
paths
35-
.filter((path) => !paths.includes(`${path.slice(0, -5)}.json`))
36+
.filter((path) => !path.endsWith('.json') && !paths.includes(`${path.slice(0, -5)}.json`))
3637
.map(async (path): Promise<void> => {
3738
const html = await readFile(join(srcDir, path), 'utf-8')
3839
verifyNetlifyForms(ctx, html)
3940

4041
const isFallback = fallbacks.includes(path.slice(0, -5))
42+
const isFullyStaticPage = !isFallback && fullyStaticPages.includes(path)
4143

4244
await writeFile(
4345
join(destDir, await encodeBlobKey(path)),
44-
JSON.stringify({ html, isFallback } satisfies HtmlBlob),
46+
JSON.stringify({ html, isFullyStaticPage } satisfies HtmlBlob),
4547
'utf-8',
4648
)
4749
}),

src/build/plugin-context.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs'
22
import { readFile } from 'node:fs/promises'
33
import { createRequire } from 'node:module'
44
import { join, relative, resolve } from 'node:path'
5-
import { join as posixJoin } from 'node:path/posix'
5+
import { join as posixJoin, relative as posixRelative } from 'node:path/posix'
66
import { fileURLToPath } from 'node:url'
77

88
import type {
@@ -12,6 +12,7 @@ import type {
1212
} from '@netlify/build'
1313
import type { PrerenderManifest, RoutesManifest } from 'next/dist/build/index.js'
1414
import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
15+
import type { PagesManifest } from 'next/dist/build/webpack/plugins/pages-manifest-plugin.js'
1516
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
1617
import { satisfies } from 'semver'
1718

@@ -370,6 +371,35 @@ export class PluginContext {
370371
return this.#fallbacks
371372
}
372373

374+
#fullyStaticHtmlPages: string[] | null = null
375+
/**
376+
* Get an array of fully static pages router pages (no `getServerSideProps` or `getStaticProps`).
377+
* Those are being served as-is without involving CacheHandler, so we need to keep track of them
378+
* to make sure we apply permanent caching headers for responses that use them.
379+
*/
380+
async getFullyStaticHtmlPages(): Promise<string[]> {
381+
if (!this.#fullyStaticHtmlPages) {
382+
const pagesManifest = JSON.parse(
383+
await readFile(join(this.publishDir, 'server/pages-manifest.json'), 'utf-8'),
384+
) as PagesManifest
385+
386+
this.#fullyStaticHtmlPages = Object.values(pagesManifest)
387+
.filter(
388+
(filePath) =>
389+
// Limit handling to pages router files (App Router pages should not be included in pages-manifest.json
390+
// as they have their own app-paths-manifest.json)
391+
filePath.startsWith('pages/') &&
392+
// Fully static pages will have entries in the pages-manifest.json pointing to .html files.
393+
// Pages with data fetching exports will point to .js files.
394+
filePath.endsWith('.html'),
395+
)
396+
// values will be prefixed with `pages/`, so removing it here for consistency with other methods
397+
// like `getFallbacks` that return the route without the prefix
398+
.map((filePath) => posixRelative('pages', filePath))
399+
}
400+
return this.#fullyStaticHtmlPages
401+
}
402+
373403
/** Fails a build with a message and an optional error */
374404
failBuild(message: string, error?: unknown): never {
375405
return this.utils.build.failBuild(message, error instanceof Error ? { error } : undefined)

0 commit comments

Comments
 (0)