Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial support for static 404 page #10113

Merged
merged 8 commits into from Jan 20, 2020
19 changes: 18 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 @@ -521,6 +524,11 @@ export default async function build(dir: string, conf = null): Promise<void> {
)
staticCheckWorkers.end()

const useStatic404 =
!customAppGetInitialProps &&
!hasCustomErrorPage &&
config.experimental.static404

if (invalidPages.size > 0) {
throw new Error(
`Build optimization failed: found page${
Expand Down Expand Up @@ -551,7 +559,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 @@ -587,6 +595,11 @@ export default async function build(dir: string, conf = null): Promise<void> {
defaultMap[route] = { page }
})
})

if (useStatic404) {
defaultMap['/_errors/404'] = { page: '/_error' }
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
}

return defaultMap
},
exportTrailingSlash: false,
Expand Down Expand Up @@ -627,6 +640,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)
})
})
})