diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts
index 7bf978f869691..a0dc62462b8ac 100644
--- a/packages/next/build/index.ts
+++ b/packages/next/build/index.ts
@@ -11,7 +11,7 @@ import { pathToRegexp } from 'next/dist/compiled/path-to-regexp'
import path from 'path'
import formatWebpackMessages from '../client/dev/error-overlay/format-webpack-messages'
import {
- PAGES_404_GET_INITIAL_PROPS_ERROR,
+ STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR,
PUBLIC_DIR_MIDDLEWARE_CONFLICT,
} from '../lib/constants'
import { fileExists } from '../lib/file-exists'
@@ -41,6 +41,7 @@ import {
SERVERLESS_DIRECTORY,
SERVER_DIRECTORY,
SERVER_FILES_MANIFEST,
+ STATIC_STATUS_PAGES,
} from '../next-server/lib/constants'
import {
getRouteRegex,
@@ -742,7 +743,9 @@ export default async function build(
!workerResult.isStatic &&
!workerResult.hasStaticProps
) {
- throw new Error(PAGES_404_GET_INITIAL_PROPS_ERROR)
+ throw new Error(
+ `\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
+ )
}
// we need to ensure the 404 lambda is present since we use
// it when _app has getInitialProps
@@ -753,6 +756,16 @@ export default async function build(
staticPages.delete(page)
}
}
+
+ if (
+ STATIC_STATUS_PAGES.includes(page) &&
+ !workerResult.isStatic &&
+ !workerResult.hasStaticProps
+ ) {
+ throw new Error(
+ `\`pages${page}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
+ )
+ }
} catch (err) {
if (err.message !== 'INVALID_DEFAULT_EXPORT') throw err
invalidPages.add(page)
@@ -891,400 +904,412 @@ export default async function build(
const { i18n } = config
+ const usedStaticStatusPages = STATIC_STATUS_PAGES.filter(
+ (page) =>
+ mappedPages[page] && mappedPages[page].startsWith('private-next-pages')
+ )
+ usedStaticStatusPages.forEach((page) => {
+ if (!ssgPages.has(page)) {
+ staticPages.add(page)
+ }
+ })
+
+ const hasPages500 = usedStaticStatusPages.includes('/500')
+
await traceAsyncFn(tracer.startSpan('static-generation'), async () => {
- if (staticPages.size > 0 || ssgPages.size > 0 || useStatic404) {
- const combinedPages = [...staticPages, ...ssgPages]
-
- detectConflictingPaths(
- [
- ...combinedPages,
- ...pageKeys.filter((page) => !combinedPages.includes(page)),
- ],
- ssgPages,
- additionalSsgPaths
- )
- const exportApp = require('../export').default
- const exportOptions = {
- silent: false,
- buildExport: true,
- threads: config.experimental.cpus,
- pages: combinedPages,
- outdir: path.join(distDir, 'export'),
- statusMessage: 'Generating static pages',
- }
- const exportConfig: any = {
- ...config,
- initialPageRevalidationMap: {},
- ssgNotFoundPaths: [] as string[],
- // Default map will be the collection of automatic statically exported
- // pages and incremental pages.
- // n.b. we cannot handle this above in combinedPages because the dynamic
- // page must be in the `pages` array, but not in the mapping.
- exportPathMap: (defaultMap: any) => {
- // Dynamically routed pages should be prerendered to be used as
- // a client-side skeleton (fallback) while data is being fetched.
- // This ensures the end-user never sees a 500 or slow response from the
- // server.
- //
- // Note: prerendering disables automatic static optimization.
- ssgPages.forEach((page) => {
- if (isDynamicRoute(page)) {
- tbdPrerenderRoutes.push(page)
-
- if (ssgStaticFallbackPages.has(page)) {
- // Override the rendering for the dynamic page to be treated as a
- // fallback render.
- if (i18n) {
- defaultMap[`/${i18n.defaultLocale}${page}`] = {
- page,
- query: { __nextFallback: true },
- }
- } else {
- defaultMap[page] = { page, query: { __nextFallback: true } }
+ const combinedPages = [...staticPages, ...ssgPages]
+
+ detectConflictingPaths(
+ [
+ ...combinedPages,
+ ...pageKeys.filter((page) => !combinedPages.includes(page)),
+ ],
+ ssgPages,
+ additionalSsgPaths
+ )
+ const exportApp = require('../export').default
+ const exportOptions = {
+ silent: false,
+ buildExport: true,
+ threads: config.experimental.cpus,
+ pages: combinedPages,
+ outdir: path.join(distDir, 'export'),
+ statusMessage: 'Generating static pages',
+ }
+ const exportConfig: any = {
+ ...config,
+ initialPageRevalidationMap: {},
+ ssgNotFoundPaths: [] as string[],
+ // Default map will be the collection of automatic statically exported
+ // pages and incremental pages.
+ // n.b. we cannot handle this above in combinedPages because the dynamic
+ // page must be in the `pages` array, but not in the mapping.
+ exportPathMap: (defaultMap: any) => {
+ // Dynamically routed pages should be prerendered to be used as
+ // a client-side skeleton (fallback) while data is being fetched.
+ // This ensures the end-user never sees a 500 or slow response from the
+ // server.
+ //
+ // Note: prerendering disables automatic static optimization.
+ ssgPages.forEach((page) => {
+ if (isDynamicRoute(page)) {
+ tbdPrerenderRoutes.push(page)
+
+ if (ssgStaticFallbackPages.has(page)) {
+ // Override the rendering for the dynamic page to be treated as a
+ // fallback render.
+ if (i18n) {
+ defaultMap[`/${i18n.defaultLocale}${page}`] = {
+ page,
+ query: { __nextFallback: true },
}
} else {
- // Remove dynamically routed pages from the default path map when
- // fallback behavior is disabled.
- delete defaultMap[page]
+ defaultMap[page] = { page, query: { __nextFallback: true } }
}
+ } else {
+ // Remove dynamically routed pages from the default path map when
+ // fallback behavior is disabled.
+ delete defaultMap[page]
+ }
+ }
+ })
+ // Append the "well-known" routes we should prerender for, e.g. blog
+ // post slugs.
+ additionalSsgPaths.forEach((routes, page) => {
+ const encodedRoutes = additionalSsgPathsEncoded.get(page)
+
+ routes.forEach((route, routeIdx) => {
+ defaultMap[route] = {
+ page,
+ query: { __nextSsgPath: encodedRoutes?.[routeIdx] },
}
})
- // Append the "well-known" routes we should prerender for, e.g. blog
- // post slugs.
- additionalSsgPaths.forEach((routes, page) => {
- const encodedRoutes = additionalSsgPathsEncoded.get(page)
-
- routes.forEach((route, routeIdx) => {
- defaultMap[route] = {
- page,
- query: { __nextSsgPath: encodedRoutes?.[routeIdx] },
- }
- })
- })
+ })
- if (useStatic404) {
- defaultMap['/404'] = {
- page: hasPages404 ? '/404' : '/_error',
- }
+ if (useStatic404) {
+ defaultMap['/404'] = {
+ page: hasPages404 ? '/404' : '/_error',
}
+ }
- if (i18n) {
- for (const page of [
- ...staticPages,
- ...ssgPages,
- ...(useStatic404 ? ['/404'] : []),
- ]) {
- const isSsg = ssgPages.has(page)
- const isDynamic = isDynamicRoute(page)
- const isFallback = isSsg && ssgStaticFallbackPages.has(page)
-
- for (const locale of i18n.locales) {
- // skip fallback generation for SSG pages without fallback mode
- if (isSsg && isDynamic && !isFallback) continue
- const outputPath = `/${locale}${page === '/' ? '' : page}`
-
- defaultMap[outputPath] = {
- page: defaultMap[page]?.page || page,
- query: { __nextLocale: locale },
- }
+ // ensure 500.html is always generated even if pages/500.html
+ // doesn't exist
+ if (!hasPages500) {
+ defaultMap['/500'] = {
+ page: '/_error',
+ }
+ }
- if (isFallback) {
- defaultMap[outputPath].query.__nextFallback = true
- }
+ if (i18n) {
+ for (const page of [
+ ...staticPages,
+ ...ssgPages,
+ ...(useStatic404 ? ['/404'] : []),
+ ...(!hasPages500 ? ['/500'] : []),
+ ]) {
+ const isSsg = ssgPages.has(page)
+ const isDynamic = isDynamicRoute(page)
+ const isFallback = isSsg && ssgStaticFallbackPages.has(page)
+
+ for (const locale of i18n.locales) {
+ // skip fallback generation for SSG pages without fallback mode
+ if (isSsg && isDynamic && !isFallback) continue
+ const outputPath = `/${locale}${page === '/' ? '' : page}`
+
+ defaultMap[outputPath] = {
+ page: defaultMap[page]?.page || page,
+ query: { __nextLocale: locale },
}
- if (isSsg) {
- // remove non-locale prefixed variant from defaultMap
- delete defaultMap[page]
+ if (isFallback) {
+ defaultMap[outputPath].query.__nextFallback = true
}
}
- }
- return defaultMap
- },
- trailingSlash: false,
- }
-
- await exportApp(dir, exportOptions, exportConfig)
+ if (isSsg) {
+ // remove non-locale prefixed variant from defaultMap
+ delete defaultMap[page]
+ }
+ }
+ }
+ return defaultMap
+ },
+ trailingSlash: false,
+ }
- const postBuildSpinner = createSpinner({
- prefixText: `${Log.prefixes.info} Finalizing page optimization`,
- })
- ssgNotFoundPaths = exportConfig.ssgNotFoundPaths
+ await exportApp(dir, exportOptions, exportConfig)
- // remove server bundles that were exported
- for (const page of staticPages) {
- const serverBundle = getPagePath(page, distDir, isLikeServerless)
- await promises.unlink(serverBundle)
- }
- const serverOutputDir = path.join(
- distDir,
- isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
- )
+ const postBuildSpinner = createSpinner({
+ prefixText: `${Log.prefixes.info} Finalizing page optimization`,
+ })
+ ssgNotFoundPaths = exportConfig.ssgNotFoundPaths
- const moveExportedPage = async (
- originPage: string,
- page: string,
- file: string,
- isSsg: boolean,
- ext: 'html' | 'json',
- additionalSsgFile = false
- ) => {
- return traceAsyncFn(
- tracer.startSpan('move-exported-page'),
- async () => {
- file = `${file}.${ext}`
- const orig = path.join(exportOptions.outdir, file)
- const pagePath = getPagePath(
- originPage,
- distDir,
- isLikeServerless
- )
+ // remove server bundles that were exported
+ for (const page of staticPages) {
+ const serverBundle = getPagePath(page, distDir, isLikeServerless)
+ await promises.unlink(serverBundle)
+ }
+ const serverOutputDir = path.join(
+ distDir,
+ isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
+ )
- const relativeDest = path
- .relative(
- serverOutputDir,
+ const moveExportedPage = async (
+ originPage: string,
+ page: string,
+ file: string,
+ isSsg: boolean,
+ ext: 'html' | 'json',
+ additionalSsgFile = false
+ ) => {
+ return traceAsyncFn(
+ tracer.startSpan('move-exported-page'),
+ async () => {
+ file = `${file}.${ext}`
+ const orig = path.join(exportOptions.outdir, file)
+ const pagePath = getPagePath(originPage, distDir, isLikeServerless)
+
+ const relativeDest = path
+ .relative(
+ serverOutputDir,
+ path.join(
path.join(
- path.join(
- pagePath,
- // strip leading / and then recurse number of nested dirs
- // to place from base folder
- originPage
- .substr(1)
- .split('/')
- .map(() => '..')
- .join('/')
- ),
- file
- )
+ pagePath,
+ // strip leading / and then recurse number of nested dirs
+ // to place from base folder
+ originPage
+ .substr(1)
+ .split('/')
+ .map(() => '..')
+ .join('/')
+ ),
+ file
)
- .replace(/\\/g, '/')
-
- const dest = path.join(
- distDir,
- isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
- relativeDest
)
+ .replace(/\\/g, '/')
- if (!isSsg) {
- pagesManifest[page] = relativeDest
- }
+ const dest = path.join(
+ distDir,
+ isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
+ relativeDest
+ )
- const isNotFound = ssgNotFoundPaths.includes(page)
-
- // for SSG files with i18n the non-prerendered variants are
- // output with the locale prefixed so don't attempt moving
- // without the prefix
- if ((!i18n || additionalSsgFile) && !isNotFound) {
- await promises.mkdir(path.dirname(dest), { recursive: true })
- await promises.rename(orig, dest)
- } else if (i18n && !isSsg) {
- // this will be updated with the locale prefixed variant
- // since all files are output with the locale prefix
- delete pagesManifest[page]
- }
+ if (
+ !isSsg &&
+ !(
+ // don't add static status page to manifest if it's
+ // the default generated version e.g. no pages/500
+ (
+ STATIC_STATUS_PAGES.includes(page) &&
+ !usedStaticStatusPages.includes(page)
+ )
+ )
+ ) {
+ pagesManifest[page] = relativeDest
+ }
- if (i18n) {
- if (additionalSsgFile) return
+ const isNotFound = ssgNotFoundPaths.includes(page)
+
+ // for SSG files with i18n the non-prerendered variants are
+ // output with the locale prefixed so don't attempt moving
+ // without the prefix
+ if ((!i18n || additionalSsgFile) && !isNotFound) {
+ await promises.mkdir(path.dirname(dest), { recursive: true })
+ await promises.rename(orig, dest)
+ } else if (i18n && !isSsg) {
+ // this will be updated with the locale prefixed variant
+ // since all files are output with the locale prefix
+ delete pagesManifest[page]
+ }
- for (const locale of i18n.locales) {
- const curPath = `/${locale}${page === '/' ? '' : page}`
- const localeExt = page === '/' ? path.extname(file) : ''
- const relativeDestNoPages = relativeDest.substr(
- 'pages/'.length
- )
+ if (i18n) {
+ if (additionalSsgFile) return
- if (isSsg && ssgNotFoundPaths.includes(curPath)) {
- continue
- }
+ for (const locale of i18n.locales) {
+ const curPath = `/${locale}${page === '/' ? '' : page}`
+ const localeExt = page === '/' ? path.extname(file) : ''
+ const relativeDestNoPages = relativeDest.substr('pages/'.length)
- const updatedRelativeDest = path
- .join(
- 'pages',
- locale + localeExt,
- // if it's the top-most index page we want it to be locale.EXT
- // instead of locale/index.html
- page === '/' ? '' : relativeDestNoPages
- )
- .replace(/\\/g, '/')
+ if (isSsg && ssgNotFoundPaths.includes(curPath)) {
+ continue
+ }
- const updatedOrig = path.join(
- exportOptions.outdir,
+ const updatedRelativeDest = path
+ .join(
+ 'pages',
locale + localeExt,
- page === '/' ? '' : file
- )
- const updatedDest = path.join(
- distDir,
- isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
- updatedRelativeDest
+ // if it's the top-most index page we want it to be locale.EXT
+ // instead of locale/index.html
+ page === '/' ? '' : relativeDestNoPages
)
+ .replace(/\\/g, '/')
- if (!isSsg) {
- pagesManifest[curPath] = updatedRelativeDest
- }
- await promises.mkdir(path.dirname(updatedDest), {
- recursive: true,
- })
- await promises.rename(updatedOrig, updatedDest)
+ const updatedOrig = path.join(
+ exportOptions.outdir,
+ locale + localeExt,
+ page === '/' ? '' : file
+ )
+ const updatedDest = path.join(
+ distDir,
+ isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
+ updatedRelativeDest
+ )
+
+ if (!isSsg) {
+ pagesManifest[curPath] = updatedRelativeDest
}
+ await promises.mkdir(path.dirname(updatedDest), {
+ recursive: true,
+ })
+ await promises.rename(updatedOrig, updatedDest)
}
}
- )
- }
+ }
+ )
+ }
- // Only move /404 to /404 when there is no custom 404 as in that case we don't know about the 404 page
- if (!hasPages404 && useStatic404) {
- await moveExportedPage('/_error', '/404', '/404', false, 'html')
- }
+ // Only move /404 to /404 when there is no custom 404 as in that case we don't know about the 404 page
+ if (!hasPages404 && useStatic404) {
+ await moveExportedPage('/_error', '/404', '/404', false, 'html')
+ }
- for (const page of combinedPages) {
- const isSsg = ssgPages.has(page)
- const isStaticSsgFallback = ssgStaticFallbackPages.has(page)
- const isDynamic = isDynamicRoute(page)
- const hasAmp = hybridAmpPages.has(page)
- const file = normalizePagePath(page)
+ if (!hasPages500) {
+ await moveExportedPage('/_error', '/500', '/500', false, 'html')
+ }
- // The dynamic version of SSG pages are only prerendered if the
- // fallback is enabled. Below, we handle the specific prerenders
- // of these.
- const hasHtmlOutput = !(isSsg && isDynamic && !isStaticSsgFallback)
+ for (const page of combinedPages) {
+ const isSsg = ssgPages.has(page)
+ const isStaticSsgFallback = ssgStaticFallbackPages.has(page)
+ const isDynamic = isDynamicRoute(page)
+ const hasAmp = hybridAmpPages.has(page)
+ const file = normalizePagePath(page)
- if (hasHtmlOutput) {
- await moveExportedPage(page, page, file, isSsg, 'html')
- }
+ // The dynamic version of SSG pages are only prerendered if the
+ // fallback is enabled. Below, we handle the specific prerenders
+ // of these.
+ const hasHtmlOutput = !(isSsg && isDynamic && !isStaticSsgFallback)
- if (hasAmp && (!isSsg || (isSsg && !isDynamic))) {
- const ampPage = `${file}.amp`
- await moveExportedPage(page, ampPage, ampPage, isSsg, 'html')
+ if (hasHtmlOutput) {
+ await moveExportedPage(page, page, file, isSsg, 'html')
+ }
- if (isSsg) {
- await moveExportedPage(page, ampPage, ampPage, isSsg, 'json')
- }
- }
+ if (hasAmp && (!isSsg || (isSsg && !isDynamic))) {
+ const ampPage = `${file}.amp`
+ await moveExportedPage(page, ampPage, ampPage, isSsg, 'html')
if (isSsg) {
- // For a non-dynamic SSG page, we must copy its data file
- // from export, we already moved the HTML file above
- if (!isDynamic) {
- await moveExportedPage(page, page, file, isSsg, 'json')
-
- if (i18n) {
- // TODO: do we want to show all locale variants in build output
- for (const locale of i18n.locales) {
- const localePage = `/${locale}${page === '/' ? '' : page}`
-
- if (!ssgNotFoundPaths.includes(localePage)) {
- finalPrerenderRoutes[localePage] = {
- initialRevalidateSeconds:
- exportConfig.initialPageRevalidationMap[localePage],
- srcRoute: null,
- dataRoute: path.posix.join(
- '/_next/data',
- buildId,
- `${file}.json`
- ),
- }
+ await moveExportedPage(page, ampPage, ampPage, isSsg, 'json')
+ }
+ }
+
+ if (isSsg) {
+ // For a non-dynamic SSG page, we must copy its data file
+ // from export, we already moved the HTML file above
+ if (!isDynamic) {
+ await moveExportedPage(page, page, file, isSsg, 'json')
+
+ if (i18n) {
+ // TODO: do we want to show all locale variants in build output
+ for (const locale of i18n.locales) {
+ const localePage = `/${locale}${page === '/' ? '' : page}`
+
+ if (!ssgNotFoundPaths.includes(localePage)) {
+ finalPrerenderRoutes[localePage] = {
+ initialRevalidateSeconds:
+ exportConfig.initialPageRevalidationMap[localePage],
+ srcRoute: null,
+ dataRoute: path.posix.join(
+ '/_next/data',
+ buildId,
+ `${file}.json`
+ ),
}
}
- } else {
- finalPrerenderRoutes[page] = {
- initialRevalidateSeconds:
- exportConfig.initialPageRevalidationMap[page],
- srcRoute: null,
- dataRoute: path.posix.join(
- '/_next/data',
- buildId,
- `${file}.json`
- ),
- }
- }
- // Set Page Revalidation Interval
- const pageInfo = pageInfos.get(page)
- if (pageInfo) {
- pageInfo.initialRevalidateSeconds =
- exportConfig.initialPageRevalidationMap[page]
- pageInfos.set(page, pageInfo)
}
} else {
- // For a dynamic SSG page, we did not copy its data exports and only
- // copy the fallback HTML file (if present).
- // We must also copy specific versions of this page as defined by
- // `getStaticPaths` (additionalSsgPaths).
- const extraRoutes = additionalSsgPaths.get(page) || []
- for (const route of extraRoutes) {
- const pageFile = normalizePagePath(route)
+ finalPrerenderRoutes[page] = {
+ initialRevalidateSeconds:
+ exportConfig.initialPageRevalidationMap[page],
+ srcRoute: null,
+ dataRoute: path.posix.join(
+ '/_next/data',
+ buildId,
+ `${file}.json`
+ ),
+ }
+ }
+ // Set Page Revalidation Interval
+ const pageInfo = pageInfos.get(page)
+ if (pageInfo) {
+ pageInfo.initialRevalidateSeconds =
+ exportConfig.initialPageRevalidationMap[page]
+ pageInfos.set(page, pageInfo)
+ }
+ } else {
+ // For a dynamic SSG page, we did not copy its data exports and only
+ // copy the fallback HTML file (if present).
+ // We must also copy specific versions of this page as defined by
+ // `getStaticPaths` (additionalSsgPaths).
+ const extraRoutes = additionalSsgPaths.get(page) || []
+ for (const route of extraRoutes) {
+ const pageFile = normalizePagePath(route)
+ await moveExportedPage(page, route, pageFile, isSsg, 'html', true)
+ await moveExportedPage(page, route, pageFile, isSsg, 'json', true)
+
+ if (hasAmp) {
+ const ampPage = `${pageFile}.amp`
await moveExportedPage(
page,
- route,
- pageFile,
+ ampPage,
+ ampPage,
isSsg,
'html',
true
)
await moveExportedPage(
page,
- route,
- pageFile,
+ ampPage,
+ ampPage,
isSsg,
'json',
true
)
+ }
- if (hasAmp) {
- const ampPage = `${pageFile}.amp`
- await moveExportedPage(
- page,
- ampPage,
- ampPage,
- isSsg,
- 'html',
- true
- )
- await moveExportedPage(
- page,
- ampPage,
- ampPage,
- isSsg,
- 'json',
- true
- )
- }
-
- finalPrerenderRoutes[route] = {
- initialRevalidateSeconds:
- exportConfig.initialPageRevalidationMap[route],
- srcRoute: page,
- dataRoute: path.posix.join(
- '/_next/data',
- buildId,
- `${normalizePagePath(route)}.json`
- ),
- }
+ finalPrerenderRoutes[route] = {
+ initialRevalidateSeconds:
+ exportConfig.initialPageRevalidationMap[route],
+ srcRoute: page,
+ dataRoute: path.posix.join(
+ '/_next/data',
+ buildId,
+ `${normalizePagePath(route)}.json`
+ ),
+ }
- // Set route Revalidation Interval
- const pageInfo = pageInfos.get(route)
- if (pageInfo) {
- pageInfo.initialRevalidateSeconds =
- exportConfig.initialPageRevalidationMap[route]
- pageInfos.set(route, pageInfo)
- }
+ // Set route Revalidation Interval
+ const pageInfo = pageInfos.get(route)
+ if (pageInfo) {
+ pageInfo.initialRevalidateSeconds =
+ exportConfig.initialPageRevalidationMap[route]
+ pageInfos.set(route, pageInfo)
}
}
}
}
+ }
- // remove temporary export folder
- await recursiveDelete(exportOptions.outdir)
- await promises.rmdir(exportOptions.outdir)
- await promises.writeFile(
- manifestPath,
- JSON.stringify(pagesManifest, null, 2),
- 'utf8'
- )
+ // remove temporary export folder
+ await recursiveDelete(exportOptions.outdir)
+ await promises.rmdir(exportOptions.outdir)
+ await promises.writeFile(
+ manifestPath,
+ JSON.stringify(pagesManifest, null, 2),
+ 'utf8'
+ )
- if (postBuildSpinner) postBuildSpinner.stopAndPersist()
- console.log()
- }
+ if (postBuildSpinner) postBuildSpinner.stopAndPersist()
+ console.log()
})
const analysisEnd = process.hrtime(analysisBegin)
diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts
index 01e17ade79006..ca2eaa5734db9 100644
--- a/packages/next/export/worker.ts
+++ b/packages/next/export/worker.ts
@@ -189,6 +189,10 @@ export default async function exportPage({
...headerMocks,
} as unknown) as ServerResponse
+ if (path === '/500' && page === '/_error') {
+ res.statusCode = 500
+ }
+
envConfig.setConfig({
serverRuntimeConfig,
publicRuntimeConfig: renderOpts.runtimeConfig,
diff --git a/packages/next/lib/constants.ts b/packages/next/lib/constants.ts
index 9d202591121d7..878779267890f 100644
--- a/packages/next/lib/constants.ts
+++ b/packages/next/lib/constants.ts
@@ -30,7 +30,7 @@ export const SERVER_PROPS_GET_INIT_PROPS_CONFLICT = `You can not use getInitialP
export const SERVER_PROPS_SSG_CONFLICT = `You can not use getStaticProps or getStaticPaths with getServerSideProps. To use SSG, please remove getServerSideProps`
-export const PAGES_404_GET_INITIAL_PROPS_ERROR = `\`pages/404\` can not have getInitialProps/getServerSideProps, https://err.sh/next.js/404-get-initial-props`
+export const STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR = `can not have getInitialProps/getServerSideProps, https://err.sh/next.js/404-get-initial-props`
export const SERVER_PROPS_EXPORT_ERROR = `pages with \`getServerSideProps\` can not be exported. See more info here: https://err.sh/next.js/gssp-export`
diff --git a/packages/next/next-server/lib/constants.ts b/packages/next/next-server/lib/constants.ts
index 03a4fc100705b..c07b817533ce2 100644
--- a/packages/next/next-server/lib/constants.ts
+++ b/packages/next/next-server/lib/constants.ts
@@ -38,3 +38,4 @@ export const PERMANENT_REDIRECT_STATUS = 308
export const STATIC_PROPS_ID = '__N_SSG'
export const SERVER_PROPS_ID = '__N_SSP'
export const OPTIMIZED_FONT_PROVIDERS = ['https://fonts.googleapis.com/css']
+export const STATIC_STATUS_PAGES = ['/500']
diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts
index 970ab91d2510b..0b957150e62e2 100644
--- a/packages/next/next-server/server/next-server.ts
+++ b/packages/next/next-server/server/next-server.ts
@@ -32,6 +32,7 @@ import {
ROUTES_MANIFEST,
SERVERLESS_DIRECTORY,
SERVER_DIRECTORY,
+ STATIC_STATUS_PAGES,
TEMPORARY_REDIRECT_STATUS,
} from '../lib/constants'
import {
@@ -1364,6 +1365,12 @@ export default class Server {
res.statusCode = 404
}
+ // ensure correct status is set when visiting a status page
+ // directly e.g. /500
+ if (STATIC_STATUS_PAGES.includes(pathname)) {
+ res.statusCode = parseInt(pathname.substr(1), 10)
+ }
+
// handle static page
if (typeof components.Component === 'string') {
return components.Component
@@ -1908,9 +1915,15 @@ export default class Server {
result = await this.findPageComponents('/404', query)
using404Page = result !== null
}
+ let statusPage = `/${res.statusCode}`
+
+ if (!result && STATIC_STATUS_PAGES.includes(statusPage)) {
+ result = await this.findPageComponents(statusPage, query)
+ }
if (!result) {
result = await this.findPageComponents('/_error', query)
+ statusPage = '/_error'
}
if (
@@ -1928,7 +1941,7 @@ export default class Server {
html = await this.renderToHTMLWithComponents(
req,
res,
- using404Page ? '/404' : '/_error',
+ statusPage,
result!,
{
...this.renderOpts,
diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx
index c688e78e10e7c..8830769d9c418 100644
--- a/packages/next/next-server/server/render.tsx
+++ b/packages/next/next-server/server/render.tsx
@@ -8,7 +8,7 @@ import {
GSP_NO_RETURNED_VALUE,
GSSP_COMPONENT_MEMBER_ERROR,
GSSP_NO_RETURNED_VALUE,
- PAGES_404_GET_INITIAL_PROPS_ERROR,
+ STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR,
SERVER_PROPS_GET_INIT_PROPS_CONFLICT,
SERVER_PROPS_SSG_CONFLICT,
SSG_GET_INITIAL_PROPS_CONFLICT,
@@ -22,6 +22,7 @@ import {
AMP_RENDER_TARGET,
SERVER_PROPS_ID,
STATIC_PROPS_ID,
+ STATIC_STATUS_PAGES,
} from '../lib/constants'
import { defaultHead } from '../lib/head'
import { HeadManagerContext } from '../lib/head-manager-context'
@@ -536,7 +537,17 @@ export async function renderToHTML(
}
if (pathname === '/404' && (hasPageGetInitialProps || getServerSideProps)) {
- throw new Error(PAGES_404_GET_INITIAL_PROPS_ERROR)
+ throw new Error(
+ `\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
+ )
+ }
+ if (
+ STATIC_STATUS_PAGES.includes(pathname) &&
+ (hasPageGetInitialProps || getServerSideProps)
+ ) {
+ throw new Error(
+ `\`pages${pathname}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
+ )
}
}
if (isAutoExport) renderOpts.autoExport = true
diff --git a/packages/next/server/next-dev-server.ts b/packages/next/server/next-dev-server.ts
index 4ef111da02d47..b45bfc71a5ba3 100644
--- a/packages/next/server/next-dev-server.ts
+++ b/packages/next/server/next-dev-server.ts
@@ -20,6 +20,7 @@ import {
PHASE_DEVELOPMENT_SERVER,
CLIENT_STATIC_FILES_PATH,
DEV_CLIENT_PAGES_MANIFEST,
+ STATIC_STATUS_PAGES,
} from '../next-server/lib/constants'
import {
getRouteMatcher,
@@ -627,6 +628,11 @@ export default class DevServer extends Server {
await this.devReady
if (res.statusCode === 404 && (await this.hasPage('/404'))) {
await this.hotReloader!.ensurePage('/404')
+ } else if (
+ STATIC_STATUS_PAGES.includes(`/${res.statusCode}`) &&
+ (await this.hasPage(`/${res.statusCode}`))
+ ) {
+ await this.hotReloader!.ensurePage(`/${res.statusCode}`)
} else {
await this.hotReloader!.ensurePage('/_error')
}
diff --git a/test/integration/500-page/next.config.js b/test/integration/500-page/next.config.js
new file mode 100644
index 0000000000000..4ba52ba2c8df6
--- /dev/null
+++ b/test/integration/500-page/next.config.js
@@ -0,0 +1 @@
+module.exports = {}
diff --git a/test/integration/500-page/pages/500.js b/test/integration/500-page/pages/500.js
new file mode 100644
index 0000000000000..aef5a27d3360e
--- /dev/null
+++ b/test/integration/500-page/pages/500.js
@@ -0,0 +1,5 @@
+const page = () => {
+ console.log('rendered 500')
+ return 'custom 500 page'
+}
+export default page
diff --git a/test/integration/500-page/pages/err.js b/test/integration/500-page/pages/err.js
new file mode 100644
index 0000000000000..61c7136f7ba7e
--- /dev/null
+++ b/test/integration/500-page/pages/err.js
@@ -0,0 +1,5 @@
+const page = () => 'page with err'
+page.getInitialProps = () => {
+ throw new Error('oops')
+}
+export default page
diff --git a/test/integration/500-page/pages/index.js b/test/integration/500-page/pages/index.js
new file mode 100644
index 0000000000000..f6c15d1f66e8a
--- /dev/null
+++ b/test/integration/500-page/pages/index.js
@@ -0,0 +1 @@
+export default () => 'hello from index'
diff --git a/test/integration/500-page/test/index.test.js b/test/integration/500-page/test/index.test.js
new file mode 100644
index 0000000000000..7dc3984d0c280
--- /dev/null
+++ b/test/integration/500-page/test/index.test.js
@@ -0,0 +1,340 @@
+/* eslint-env jest */
+
+import fs from 'fs-extra'
+import { join } from 'path'
+import {
+ killApp,
+ findPort,
+ launchApp,
+ nextStart,
+ nextBuild,
+ renderViaHTTP,
+ fetchViaHTTP,
+ waitFor,
+ getPageFileFromPagesManifest,
+} from 'next-test-utils'
+
+jest.setTimeout(1000 * 60 * 2)
+
+const appDir = join(__dirname, '../')
+const pages500 = join(appDir, 'pages/500.js')
+const pagesApp = join(appDir, 'pages/_app.js')
+const pagesError = join(appDir, 'pages/_error.js')
+const nextConfig = join(appDir, 'next.config.js')
+const gip500Err = /`pages\/500` can not have getInitialProps\/getServerSideProps/
+
+let nextConfigContent
+let appPort
+let app
+
+const runTests = (mode = 'server') => {
+ it('should use pages/500', async () => {
+ const html = await renderViaHTTP(appPort, '/500')
+ expect(html).toContain('custom 500 page')
+ })
+
+ it('should set correct status code with pages/500', async () => {
+ const res = await fetchViaHTTP(appPort, '/500')
+ expect(res.status).toBe(500)
+ })
+
+ it('should not error when visited directly', async () => {
+ const res = await fetchViaHTTP(appPort, '/500')
+ expect(res.status).toBe(500)
+ expect(await res.text()).toContain('custom 500 page')
+ })
+
+ if (mode !== 'dev') {
+ it('should output 500.html during build', async () => {
+ const page = getPageFileFromPagesManifest(appDir, '/500')
+ expect(page.endsWith('.html')).toBe(true)
+ })
+
+ it('should add /500 to pages-manifest correctly', async () => {
+ const manifest = await fs.readJSON(
+ join(appDir, '.next', mode, 'pages-manifest.json')
+ )
+ expect('/500' in manifest).toBe(true)
+ })
+ }
+}
+
+describe('500 Page Support', () => {
+ describe('dev mode', () => {
+ beforeAll(async () => {
+ appPort = await findPort()
+ app = await launchApp(appDir, appPort)
+ })
+ afterAll(() => killApp(app))
+
+ runTests('dev')
+ })
+
+ describe('server mode', () => {
+ beforeAll(async () => {
+ await nextBuild(appDir)
+ appPort = await findPort()
+ app = await nextStart(appDir, appPort)
+ })
+ afterAll(() => killApp(app))
+
+ runTests('server')
+ })
+
+ describe('serverless mode', () => {
+ beforeAll(async () => {
+ nextConfigContent = await fs.readFile(nextConfig, 'utf8')
+ await fs.writeFile(
+ nextConfig,
+ `
+ module.exports = {
+ target: 'serverless'
+ }
+ `
+ )
+ await nextBuild(appDir)
+ appPort = await findPort()
+ app = await nextStart(appDir, appPort)
+ })
+ afterAll(async () => {
+ await fs.writeFile(nextConfig, nextConfigContent)
+ await killApp(app)
+ })
+
+ runTests('serverless')
+ })
+
+ it('still builds 500 statically with getInitialProps in _app', async () => {
+ await fs.writeFile(
+ pagesApp,
+ `
+ import App from 'next/app'
+
+ const page = ({ Component, pageProps }) =>
Error status: {statusCode}
+ } + Error.getInitialProps = ({ res, err }) => { + console.error('called _error.getInitialProps') + return { + statusCode: res && res.statusCode ? res.statusCode : err ? err.statusCode : 404 + } + } + export default Error + ` + ) + const { stderr: buildStderr, code } = await nextBuild(appDir, [], { + stderr: true, + }) + await fs.rename(`${pages500}.bak`, pages500) + await fs.remove(pagesError) + console.log(buildStderr) + expect(buildStderr).not.toMatch(gip500Err) + expect(code).toBe(0) + expect( + await fs.pathExists(join(appDir, '.next/server/pages/500.html')) + ).toBe(true) + + let appStderr = '' + const appPort = await findPort() + const app = await nextStart(appDir, appPort, { + onStderr(msg) { + appStderr += msg || '' + }, + }) + + await renderViaHTTP(appPort, '/err') + await killApp(app) + + expect(appStderr).toContain('called _error.getInitialProps') + }) + + it('shows error with getInitialProps in pages/500 build', async () => { + await fs.move(pages500, `${pages500}.bak`) + await fs.writeFile( + pages500, + ` + const page = () => 'custom 500 page' + page.getInitialProps = () => ({ a: 'b' }) + export default page + ` + ) + const { stderr, code } = await nextBuild(appDir, [], { stderr: true }) + await fs.remove(pages500) + await fs.move(`${pages500}.bak`, pages500) + + expect(stderr).toMatch(gip500Err) + expect(code).toBe(1) + }) + + it('shows error with getInitialProps in pages/500 dev', async () => { + await fs.move(pages500, `${pages500}.bak`) + await fs.writeFile( + pages500, + ` + const page = () => 'custom 500 page' + page.getInitialProps = () => ({ a: 'b' }) + export default page + ` + ) + + let stderr = '' + appPort = await findPort() + app = await launchApp(appDir, appPort, { + onStderr(msg) { + stderr += msg || '' + }, + }) + await renderViaHTTP(appPort, '/500') + await waitFor(1000) + + await killApp(app) + + await fs.remove(pages500) + await fs.move(`${pages500}.bak`, pages500) + + expect(stderr).toMatch(gip500Err) + }) + + it('does not show error with getStaticProps in pages/500 build', async () => { + await fs.move(pages500, `${pages500}.bak`) + await fs.writeFile( + pages500, + ` + const page = () => 'custom 500 page' + export const getStaticProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + const { stderr, code } = await nextBuild(appDir, [], { stderr: true }) + await fs.remove(pages500) + await fs.move(`${pages500}.bak`, pages500) + + expect(stderr).not.toMatch(gip500Err) + expect(code).toBe(0) + }) + + it('does not show error with getStaticProps in pages/500 dev', async () => { + await fs.move(pages500, `${pages500}.bak`) + await fs.writeFile( + pages500, + ` + const page = () => 'custom 500 page' + export const getStaticProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + + let stderr = '' + appPort = await findPort() + app = await launchApp(appDir, appPort, { + onStderr(msg) { + stderr += msg || '' + }, + }) + await renderViaHTTP(appPort, '/abc') + await waitFor(1000) + + await killApp(app) + + await fs.remove(pages500) + await fs.move(`${pages500}.bak`, pages500) + + expect(stderr).not.toMatch(gip500Err) + }) + + it('shows error with getServerSideProps in pages/500 build', async () => { + await fs.move(pages500, `${pages500}.bak`) + await fs.writeFile( + pages500, + ` + const page = () => 'custom 500 page' + export const getServerSideProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + const { stderr, code } = await nextBuild(appDir, [], { stderr: true }) + await fs.remove(pages500) + await fs.move(`${pages500}.bak`, pages500) + + expect(stderr).toMatch(gip500Err) + expect(code).toBe(1) + }) + + it('shows error with getServerSideProps in pages/500 dev', async () => { + await fs.move(pages500, `${pages500}.bak`) + await fs.writeFile( + pages500, + ` + const page = () => 'custom 500 page' + export const getServerSideProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + + let stderr = '' + appPort = await findPort() + app = await launchApp(appDir, appPort, { + onStderr(msg) { + stderr += msg || '' + }, + }) + await renderViaHTTP(appPort, '/500') + await waitFor(1000) + + await killApp(app) + + await fs.remove(pages500) + await fs.move(`${pages500}.bak`, pages500) + + expect(stderr).toMatch(gip500Err) + }) +}) diff --git a/test/integration/error-in-error/pages/_error.js b/test/integration/error-in-error/pages/_error.js index 4c8c07002213f..825ef429db067 100644 --- a/test/integration/error-in-error/pages/_error.js +++ b/test/integration/error-in-error/pages/_error.js @@ -2,7 +2,7 @@ import React from 'react' class Error extends React.Component { static async getInitialProps({ req, res, err }) { - if (req.url !== '/404.html') { + if (!req.url.startsWith('/404') && !req.url.startsWith('/500')) { await Promise.reject(new Error('an error in error')) } const statusCode = res ? res.statusCode : err ? err.statusCode : null diff --git a/test/integration/polyfills/test/index.test.js b/test/integration/polyfills/test/index.test.js index 1a0b62a3973df..cbcaf257c4ef4 100644 --- a/test/integration/polyfills/test/index.test.js +++ b/test/integration/polyfills/test/index.test.js @@ -39,9 +39,9 @@ describe('Polyfills', () => { }) it('should contain generated page count in output', async () => { - expect(output).toContain('Generating static pages (0/3)') - expect(output).toContain('Generating static pages (3/3)') + expect(output).toContain('Generating static pages (0/4)') + expect(output).toContain('Generating static pages (4/4)') // we should only have 1 segment and the initial message logged out - expect(output.match(/Generating static pages/g).length).toBe(2) + expect(output.match(/Generating static pages/g).length).toBe(5) }) }) diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index 186e478c3fbb4..90ca62b5e7299 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -58,8 +58,8 @@ describe('Production Usage', () => { afterAll(() => stopApp(server)) it('should contain generated page count in output', async () => { - expect(output).toContain('Generating static pages (0/36)') - expect(output).toContain('Generating static pages (36/36)') + expect(output).toContain('Generating static pages (0/37)') + expect(output).toContain('Generating static pages (37/37)') // we should only have 4 segments and the initial message logged out expect(output.match(/Generating static pages/g).length).toBe(5) })