Skip to content

Commit

Permalink
Add initial support for static 404 page (#10113)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
ijjk and timneutkens committed Jan 20, 2020
1 parent 71c2354 commit e04e5a5
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 2 deletions.
21 changes: 20 additions & 1 deletion packages/next/build/index.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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${
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/next/next-server/server/config.ts
Expand Up @@ -51,6 +51,7 @@ const defaultConfig: { [key: string]: any } = {
reactMode: 'legacy',
workerThreads: false,
basePath: '',
static404: false,
},
future: {
excludeDefaultMomentLocales: false,
Expand Down
18 changes: 17 additions & 1 deletion packages/next/next-server/server/next-server.ts
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions test/integration/static-404/pages/index.js
@@ -0,0 +1 @@
export default () => 'hi'
105 changes: 105 additions & 0 deletions test/integration/static-404/test/index.test.js
@@ -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)
})
})
})

0 comments on commit e04e5a5

Please sign in to comment.