Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9135d10

Browse filesBrowse 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
Copy full SHA for 9135d10

File tree

Expand file treeCollapse file tree

7 files changed

+161
-56
lines changed
Filter options
Expand file treeCollapse file tree

7 files changed

+161
-56
lines changed

‎src/build/content/static.test.ts

Copy file name to clipboardExpand all lines: src/build/content/static.test.ts
+95-39Lines 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

Copy file name to clipboardExpand all lines: src/build/content/static.ts
+4-2Lines 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

Copy file name to clipboardExpand all lines: src/build/plugin-context.ts
+31-1Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.