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

Update to generate auto static pages with all locales #17730

Merged
merged 2 commits into from Oct 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
109 changes: 101 additions & 8 deletions packages/next/build/index.ts
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
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
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
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
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
@@ -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
Expand Up @@ -44,12 +44,3 @@ export default function Page(props) {
</>
)
}

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