Skip to content

Commit

Permalink
Ability to Disable SSG Fallback (#10701)
Browse files Browse the repository at this point in the history
* Ability to Disable SSG Fallback

* Throw error when value is missing

* Fix existing tests

* Adjust error message

* Do not render fallback at build time for `fallback: false` page

* Fix existing fallback behavior

* fix build

* fix version

* fix some tests

* Fix last test

* Add docs for get static paths

* Add explicit mode tests

* test for fallback error message
  • Loading branch information
Timer committed Feb 27, 2020
1 parent b3ffdab commit 47ff1eb
Show file tree
Hide file tree
Showing 20 changed files with 292 additions and 57 deletions.
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`

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` +
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

0 comments on commit 47ff1eb

Please sign in to comment.