Add initial support for static 404 page (#10113)
* Add initial support for static 404 page * Apply suggestions from code review Co-Authored-By: Tim Neutkens <tim@timneutkens.nl> * Simplify custom error page check * Add comment explaining reason for custom app check Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
This commit is contained in:
parent
71c2354ecd
commit
e04e5a5c15
5 changed files with 144 additions and 2 deletions
|
@ -197,6 +197,9 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
const pageKeys = Object.keys(mappedPages)
|
||||
const dynamicRoutes = pageKeys.filter(page => isDynamicRoute(page))
|
||||
const conflictingPublicFiles: string[] = []
|
||||
const hasCustomErrorPage = mappedPages['/_error'].startsWith(
|
||||
'private-next-pages'
|
||||
)
|
||||
|
||||
if (hasPublicDir) {
|
||||
try {
|
||||
|
@ -520,6 +523,13 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
)
|
||||
staticCheckWorkers.end()
|
||||
|
||||
// Since custom _app.js can wrap the 404 page we have to opt-out of static optimization if it has getInitialProps
|
||||
// Only export the static 404 when there is no /_error present
|
||||
const useStatic404 =
|
||||
!customAppGetInitialProps &&
|
||||
!hasCustomErrorPage &&
|
||||
config.experimental.static404
|
||||
|
||||
if (invalidPages.size > 0) {
|
||||
throw new Error(
|
||||
`Build optimization failed: found page${
|
||||
|
@ -550,7 +560,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
const finalPrerenderRoutes: { [route: string]: SsgRoute } = {}
|
||||
const tbdPrerenderRoutes: string[] = []
|
||||
|
||||
if (staticPages.size > 0 || ssgPages.size > 0) {
|
||||
if (staticPages.size > 0 || ssgPages.size > 0 || useStatic404) {
|
||||
const combinedPages = [...staticPages, ...ssgPages]
|
||||
const exportApp = require('../export').default
|
||||
const exportOptions = {
|
||||
|
@ -586,6 +596,11 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
defaultMap[route] = { page }
|
||||
})
|
||||
})
|
||||
|
||||
if (useStatic404) {
|
||||
defaultMap['/_errors/404'] = { page: '/_error' }
|
||||
}
|
||||
|
||||
return defaultMap
|
||||
},
|
||||
exportTrailingSlash: false,
|
||||
|
@ -626,6 +641,10 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
await fsMove(orig, dest)
|
||||
}
|
||||
|
||||
if (useStatic404) {
|
||||
await moveExportedPage('/_errors/404', '/_errors/404', false, 'html')
|
||||
}
|
||||
|
||||
for (const page of combinedPages) {
|
||||
const isSsg = ssgPages.has(page)
|
||||
const isDynamic = isDynamicRoute(page)
|
||||
|
|
|
@ -51,6 +51,7 @@ const defaultConfig: { [key: string]: any } = {
|
|||
reactMode: 'legacy',
|
||||
workerThreads: false,
|
||||
basePath: '',
|
||||
static404: false,
|
||||
},
|
||||
future: {
|
||||
excludeDefaultMomentLocales: false,
|
||||
|
|
|
@ -1049,7 +1049,23 @@ export default class Server {
|
|||
_pathname: string,
|
||||
query: ParsedUrlQuery = {}
|
||||
) {
|
||||
const result = await this.findPageComponents('/_error', query)
|
||||
let result: null | LoadComponentsReturnType = null
|
||||
|
||||
// use static 404 page if available and is 404 response
|
||||
if (this.nextConfig.experimental.static404 && err === null) {
|
||||
try {
|
||||
result = await this.findPageComponents('/_errors/404')
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
result = await this.findPageComponents('/_error', query)
|
||||
}
|
||||
|
||||
let html
|
||||
try {
|
||||
html = await this.renderToHTMLWithComponents(
|
||||
|
|
1
test/integration/static-404/pages/index.js
Normal file
1
test/integration/static-404/pages/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export default () => 'hi'
|
105
test/integration/static-404/test/index.test.js
Normal file
105
test/integration/static-404/test/index.test.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
/* eslint-env jest */
|
||||
/* global jasmine */
|
||||
import fs from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import {
|
||||
renderViaHTTP,
|
||||
findPort,
|
||||
nextBuild,
|
||||
nextStart,
|
||||
killApp,
|
||||
} from 'next-test-utils'
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2
|
||||
const appDir = join(__dirname, '..')
|
||||
const nextConfig = join(appDir, 'next.config.js')
|
||||
const static404 = join(
|
||||
appDir,
|
||||
'.next/server/static/test-id/pages/_errors/404.html'
|
||||
)
|
||||
const appPage = join(appDir, 'pages/_app.js')
|
||||
const errorPage = join(appDir, 'pages/_error.js')
|
||||
const buildId = `generateBuildId: () => 'test-id'`
|
||||
const experimentalConfig = `experimental: { static404: true }`
|
||||
let app
|
||||
let appPort
|
||||
|
||||
describe('Static 404 page', () => {
|
||||
afterEach(async () => {
|
||||
await fs.remove(appPage)
|
||||
await fs.remove(errorPage)
|
||||
await fs.remove(nextConfig)
|
||||
})
|
||||
beforeEach(() => fs.remove(join(appDir, '.next/server')))
|
||||
|
||||
describe('With config disabled', () => {
|
||||
it('should not have exported static 404 page', async () => {
|
||||
await fs.writeFile(nextConfig, `module.exports = { ${buildId} }`)
|
||||
await nextBuild(appDir)
|
||||
expect(await fs.exists(static404)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With config enabled', () => {
|
||||
beforeEach(() =>
|
||||
fs.writeFile(
|
||||
nextConfig,
|
||||
`module.exports = { ${buildId}, ${experimentalConfig} }`
|
||||
)
|
||||
)
|
||||
|
||||
it('should export 404 page without custom _error', async () => {
|
||||
await nextBuild(appDir)
|
||||
appPort = await findPort()
|
||||
app = await nextStart(appDir, appPort)
|
||||
const html = await renderViaHTTP(appPort, '/non-existent')
|
||||
await killApp(app)
|
||||
expect(html).toContain('This page could not be found')
|
||||
expect(await fs.exists(static404)).toBe(true)
|
||||
})
|
||||
|
||||
it('should export 404 page without custom _error (serverless)', async () => {
|
||||
await fs.writeFile(
|
||||
nextConfig,
|
||||
`
|
||||
module.exports = {
|
||||
target: 'experimental-serverless-trace',
|
||||
experimental: { static404: true }
|
||||
}
|
||||
`
|
||||
)
|
||||
await nextBuild(appDir)
|
||||
appPort = await findPort()
|
||||
app = await nextStart(appDir, appPort)
|
||||
const html = await renderViaHTTP(appPort, '/non-existent')
|
||||
await killApp(app)
|
||||
expect(html).toContain('This page could not be found')
|
||||
expect(
|
||||
await fs.exists(join(appDir, '.next/serverless/pages/_errors/404.html'))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should not export 404 page with custom _error', async () => {
|
||||
await fs.writeFile(errorPage, `export { default } from 'next/error'`)
|
||||
await nextBuild(appDir)
|
||||
await fs.remove(errorPage)
|
||||
expect(await fs.exists(static404)).toBe(false)
|
||||
})
|
||||
|
||||
it('should not export 404 page with getInitialProps in _app', async () => {
|
||||
await fs.writeFile(
|
||||
appPage,
|
||||
`
|
||||
const Page = ({ Component, pageProps }) => {
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
Page.getInitialProps = () => ({ hello: 'world', pageProps: {} })
|
||||
export default Page
|
||||
`
|
||||
)
|
||||
await nextBuild(appDir)
|
||||
await fs.remove(appPage)
|
||||
expect(await fs.exists(static404)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue