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

Ability to Disable SSG Fallback #10701

Merged
merged 14 commits into from Feb 27, 2020
25 changes: 24 additions & 1 deletion errors/invalid-getstaticpaths-value.md
Expand Up @@ -11,7 +11,30 @@ Make sure to return the following shape from `unstable_getStaticPaths`:
```js
export async function unstable_getStaticPaths() {
return {
paths: Array<string | { params: { [key: string]: string } }>
paths: Array<string | { params: { [key: string]: string } }>,
fallback: boolean
}
}
```

There are two required properties:

1. `paths`: this property is an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of URLs ("paths") that should be statically generated at build-time. The returned paths must match the dynamic route shape.
- You may return a [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) or an [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) that explicitly defines all URL `params`.
```js
// pages/blog/[slug].js
export async function unstable_getStaticPaths() {
return {
paths: [
// String variant:
'/blog/first-post',
// Object variant:
{ params: { slug: 'second-post' } },
],
fallback: true,
}
}
```
1. `fallback`: this property is a [Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean), specifying whether or not a fallback version of this page should be generated.
- Enabling `fallback` (via `true`) allows you to return a subset of all the possible paths that should be statically generated. At runtime, Next.js will statically generate the remaining paths the **first time they are requested**. Consecutive calls to the path will be served as-if it was statically generated at build-time. This reduces build times when dealing with thousands or millions of pages.
- Disabling `fallback` (via `false`) requires you return the full collection of paths you would like to statically generate at build-time. At runtime, any path that was not generated at build-time **will 404**.
41 changes: 30 additions & 11 deletions packages/next/build/index.ts
Expand Up @@ -90,13 +90,13 @@ export type SsgRoute = {

export type DynamicSsgRoute = {
routeRegex: string
fallback: string
fallback: string | false
dataRoute: string
dataRouteRegex: string
}

export type PrerenderManifest = {
version: number
version: 2
routes: { [route: string]: SsgRoute }
dynamicRoutes: { [route: string]: DynamicSsgRoute }
preview: __ApiPreviewProps
Expand Down Expand Up @@ -432,6 +432,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
const buildManifestPath = path.join(distDir, BUILD_MANIFEST)

const ssgPages = new Set<string>()
const ssgFallbackPages = new Set<string>()
const staticPages = new Set<string>()
const invalidPages = new Set<string>()
const hybridAmpPages = new Set<string>()
Expand Down Expand Up @@ -478,6 +479,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
let isStatic = false
let isHybridAmp = false
let ssgPageRoutes: string[] | null = null
let hasSsgFallback: boolean = false

pagesManifest[page] = bundleRelative.replace(/\\/g, '/')

Expand Down Expand Up @@ -533,6 +535,10 @@ export default async function build(dir: string, conf = null): Promise<void> {
additionalSsgPaths.set(page, result.prerenderRoutes)
ssgPageRoutes = result.prerenderRoutes
}
if (result.prerenderFallback) {
hasSsgFallback = true
ssgFallbackPages.add(page)
}
} else if (result.hasServerProps) {
serverPropsPages.add(page)
} else if (result.isStatic && customAppGetInitialProps === false) {
Expand Down Expand Up @@ -564,6 +570,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
isSsg,
isHybridAmp,
ssgPageRoutes,
hasSsgFallback,
})
})
)
Expand Down Expand Up @@ -669,9 +676,15 @@ export default async function build(dir: string, conf = null): Promise<void> {
if (isDynamicRoute(page)) {
tbdPrerenderRoutes.push(page)

// Override the rendering for the dynamic page to be treated as a
// fallback render.
defaultMap[page] = { page, query: { __nextFallback: true } }
if (ssgFallbackPages.has(page)) {
// Override the rendering for the dynamic page to be treated as a
// fallback render.
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
Expand Down Expand Up @@ -736,12 +749,16 @@ export default async function build(dir: string, conf = null): Promise<void> {

for (const page of combinedPages) {
const isSsg = ssgPages.has(page)
const isSsgFallback = ssgFallbackPages.has(page)
const isDynamic = isDynamicRoute(page)
const hasAmp = hybridAmpPages.has(page)
let file = normalizePagePath(page)

// We should always have an HTML file to move for each page
await moveExportedPage(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.
if (!(isSsg && isDynamic && !isSsgFallback)) {
await moveExportedPage(page, file, isSsg, 'html')
}

if (hasAmp) {
await moveExportedPage(`${page}.amp`, `${file}.amp`, isSsg, 'html')
Expand All @@ -760,7 +777,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
}
} else {
// For a dynamic SSG page, we did not copy its data exports and only
// copy the fallback HTML file.
// copy the fallback HTML file (if present).
// We must also copy specific versions of this page as defined by
// `unstable_getStaticPaths` (additionalSsgPaths).
const extraRoutes = additionalSsgPaths.get(page) || []
Expand Down Expand Up @@ -814,14 +831,16 @@ export default async function build(dir: string, conf = null): Promise<void> {
finalDynamicRoutes[tbdRoute] = {
routeRegex: getRouteRegex(tbdRoute).re.source,
dataRoute,
fallback: `${normalizedRoute}.html`,
fallback: ssgFallbackPages.has(tbdRoute)
? `${normalizedRoute}.html`
: false,
dataRouteRegex: getRouteRegex(
dataRoute.replace(/\.json$/, '')
).re.source.replace(/\(\?:\\\/\)\?\$$/, '\\.json$'),
}
})
const prerenderManifest: PrerenderManifest = {
version: 1,
version: 2,
routes: finalPrerenderRoutes,
dynamicRoutes: finalDynamicRoutes,
preview: previewProps,
Expand All @@ -834,7 +853,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
)
} else {
const prerenderManifest: PrerenderManifest = {
version: 1,
version: 2,
routes: {},
dynamicRoutes: {},
preview: previewProps,
Expand Down
27 changes: 19 additions & 8 deletions packages/next/build/utils.ts
Expand Up @@ -42,6 +42,7 @@ export interface PageInfo {
static: boolean
isSsg: boolean
ssgPageRoutes: string[] | null
hasSsgFallback: boolean
serverBundle: string
}

Expand Down Expand Up @@ -500,7 +501,7 @@ export async function getPageSizeInKb(
export async function buildStaticPaths(
page: string,
unstable_getStaticPaths: Unstable_getStaticPaths
): Promise<Array<string>> {
): Promise<{ paths: string[]; fallback: boolean }> {
const prerenderPaths = new Set<string>()
const _routeRegex = getRouteRegex(page)
const _routeMatcher = getRouteMatcher(_routeRegex)
Expand All @@ -511,7 +512,7 @@ export async function buildStaticPaths(
const staticPathsResult = await unstable_getStaticPaths()

const expectedReturnVal =
`Expected: { paths: [] }\n` +
`Expected: { paths: [], fallback: boolean }\n` +
`See here for more info: https://err.sh/zeit/next.js/invalid-getstaticpaths-value`
Timer marked this conversation as resolved.
Show resolved Hide resolved

if (
Expand All @@ -525,7 +526,7 @@ export async function buildStaticPaths(
}

const invalidStaticPathKeys = Object.keys(staticPathsResult).filter(
key => key !== 'paths'
key => !(key === 'paths' || key === 'fallback')
)

if (invalidStaticPathKeys.length > 0) {
Expand All @@ -536,6 +537,13 @@ export async function buildStaticPaths(
)
}

if (typeof staticPathsResult.fallback !== 'boolean') {
throw new Error(
`The \`fallback\` key must be returned from unstable_getStaticProps in ${page}.\n` +
Timer marked this conversation as resolved.
Show resolved Hide resolved
expectedReturnVal
)
}

const toPrerender = staticPathsResult.paths

if (!Array.isArray(toPrerender)) {
Expand Down Expand Up @@ -601,7 +609,7 @@ export async function buildStaticPaths(
}
})

return [...prerenderPaths]
return { paths: [...prerenderPaths], fallback: staticPathsResult.fallback }
}

export async function isPageStatic(
Expand All @@ -614,6 +622,7 @@ export async function isPageStatic(
hasServerProps?: boolean
hasStaticProps?: boolean
prerenderRoutes?: string[] | undefined
prerenderFallback?: boolean | undefined
}> {
try {
require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
Expand Down Expand Up @@ -667,18 +676,20 @@ export async function isPageStatic(
}

let prerenderRoutes: Array<string> | undefined
let prerenderFallback: boolean | undefined
if (hasStaticProps && hasStaticPaths) {
prerenderRoutes = await buildStaticPaths(
page,
mod.unstable_getStaticPaths
)
;({
paths: prerenderRoutes,
fallback: prerenderFallback,
} = await buildStaticPaths(page, mod.unstable_getStaticPaths))
}

const config = mod.config || {}
return {
isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps,
isHybridAmp: config.amp === 'hybrid',
prerenderRoutes,
prerenderFallback,
hasStaticProps,
hasServerProps,
}
Expand Down
2 changes: 1 addition & 1 deletion packages/next/export/worker.js
Expand Up @@ -264,7 +264,7 @@ export default async function({
return results
} catch (error) {
console.error(
`\nError occurred prerendering page "${path}" https://err.sh/next.js/prerender-error:\n` +
`\nError occurred prerendering page "${path}". Read more: https://err.sh/next.js/prerender-error:\n` +
error
)
return { ...results, error: true }
Expand Down
1 change: 1 addition & 0 deletions packages/next/next-server/server/load-components.ts
Expand Up @@ -52,6 +52,7 @@ type Unstable_getStaticProps = (ctx: {

export type Unstable_getStaticPaths = () => Promise<{
paths: Array<string | { params: ParsedUrlQuery }>
fallback: boolean
}>

type Unstable_getServerProps = (context: {
Expand Down