Skip to content

Commit 2170dfd

Browse files
authored
Update to generate auto static pages with all locales (#17730)
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
1 parent 62b9183 commit 2170dfd

File tree

8 files changed

+219
-54
lines changed

8 files changed

+219
-54
lines changed

packages/next/build/index.ts

+101-8
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,8 @@ export default async function build(
725725
// n.b. we cannot handle this above in combinedPages because the dynamic
726726
// page must be in the `pages` array, but not in the mapping.
727727
exportPathMap: (defaultMap: any) => {
728+
const { i18n } = config.experimental
729+
728730
// Dynamically routed pages should be prerendered to be used as
729731
// a client-side skeleton (fallback) while data is being fetched.
730732
// This ensures the end-user never sees a 500 or slow response from the
@@ -738,7 +740,14 @@ export default async function build(
738740
if (ssgStaticFallbackPages.has(page)) {
739741
// Override the rendering for the dynamic page to be treated as a
740742
// fallback render.
741-
defaultMap[page] = { page, query: { __nextFallback: true } }
743+
if (i18n) {
744+
defaultMap[`/${i18n.defaultLocale}${page}`] = {
745+
page,
746+
query: { __nextFallback: true },
747+
}
748+
} else {
749+
defaultMap[page] = { page, query: { __nextFallback: true } }
750+
}
742751
} else {
743752
// Remove dynamically routed pages from the default path map when
744753
// fallback behavior is disabled.
@@ -760,6 +769,39 @@ export default async function build(
760769
}
761770
}
762771

772+
if (i18n) {
773+
for (const page of [
774+
...staticPages,
775+
...ssgPages,
776+
...(useStatic404 ? ['/404'] : []),
777+
]) {
778+
const isSsg = ssgPages.has(page)
779+
const isDynamic = isDynamicRoute(page)
780+
const isFallback = isSsg && ssgStaticFallbackPages.has(page)
781+
782+
for (const locale of i18n.locales) {
783+
if (!isSsg && locale === i18n.defaultLocale) continue
784+
// skip fallback generation for SSG pages without fallback mode
785+
if (isSsg && isDynamic && !isFallback) continue
786+
const outputPath = `/${locale}${page === '/' ? '' : page}`
787+
788+
defaultMap[outputPath] = {
789+
page: defaultMap[page].page,
790+
query: { __nextLocale: locale },
791+
}
792+
793+
if (isFallback) {
794+
defaultMap[outputPath].query.__nextFallback = true
795+
}
796+
}
797+
798+
if (isSsg && !isFallback) {
799+
// remove non-locale prefixed variant from defaultMap
800+
delete defaultMap[page]
801+
}
802+
}
803+
}
804+
763805
return defaultMap
764806
},
765807
trailingSlash: false,
@@ -786,7 +828,8 @@ export default async function build(
786828
page: string,
787829
file: string,
788830
isSsg: boolean,
789-
ext: 'html' | 'json'
831+
ext: 'html' | 'json',
832+
additionalSsgFile = false
790833
) => {
791834
file = `${file}.${ext}`
792835
const orig = path.join(exportOptions.outdir, file)
@@ -820,8 +863,58 @@ export default async function build(
820863
if (!isSsg) {
821864
pagesManifest[page] = relativeDest
822865
}
823-
await promises.mkdir(path.dirname(dest), { recursive: true })
824-
await promises.rename(orig, dest)
866+
867+
const { i18n } = config.experimental
868+
869+
// for SSG files with i18n the non-prerendered variants are
870+
// output with the locale prefixed so don't attempt moving
871+
// without the prefix
872+
if (!i18n || !isSsg || additionalSsgFile) {
873+
await promises.mkdir(path.dirname(dest), { recursive: true })
874+
await promises.rename(orig, dest)
875+
}
876+
877+
if (i18n) {
878+
if (additionalSsgFile) return
879+
880+
for (const locale of i18n.locales) {
881+
// auto-export default locale files exist at root
882+
// TODO: should these always be prefixed with locale
883+
// similar to SSG prerender/fallback files?
884+
if (!isSsg && locale === i18n.defaultLocale) {
885+
continue
886+
}
887+
888+
const localeExt = page === '/' ? path.extname(file) : ''
889+
const relativeDestNoPages = relativeDest.substr('pages/'.length)
890+
891+
const updatedRelativeDest = path.join(
892+
'pages',
893+
locale + localeExt,
894+
// if it's the top-most index page we want it to be locale.EXT
895+
// instead of locale/index.html
896+
page === '/' ? '' : relativeDestNoPages
897+
)
898+
const updatedOrig = path.join(
899+
exportOptions.outdir,
900+
locale + localeExt,
901+
page === '/' ? '' : file
902+
)
903+
const updatedDest = path.join(
904+
distDir,
905+
isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
906+
updatedRelativeDest
907+
)
908+
909+
if (!isSsg) {
910+
pagesManifest[
911+
`/${locale}${page === '/' ? '' : page}`
912+
] = updatedRelativeDest
913+
}
914+
await promises.mkdir(path.dirname(updatedDest), { recursive: true })
915+
await promises.rename(updatedOrig, updatedDest)
916+
}
917+
}
825918
}
826919

827920
// Only move /404 to /404 when there is no custom 404 as in that case we don't know about the 404 page
@@ -877,13 +970,13 @@ export default async function build(
877970
const extraRoutes = additionalSsgPaths.get(page) || []
878971
for (const route of extraRoutes) {
879972
const pageFile = normalizePagePath(route)
880-
await moveExportedPage(page, route, pageFile, true, 'html')
881-
await moveExportedPage(page, route, pageFile, true, 'json')
973+
await moveExportedPage(page, route, pageFile, true, 'html', true)
974+
await moveExportedPage(page, route, pageFile, true, 'json', true)
882975

883976
if (hasAmp) {
884977
const ampPage = `${pageFile}.amp`
885-
await moveExportedPage(page, ampPage, ampPage, true, 'html')
886-
await moveExportedPage(page, ampPage, ampPage, true, 'json')
978+
await moveExportedPage(page, ampPage, ampPage, true, 'html', true)
979+
await moveExportedPage(page, ampPage, ampPage, true, 'json', true)
887980
}
888981

889982
finalPrerenderRoutes[route] = {

packages/next/build/webpack/loaders/next-serverless-loader.ts

+1
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ const nextServerlessLoader: loader.Loader = function () {
242242
detectedLocale = detectedLocale || i18n.defaultLocale
243243
244244
if (
245+
!fromExport &&
245246
!nextStartMode &&
246247
i18n.localeDetection !== false &&
247248
(shouldAddLocalePrefix || shouldStripDefaultLocale)

packages/next/export/worker.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,21 @@ export default async function exportPage({
100100
const { page } = pathMap
101101
const filePath = normalizePagePath(path)
102102
const ampPath = `${filePath}.amp`
103+
const isDynamic = isDynamicRoute(page)
103104
let query = { ...originalQuery }
104105
let params: { [key: string]: string | string[] } | undefined
105106

106-
const localePathResult = normalizeLocalePath(path, renderOpts.locales)
107+
let updatedPath = path
108+
let locale = query.__nextLocale || renderOpts.locale
109+
delete query.__nextLocale
107110

108-
if (localePathResult.detectedLocale) {
109-
path = localePathResult.pathname
110-
renderOpts.locale = localePathResult.detectedLocale
111+
if (renderOpts.locale) {
112+
const localePathResult = normalizeLocalePath(path, renderOpts.locales)
113+
114+
if (localePathResult.detectedLocale) {
115+
updatedPath = localePathResult.pathname
116+
locale = localePathResult.detectedLocale
117+
}
111118
}
112119

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

124131
// Check if the page is a specified dynamic route
125-
if (isDynamicRoute(page) && page !== path) {
126-
params = getRouteMatcher(getRouteRegex(page))(path) || undefined
132+
if (isDynamic && page !== path) {
133+
params = getRouteMatcher(getRouteRegex(page))(updatedPath) || undefined
127134
if (params) {
128135
// we have to pass these separately for serverless
129136
if (!serverless) {
@@ -134,7 +141,7 @@ export default async function exportPage({
134141
}
135142
} else {
136143
throw new Error(
137-
`The provided export path '${path}' doesn't match the '${page}' page.\nRead more: https://err.sh/vercel/next.js/export-path-mismatch`
144+
`The provided export path '${updatedPath}' doesn't match the '${page}' page.\nRead more: https://err.sh/vercel/next.js/export-path-mismatch`
138145
)
139146
}
140147
}
@@ -149,7 +156,7 @@ export default async function exportPage({
149156
}
150157

151158
const req = ({
152-
url: path,
159+
url: updatedPath,
153160
...headerMocks,
154161
} as unknown) as IncomingMessage
155162
const res = ({
@@ -239,7 +246,7 @@ export default async function exportPage({
239246
fontManifest: optimizeFonts
240247
? requireFontManifest(distDir, serverless)
241248
: null,
242-
locale: renderOpts.locale!,
249+
locale: locale!,
243250
locales: renderOpts.locales!,
244251
},
245252
// @ts-ignore
@@ -298,6 +305,7 @@ export default async function exportPage({
298305
fontManifest: optimizeFonts
299306
? requireFontManifest(distDir, serverless)
300307
: null,
308+
locale: locale as string,
301309
}
302310
// @ts-ignore
303311
html = await renderMethod(req, res, page, query, curRenderOpts)

packages/next/next-server/server/next-server.ts

+26-10
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export default class Server {
354354
parsedUrl.pathname = localePathResult.pathname
355355
}
356356

357-
;(req as any)._nextLocale = detectedLocale || i18n.defaultLocale
357+
parsedUrl.query.__nextLocale = detectedLocale || i18n.defaultLocale
358358
}
359359

360360
res.statusCode = 200
@@ -510,7 +510,7 @@ export default class Server {
510510
pathname = localePathResult.pathname
511511
detectedLocale = localePathResult.detectedLocale
512512
}
513-
;(req as any)._nextLocale = detectedLocale || i18n.defaultLocale
513+
_parsedUrl.query.__nextLocale = detectedLocale || i18n.defaultLocale
514514
}
515515
pathname = getRouteFromAssetPath(pathname, '.json')
516516

@@ -1015,11 +1015,21 @@ export default class Server {
10151015
query: ParsedUrlQuery = {},
10161016
params: Params | null = null
10171017
): Promise<FindComponentsResult | null> {
1018-
const paths = [
1018+
let paths = [
10191019
// try serving a static AMP version first
10201020
query.amp ? normalizePagePath(pathname) + '.amp' : null,
10211021
pathname,
10221022
].filter(Boolean)
1023+
1024+
if (query.__nextLocale) {
1025+
paths = [
1026+
...paths.map(
1027+
(path) => `/${query.__nextLocale}${path === '/' ? '' : path}`
1028+
),
1029+
...paths,
1030+
]
1031+
}
1032+
10231033
for (const pagePath of paths) {
10241034
try {
10251035
const components = await loadComponents(
@@ -1031,7 +1041,11 @@ export default class Server {
10311041
components,
10321042
query: {
10331043
...(components.getStaticProps
1034-
? { _nextDataReq: query._nextDataReq, amp: query.amp }
1044+
? {
1045+
amp: query.amp,
1046+
_nextDataReq: query._nextDataReq,
1047+
__nextLocale: query.__nextLocale,
1048+
}
10351049
: query),
10361050
...(params || {}),
10371051
},
@@ -1141,7 +1155,8 @@ export default class Server {
11411155
urlPathname = stripNextDataPath(urlPathname)
11421156
}
11431157

1144-
const locale = (req as any)._nextLocale
1158+
const locale = query.__nextLocale as string
1159+
delete query.__nextLocale
11451160

11461161
const ssgCacheKey =
11471162
isPreviewMode || !isSSG
@@ -1214,7 +1229,7 @@ export default class Server {
12141229
'passthrough',
12151230
{
12161231
fontManifest: this.renderOpts.fontManifest,
1217-
locale: (req as any)._nextLocale,
1232+
locale,
12181233
locales: this.renderOpts.locales,
12191234
}
12201235
)
@@ -1235,7 +1250,7 @@ export default class Server {
12351250
...opts,
12361251
isDataReq,
12371252
resolvedUrl,
1238-
locale: (req as any)._nextLocale,
1253+
locale,
12391254
// For getServerSideProps we need to ensure we use the original URL
12401255
// and not the resolved URL to prevent a hydration mismatch on
12411256
// asPath
@@ -1321,7 +1336,9 @@ export default class Server {
13211336

13221337
// Production already emitted the fallback as static HTML.
13231338
if (isProduction) {
1324-
html = await this.incrementalCache.getFallback(pathname)
1339+
html = await this.incrementalCache.getFallback(
1340+
locale ? `/${locale}${pathname}` : pathname
1341+
)
13251342
}
13261343
// We need to generate the fallback on-demand for development.
13271344
else {
@@ -1442,7 +1459,6 @@ export default class Server {
14421459
res.statusCode = 500
14431460
return await this.renderErrorToHTML(err, req, res, pathname, query)
14441461
}
1445-
14461462
res.statusCode = 404
14471463
return await this.renderErrorToHTML(null, req, res, pathname, query)
14481464
}
@@ -1488,7 +1504,7 @@ export default class Server {
14881504

14891505
// use static 404 page if available and is 404 response
14901506
if (is404) {
1491-
result = await this.findPageComponents('/404')
1507+
result = await this.findPageComponents('/404', query)
14921508
using404Page = result !== null
14931509
}
14941510

packages/next/next-server/server/render.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ export async function renderToHTML(
413413

414414
const isFallback = !!query.__nextFallback
415415
delete query.__nextFallback
416+
delete query.__nextLocale
416417

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

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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Link from 'next/link'
2+
import { useRouter } from 'next/router'
3+
4+
export default function Page(props) {
5+
const router = useRouter()
6+
7+
return (
8+
<>
9+
<p id="auto-export">auto-export page</p>
10+
<p id="props">{JSON.stringify(props)}</p>
11+
<p id="router-locale">{router.locale}</p>
12+
<p id="router-locales">{JSON.stringify(router.locales)}</p>
13+
<p id="router-query">{JSON.stringify(router.query)}</p>
14+
<p id="router-pathname">{router.pathname}</p>
15+
<p id="router-as-path">{router.asPath}</p>
16+
<Link href="/">
17+
<a id="to-index">to /</a>
18+
</Link>
19+
</>
20+
)
21+
}

test/integration/i18n-support/pages/index.js

-9
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,3 @@ export default function Page(props) {
4444
</>
4545
)
4646
}
47-
48-
export const getServerSideProps = ({ locale, locales }) => {
49-
return {
50-
props: {
51-
locale,
52-
locales,
53-
},
54-
}
55-
}

0 commit comments

Comments
 (0)