Skip to content
This repository was archived by the owner on Jan 28, 2025. It is now read-only.

Commit f9a6787

Browse files
authored
fix(lambda-at-edge): locale subpath bug fixes (#888)
* SSG index page routing * static pages / json files only in locale directories * separate unit tests for locale testing with/without basepath
1 parent 8e39294 commit f9a6787

29 files changed

+3092
-836
lines changed

packages/libs/lambda-at-edge/src/build.ts

Lines changed: 35 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ class Builder {
652652
htmlPagesNonDynamicLoop: for (const key in htmlPages.nonDynamic) {
653653
// Locale-prefixed pages don't need to be duplicated
654654
for (const locale of routesManifest.i18n.locales) {
655-
if (key.startsWith(`/${locale}/`)) {
655+
if (key.startsWith(`/${locale}/`) || key === `/${locale}`) {
656656
break htmlPagesNonDynamicLoop;
657657
}
658658
}
@@ -715,11 +715,18 @@ class Builder {
715715
ssgPages.nonDynamic[key]
716716
);
717717

718-
// Replace with localized value
719-
newSsgRoute.dataRoute = newSsgRoute.dataRoute.replace(
720-
`/_next/data/${buildId}/`,
721-
`/_next/data/${buildId}/${locale}/`
722-
);
718+
// Replace with localized value. For non-dynamic index page, this is in format "en.json"
719+
if (key === "/") {
720+
newSsgRoute.dataRoute = newSsgRoute.dataRoute.replace(
721+
`/_next/data/${buildId}/index.json`,
722+
`/_next/data/${buildId}/${locale}.json`
723+
);
724+
} else {
725+
newSsgRoute.dataRoute = newSsgRoute.dataRoute.replace(
726+
`/_next/data/${buildId}/`,
727+
`/_next/data/${buildId}/${locale}/`
728+
);
729+
}
723730

724731
newSsgRoute.srcRoute = newSsgRoute.srcRoute
725732
? `/${locale}/${newSsgRoute.srcRoute}`
@@ -911,9 +918,9 @@ class Builder {
911918
let prerenderManifestHTMLPageAssets: Promise<void>[] = [];
912919
let fallbackHTMLPageAssets: Promise<void>[] = [];
913920

914-
// Copy locale-specific prerendered files if defined, otherwise use empty which works for no locale
921+
// Copy locale-specific prerendered files if defined, otherwise use empty locale
922+
// which would copy to root only
915923
const locales = routesManifest.i18n?.locales ?? [""];
916-
const defaultLocale = routesManifest.i18n?.defaultLocale;
917924

918925
for (const locale of locales) {
919926
prerenderManifestJSONPropFileAssets.concat(
@@ -922,7 +929,15 @@ class Builder {
922929
? key + "index.json"
923930
: key + ".json";
924931

925-
const localePrefixedJSONFileName = locale + JSONFileName;
932+
let localePrefixedJSONFileName;
933+
934+
// If there are locales and index is SSG page
935+
// Filename is <locale>.json e.g en.json, not index.json or en/index.json
936+
if (locale && key === "/") {
937+
localePrefixedJSONFileName = `${locale}.json`;
938+
} else {
939+
localePrefixedJSONFileName = locale + JSONFileName;
940+
}
926941

927942
const source = path.join(
928943
dotNextDirectory,
@@ -933,23 +948,7 @@ class Builder {
933948
withBasePath(`_next/data/${buildId}/${localePrefixedJSONFileName}`)
934949
);
935950

936-
if (defaultLocale && defaultLocale === locale) {
937-
// If this is default locale, we need to copy to two destinations
938-
// the locale-prefixed path and non-locale-prefixed path
939-
const defaultDestination = path.join(
940-
assetOutputDirectory,
941-
withBasePath(`_next/data/${buildId}/${JSONFileName}`)
942-
);
943-
944-
return new Promise(async () => {
945-
await Promise.all([
946-
copyIfExists(source, destination),
947-
copyIfExists(source, defaultDestination)
948-
]);
949-
});
950-
} else {
951-
return copyIfExists(source, destination);
952-
}
951+
return copyIfExists(source, destination);
953952
})
954953
);
955954

@@ -959,7 +958,14 @@ class Builder {
959958
? path.join(key, "index.html")
960959
: key + ".html";
961960

962-
const localePrefixedPageFilePath = locale + pageFilePath;
961+
// If there are locales and index is SSG page,
962+
// Filename is <locale>.html e.g en.html, not index.html or en/index.html
963+
let localePrefixedPageFilePath;
964+
if (locale && key === "/") {
965+
localePrefixedPageFilePath = `${locale}.html`;
966+
} else {
967+
localePrefixedPageFilePath = locale + pageFilePath;
968+
}
963969

964970
const source = path.join(
965971
dotNextDirectory,
@@ -972,23 +978,7 @@ class Builder {
972978
)
973979
);
974980

975-
if (defaultLocale && defaultLocale === locale) {
976-
// If this is default locale, we need to copy to two destinations
977-
// the locale-prefixed path and non-locale-prefixed path
978-
const defaultDestination = path.join(
979-
assetOutputDirectory,
980-
withBasePath(path.join("static-pages", buildId, pageFilePath))
981-
);
982-
983-
return new Promise(async () => {
984-
await Promise.all([
985-
copyIfExists(source, destination),
986-
copyIfExists(source, defaultDestination)
987-
]);
988-
});
989-
} else {
990-
return copyIfExists(source, destination);
991-
}
981+
return copyIfExists(source, destination);
992982
})
993983
);
994984

@@ -1013,23 +1003,7 @@ class Builder {
10131003
)
10141004
);
10151005

1016-
if (defaultLocale && defaultLocale === locale) {
1017-
// If this is default locale, we need to copy to two destinations
1018-
// the locale-prefixed path and non-locale-prefixed path
1019-
const defaultDestination = path.join(
1020-
assetOutputDirectory,
1021-
withBasePath(path.join("static-pages", buildId, fallback))
1022-
);
1023-
1024-
return new Promise(async () => {
1025-
await Promise.all([
1026-
copyIfExists(source, destination),
1027-
copyIfExists(source, defaultDestination)
1028-
]);
1029-
});
1030-
} else {
1031-
return copyIfExists(source, destination);
1032-
}
1006+
return copyIfExists(source, destination);
10331007
})
10341008
);
10351009
}

packages/libs/lambda-at-edge/src/default-handler.ts

Lines changed: 72 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ import { addHeadersToResponse } from "./headers/addHeaders";
3838
import { isValidPreviewRequest } from "./lib/isValidPreviewRequest";
3939
import { getUnauthenticatedResponse } from "./auth/authenticator";
4040
import { buildS3RetryStrategy } from "./s3/s3RetryStrategy";
41-
import { isLocaleIndexUri } from "./routing/locale-utils";
41+
import {
42+
isLocalePrefixedUri,
43+
removeLocalePrefixFromUri
44+
} from "./routing/locale-utils";
4245

4346
const basePath = RoutesManifestJson.basePath;
4447

@@ -67,13 +70,17 @@ const addS3HostHeader = (
6770

6871
const isDataRequest = (uri: string): boolean => uri.startsWith("/_next/data");
6972

70-
const normaliseUri = (uri: string): string => {
73+
const normaliseUri = (uri: string, routesManifest: RoutesManifest): string => {
7174
if (basePath) {
7275
if (uri.startsWith(basePath)) {
7376
uri = uri.slice(basePath.length);
7477
} else {
7578
// basePath set but URI does not start with basePath, return 404
76-
return "/404";
79+
if (routesManifest.i18n?.defaultLocale) {
80+
return `/${routesManifest.i18n?.defaultLocale}/404`;
81+
} else {
82+
return "/404";
83+
}
7784
}
7885
}
7986

@@ -217,7 +224,8 @@ export const handler = async (
217224
response = await handleOriginResponse({
218225
event,
219226
manifest,
220-
prerenderManifest
227+
prerenderManifest,
228+
routesManifest
221229
});
222230
} else {
223231
response = await handleOriginRequest({
@@ -277,7 +285,7 @@ const handleOriginRequest = async ({
277285
}
278286

279287
const basePath = routesManifest.basePath;
280-
let uri = normaliseUri(request.uri);
288+
let uri = normaliseUri(request.uri, routesManifest);
281289
const decodedUri = decodeURI(uri);
282290
const { pages, publicFiles } = manifest;
283291

@@ -294,7 +302,11 @@ const handleOriginRequest = async ({
294302
if (newUri.endsWith("/")) {
295303
newUri = newUri.slice(0, -1);
296304
}
297-
} else if (request.uri !== "/" && request.uri !== "" && uri !== "/404") {
305+
} else if (
306+
request.uri !== "/" &&
307+
request.uri !== "" &&
308+
!uri.endsWith("/404")
309+
) {
298310
// HTML/SSR pages get redirected based on trailingSlash in next.config.js
299311
// We do not redirect:
300312
// 1. Unnormalised URI is "/" or "" as this could cause a redirect loop due to browsers appending trailing slash
@@ -324,6 +336,34 @@ const handleOriginRequest = async ({
324336
);
325337
}
326338

339+
// Handle root language redirect
340+
const languageHeader = request.headers["accept-language"];
341+
const languageRedirectUri = getLanguageRedirect(
342+
languageHeader ? languageHeader[0].value : undefined,
343+
uri,
344+
routesManifest,
345+
manifest
346+
);
347+
348+
if (languageRedirectUri) {
349+
return createRedirectResponse(
350+
languageRedirectUri,
351+
request.querystring,
352+
307
353+
);
354+
}
355+
356+
// Always add default locale prefix to URIs without it that are not public files or data requests
357+
const defaultLocale = routesManifest.i18n?.defaultLocale;
358+
if (
359+
defaultLocale &&
360+
!isLocalePrefixedUri(uri, routesManifest) &&
361+
!isPublicFile &&
362+
!isDataReq
363+
) {
364+
uri = uri === "/" ? `/${defaultLocale}` : `/${defaultLocale}${uri}`;
365+
}
366+
327367
// Check for non-dynamic pages before rewriting
328368
const isNonDynamicRoute =
329369
pages.html.nonDynamic[uri] || pages.ssr.nonDynamic[uri] || isPublicFile;
@@ -369,25 +409,17 @@ const handleOriginRequest = async ({
369409
return await responsePromise;
370410
}
371411

372-
uri = normaliseUri(request.uri);
373-
}
374-
}
412+
uri = normaliseUri(request.uri, routesManifest);
375413

376-
// Handle root language rewrite
377-
const languageHeader = request.headers["accept-language"];
378-
const languageRedirectUri = getLanguageRedirect(
379-
languageHeader ? languageHeader[0].value : undefined,
380-
uri,
381-
routesManifest,
382-
manifest
383-
);
384-
385-
if (languageRedirectUri) {
386-
return createRedirectResponse(
387-
languageRedirectUri,
388-
request.querystring,
389-
307
390-
);
414+
if (
415+
defaultLocale &&
416+
!isLocalePrefixedUri(uri, routesManifest) &&
417+
!isPublicFile &&
418+
!isDataReq
419+
) {
420+
uri = uri === "/" ? `/${defaultLocale}` : `/${defaultLocale}${uri}`;
421+
}
422+
}
391423
}
392424

393425
const isStaticPage = pages.html.nonDynamic[uri]; // plain page without any props
@@ -396,7 +428,7 @@ const handleOriginRequest = async ({
396428
const s3Origin = origin.s3 as CloudFrontS3Origin;
397429
const isHTMLPage = isStaticPage || isPrerenderedPage;
398430
const normalisedS3DomainName = normaliseS3OriginDomain(s3Origin);
399-
const hasFallback = hasFallbackForUri(uri, prerenderManifest, manifest);
431+
const hasFallback = hasFallbackForUri(uri, manifest, routesManifest);
400432
const { now, log } = perfLogger(manifest.logLambdaExecutionTimes);
401433
const isPreviewRequest = isValidPreviewRequest(
402434
request.headers.cookie,
@@ -420,12 +452,7 @@ const handleOriginRequest = async ({
420452
}
421453
} else if (isHTMLPage || hasFallback) {
422454
s3Origin.path = `${basePath}/static-pages/${manifest.buildId}`;
423-
let pageName;
424-
if (isLocaleIndexUri(uri, routesManifest)) {
425-
pageName = `${uri}/index`;
426-
} else {
427-
pageName = uri === "/" ? "/index" : uri;
428-
}
455+
const pageName = uri === "/" ? "/index" : uri;
429456
request.uri = `${pageName}.html`;
430457
} else if (isDataReq) {
431458
// We need to check whether data request is unmatched i.e routed to 404.html or _error.js
@@ -441,8 +468,8 @@ const handleOriginRequest = async ({
441468
(!pages.ssg.nonDynamic[normalisedDataRequestUri] &&
442469
!hasFallbackForUri(
443470
normalisedDataRequestUri,
444-
prerenderManifest,
445-
manifest
471+
manifest,
472+
routesManifest
446473
))
447474
) {
448475
// Break to continue to SSR render in two cases:
@@ -526,11 +553,13 @@ const handleOriginRequest = async ({
526553
const handleOriginResponse = async ({
527554
event,
528555
manifest,
529-
prerenderManifest
556+
prerenderManifest,
557+
routesManifest
530558
}: {
531559
event: OriginResponseEvent;
532560
manifest: OriginRequestDefaultHandlerManifest;
533561
prerenderManifest: PrerenderManifestType;
562+
routesManifest: RoutesManifest;
534563
}) => {
535564
const response = event.Records[0].cf.response;
536565
const request = event.Records[0].cf.request;
@@ -549,7 +578,7 @@ const handleOriginResponse = async ({
549578
return response;
550579
}
551580

552-
const uri = normaliseUri(request.uri);
581+
const uri = normaliseUri(request.uri, routesManifest);
553582
const { domainName, region } = request.origin!.s3!;
554583
const bucketName = domainName.replace(`.s3.${region}.amazonaws.com`, "");
555584

@@ -615,7 +644,7 @@ const handleOriginResponse = async ({
615644
res.end(JSON.stringify(renderOpts.pageData));
616645
return await responsePromise;
617646
} else {
618-
const hasFallback = hasFallbackForUri(uri, prerenderManifest, manifest);
647+
const hasFallback = hasFallbackForUri(uri, manifest, routesManifest);
619648
if (!hasFallback) return response;
620649

621650
// If route has fallback, return that page from S3, otherwise return 404 page
@@ -675,8 +704,8 @@ const isOriginResponse = (
675704

676705
const hasFallbackForUri = (
677706
uri: string,
678-
prerenderManifest: PrerenderManifestType,
679-
manifest: OriginRequestDefaultHandlerManifest
707+
manifest: OriginRequestDefaultHandlerManifest,
708+
routesManifest: RoutesManifest
680709
) => {
681710
const {
682711
pages: { ssr, html, ssg }
@@ -712,7 +741,11 @@ const hasFallbackForUri = (
712741
const matchesFallbackRoute = Object.keys(ssg.dynamic).find(
713742
(dynamicSsgRoute) => {
714743
const fileMatchesPrerenderRoute =
715-
dynamicRoute.file === `pages${dynamicSsgRoute}.js`;
744+
dynamicRoute.file ===
745+
`pages${removeLocalePrefixFromUri(
746+
dynamicSsgRoute,
747+
routesManifest
748+
)}.js`;
716749

717750
if (fileMatchesPrerenderRoute) {
718751
foundFallback = ssg.dynamic[dynamicSsgRoute];

0 commit comments

Comments
 (0)