Skip to content

Commit

Permalink
Update to generate auto static pages with all locales (#17730)
Browse files Browse the repository at this point in the history
Follow-up PR to #17370 this adds generating auto-export, non-dynamic SSG, and fallback pages with all locales. Dynamic SSG pages still control which locales the pages are generated with using `getStaticPaths`. To further control which locales non-dynamic SSG pages will be prerendered with a follow-up PR adding handling for 404 behavior from `getStaticProps` will be needed. 

x-ref: #17110
  • Loading branch information
ijjk authored Oct 9, 2020
1 parent 62b9183 commit 2170dfd
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 54 deletions.
109 changes: 101 additions & 8 deletions packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,8 @@ export default async function build(
// 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) => {
const { i18n } = config.experimental

// 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
Expand All @@ -738,7 +740,14 @@ export default async function build(
if (ssgStaticFallbackPages.has(page)) {
// Override the rendering for the dynamic page to be treated as a
// fallback render.
defaultMap[page] = { page, query: { __nextFallback: true } }
if (i18n) {
defaultMap[`/${i18n.defaultLocale}${page}`] = {
page,
query: { __nextFallback: true },
}
} else {
defaultMap[page] = { page, query: { __nextFallback: true } }
}
} else {
// Remove dynamically routed pages from the default path map when
// fallback behavior is disabled.
Expand All @@ -760,6 +769,39 @@ export default async function build(
}
}

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) {
if (!isSsg && locale === i18n.defaultLocale) continue
// 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,
query: { __nextLocale: locale },
}

if (isFallback) {
defaultMap[outputPath].query.__nextFallback = true
}
}

if (isSsg && !isFallback) {
// remove non-locale prefixed variant from defaultMap
delete defaultMap[page]
}
}
}

return defaultMap
},
trailingSlash: false,
Expand All @@ -786,7 +828,8 @@ export default async function build(
page: string,
file: string,
isSsg: boolean,
ext: 'html' | 'json'
ext: 'html' | 'json',
additionalSsgFile = false
) => {
file = `${file}.${ext}`
const orig = path.join(exportOptions.outdir, file)
Expand Down Expand Up @@ -820,8 +863,58 @@ export default async function build(
if (!isSsg) {
pagesManifest[page] = relativeDest
}
await promises.mkdir(path.dirname(dest), { recursive: true })
await promises.rename(orig, dest)

const { i18n } = config.experimental

// 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 || !isSsg || additionalSsgFile) {
await promises.mkdir(path.dirname(dest), { recursive: true })
await promises.rename(orig, dest)
}

if (i18n) {
if (additionalSsgFile) return

for (const locale of i18n.locales) {
// auto-export default locale files exist at root
// TODO: should these always be prefixed with locale
// similar to SSG prerender/fallback files?
if (!isSsg && locale === i18n.defaultLocale) {
continue
}

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
)
const updatedOrig = path.join(
exportOptions.outdir,
locale + localeExt,
page === '/' ? '' : file
)
const updatedDest = path.join(
distDir,
isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
updatedRelativeDest
)

if (!isSsg) {
pagesManifest[
`/${locale}${page === '/' ? '' : page}`
] = 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
Expand Down Expand Up @@ -877,13 +970,13 @@ export default async function build(
const extraRoutes = additionalSsgPaths.get(page) || []
for (const route of extraRoutes) {
const pageFile = normalizePagePath(route)
await moveExportedPage(page, route, pageFile, true, 'html')
await moveExportedPage(page, route, pageFile, true, 'json')
await moveExportedPage(page, route, pageFile, true, 'html', true)
await moveExportedPage(page, route, pageFile, true, 'json', true)

if (hasAmp) {
const ampPage = `${pageFile}.amp`
await moveExportedPage(page, ampPage, ampPage, true, 'html')
await moveExportedPage(page, ampPage, ampPage, true, 'json')
await moveExportedPage(page, ampPage, ampPage, true, 'html', true)
await moveExportedPage(page, ampPage, ampPage, true, 'json', true)
}

finalPrerenderRoutes[route] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ const nextServerlessLoader: loader.Loader = function () {
detectedLocale = detectedLocale || i18n.defaultLocale
if (
!fromExport &&
!nextStartMode &&
i18n.localeDetection !== false &&
(shouldAddLocalePrefix || shouldStripDefaultLocale)
Expand Down
26 changes: 17 additions & 9 deletions packages/next/export/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,21 @@ export default async function exportPage({
const { page } = pathMap
const filePath = normalizePagePath(path)
const ampPath = `${filePath}.amp`
const isDynamic = isDynamicRoute(page)
let query = { ...originalQuery }
let params: { [key: string]: string | string[] } | undefined

const localePathResult = normalizeLocalePath(path, renderOpts.locales)
let updatedPath = path
let locale = query.__nextLocale || renderOpts.locale
delete query.__nextLocale

if (localePathResult.detectedLocale) {
path = localePathResult.pathname
renderOpts.locale = localePathResult.detectedLocale
if (renderOpts.locale) {
const localePathResult = normalizeLocalePath(path, renderOpts.locales)

if (localePathResult.detectedLocale) {
updatedPath = localePathResult.pathname
locale = localePathResult.detectedLocale
}
}

// We need to show a warning if they try to provide query values
Expand All @@ -122,8 +129,8 @@ export default async function exportPage({
}

// Check if the page is a specified dynamic route
if (isDynamicRoute(page) && page !== path) {
params = getRouteMatcher(getRouteRegex(page))(path) || undefined
if (isDynamic && page !== path) {
params = getRouteMatcher(getRouteRegex(page))(updatedPath) || undefined
if (params) {
// we have to pass these separately for serverless
if (!serverless) {
Expand All @@ -134,7 +141,7 @@ export default async function exportPage({
}
} else {
throw new Error(
`The provided export path '${path}' doesn't match the '${page}' page.\nRead more: https://err.sh/vercel/next.js/export-path-mismatch`
`The provided export path '${updatedPath}' doesn't match the '${page}' page.\nRead more: https://err.sh/vercel/next.js/export-path-mismatch`
)
}
}
Expand All @@ -149,7 +156,7 @@ export default async function exportPage({
}

const req = ({
url: path,
url: updatedPath,
...headerMocks,
} as unknown) as IncomingMessage
const res = ({
Expand Down Expand Up @@ -239,7 +246,7 @@ export default async function exportPage({
fontManifest: optimizeFonts
? requireFontManifest(distDir, serverless)
: null,
locale: renderOpts.locale!,
locale: locale!,
locales: renderOpts.locales!,
},
// @ts-ignore
Expand Down Expand Up @@ -298,6 +305,7 @@ export default async function exportPage({
fontManifest: optimizeFonts
? requireFontManifest(distDir, serverless)
: null,
locale: locale as string,
}
// @ts-ignore
html = await renderMethod(req, res, page, query, curRenderOpts)
Expand Down
36 changes: 26 additions & 10 deletions packages/next/next-server/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export default class Server {
parsedUrl.pathname = localePathResult.pathname
}

;(req as any)._nextLocale = detectedLocale || i18n.defaultLocale
parsedUrl.query.__nextLocale = detectedLocale || i18n.defaultLocale
}

res.statusCode = 200
Expand Down Expand Up @@ -510,7 +510,7 @@ export default class Server {
pathname = localePathResult.pathname
detectedLocale = localePathResult.detectedLocale
}
;(req as any)._nextLocale = detectedLocale || i18n.defaultLocale
_parsedUrl.query.__nextLocale = detectedLocale || i18n.defaultLocale
}
pathname = getRouteFromAssetPath(pathname, '.json')

Expand Down Expand Up @@ -1015,11 +1015,21 @@ export default class Server {
query: ParsedUrlQuery = {},
params: Params | null = null
): Promise<FindComponentsResult | null> {
const paths = [
let paths = [
// try serving a static AMP version first
query.amp ? normalizePagePath(pathname) + '.amp' : null,
pathname,
].filter(Boolean)

if (query.__nextLocale) {
paths = [
...paths.map(
(path) => `/${query.__nextLocale}${path === '/' ? '' : path}`
),
...paths,
]
}

for (const pagePath of paths) {
try {
const components = await loadComponents(
Expand All @@ -1031,7 +1041,11 @@ export default class Server {
components,
query: {
...(components.getStaticProps
? { _nextDataReq: query._nextDataReq, amp: query.amp }
? {
amp: query.amp,
_nextDataReq: query._nextDataReq,
__nextLocale: query.__nextLocale,
}
: query),
...(params || {}),
},
Expand Down Expand Up @@ -1141,7 +1155,8 @@ export default class Server {
urlPathname = stripNextDataPath(urlPathname)
}

const locale = (req as any)._nextLocale
const locale = query.__nextLocale as string
delete query.__nextLocale

const ssgCacheKey =
isPreviewMode || !isSSG
Expand Down Expand Up @@ -1214,7 +1229,7 @@ export default class Server {
'passthrough',
{
fontManifest: this.renderOpts.fontManifest,
locale: (req as any)._nextLocale,
locale,
locales: this.renderOpts.locales,
}
)
Expand All @@ -1235,7 +1250,7 @@ export default class Server {
...opts,
isDataReq,
resolvedUrl,
locale: (req as any)._nextLocale,
locale,
// For getServerSideProps we need to ensure we use the original URL
// and not the resolved URL to prevent a hydration mismatch on
// asPath
Expand Down Expand Up @@ -1321,7 +1336,9 @@ export default class Server {

// Production already emitted the fallback as static HTML.
if (isProduction) {
html = await this.incrementalCache.getFallback(pathname)
html = await this.incrementalCache.getFallback(
locale ? `/${locale}${pathname}` : pathname
)
}
// We need to generate the fallback on-demand for development.
else {
Expand Down Expand Up @@ -1442,7 +1459,6 @@ export default class Server {
res.statusCode = 500
return await this.renderErrorToHTML(err, req, res, pathname, query)
}

res.statusCode = 404
return await this.renderErrorToHTML(null, req, res, pathname, query)
}
Expand Down Expand Up @@ -1488,7 +1504,7 @@ export default class Server {

// use static 404 page if available and is 404 response
if (is404) {
result = await this.findPageComponents('/404')
result = await this.findPageComponents('/404', query)
using404Page = result !== null
}

Expand Down
4 changes: 1 addition & 3 deletions packages/next/next-server/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ export async function renderToHTML(

const isFallback = !!query.__nextFallback
delete query.__nextFallback
delete query.__nextLocale

const isSSG = !!getStaticProps
const isBuildTimeSSG = isSSG && renderOpts.nextExport
Expand Down Expand Up @@ -506,9 +507,6 @@ export async function renderToHTML(
}
if (isAutoExport) renderOpts.autoExport = true
if (isSSG) renderOpts.nextExport = false
// don't set default locale for fallback pages since this needs to be
// handled at request time
if (isFallback) renderOpts.locale = undefined

await Loadable.preloadAll() // Make sure all dynamic imports are loaded

Expand Down
21 changes: 21 additions & 0 deletions test/integration/i18n-support/pages/auto-export/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Link from 'next/link'
import { useRouter } from 'next/router'

export default function Page(props) {
const router = useRouter()

return (
<>
<p id="auto-export">auto-export page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
</>
)
}
9 changes: 0 additions & 9 deletions test/integration/i18n-support/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,3 @@ export default function Page(props) {
</>
)
}

export const getServerSideProps = ({ locale, locales }) => {
return {
props: {
locale,
locales,
},
}
}
Loading

0 comments on commit 2170dfd

Please sign in to comment.