Skip to content

Commit 04923ee

Browse files
Merge pull request #50 from DBB-Software/feat/PLATFORM-1707
feat: added localization support with redirects
2 parents fe164ee + 3ecaae9 commit 04923ee

File tree

7 files changed

+83
-25
lines changed

7 files changed

+83
-25
lines changed

src/cdk/constructs/ViewerRequestLambdaEdge.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import * as logs from 'aws-cdk-lib/aws-logs'
66
import * as iam from 'aws-cdk-lib/aws-iam'
77
import path from 'node:path'
88
import { buildLambda } from '../../build/edge'
9-
import { NextRedirects } from '../../types'
9+
import { NextRedirects, DeployConfig } from '../../types'
1010

1111
interface ViewerRequestLambdaEdgeProps extends cdk.StackProps {
1212
buildOutputPath: string
1313
nodejs?: string
1414
redirects?: NextRedirects
15+
internationalizationConfig?: DeployConfig['internationalization']
16+
trailingSlash?: boolean
1517
}
1618

1719
const NodeJSEnvironmentMapping: Record<string, lambda.Runtime> = {
@@ -23,15 +25,17 @@ export class ViewerRequestLambdaEdge extends Construct {
2325
public readonly lambdaEdge: cloudfront.experimental.EdgeFunction
2426

2527
constructor(scope: Construct, id: string, props: ViewerRequestLambdaEdgeProps) {
26-
const { nodejs, buildOutputPath } = props
28+
const { nodejs, buildOutputPath, redirects, internationalizationConfig, trailingSlash = false } = props
2729
super(scope, id)
2830

2931
const nodeJSEnvironment = NodeJSEnvironmentMapping[nodejs ?? ''] ?? NodeJSEnvironmentMapping['20']
3032
const name = 'viewerRequest'
3133

3234
buildLambda(name, buildOutputPath, {
3335
define: {
34-
'process.env.REDIRECTS': JSON.stringify(props.redirects ?? [])
36+
'process.env.REDIRECTS': JSON.stringify(redirects ?? []),
37+
'process.env.LOCALES_CONFIG': JSON.stringify(internationalizationConfig ?? null),
38+
'process.env.IS_TRAILING_SLASH': JSON.stringify(trailingSlash)
3539
}
3640
})
3741

src/cdk/stacks/NextCloudfrontStack.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { CloudFrontDistribution } from '../constructs/CloudFrontDistribution'
66
import { OriginResponseLambdaEdge } from '../constructs/OriginResponseLambdaEdge'
77
import { ViewerResponseLambdaEdge } from '../constructs/ViewerResponseLambdaEdge'
88
import { ViewerRequestLambdaEdge } from '../constructs/ViewerRequestLambdaEdge'
9-
import { CacheConfig, NextRedirects } from '../../types'
9+
import { DeployConfig, NextRedirects } from '../../types'
1010

1111
export interface NextCloudfrontStackProps extends StackProps {
1212
nodejs?: string
@@ -16,9 +16,10 @@ export interface NextCloudfrontStackProps extends StackProps {
1616
renderWorkerQueueUrl: string
1717
renderWorkerQueueArn: string
1818
buildOutputPath: string
19-
cacheConfig: CacheConfig
19+
deployConfig: DeployConfig
2020
imageTTL?: number
2121
redirects?: NextRedirects
22+
trailingSlash?: boolean
2223
}
2324

2425
export class NextCloudfrontStack extends Stack {
@@ -38,33 +39,36 @@ export class NextCloudfrontStack extends Stack {
3839
renderWorkerQueueUrl,
3940
renderWorkerQueueArn,
4041
region,
41-
cacheConfig,
42+
deployConfig,
4243
imageTTL,
43-
redirects
44+
redirects,
45+
trailingSlash = false
4446
} = props
4547

4648
this.originRequestLambdaEdge = new OriginRequestLambdaEdge(this, `${id}-OriginRequestLambdaEdge`, {
4749
nodejs,
4850
bucketName: staticBucketName,
4951
renderServerDomain,
5052
buildOutputPath,
51-
cacheConfig,
53+
cacheConfig: deployConfig.cache,
5254
bucketRegion: region
5355
})
5456

5557
this.originResponseLambdaEdge = new OriginResponseLambdaEdge(this, `${id}-OriginResponseLambdaEdge`, {
5658
nodejs,
5759
renderWorkerQueueUrl,
5860
buildOutputPath,
59-
cacheConfig,
61+
cacheConfig: deployConfig.cache,
6062
renderWorkerQueueArn,
6163
region
6264
})
6365

6466
this.viewerRequestLambdaEdge = new ViewerRequestLambdaEdge(this, `${id}-ViewerRequestLambdaEdge`, {
6567
buildOutputPath,
6668
nodejs,
67-
redirects
69+
redirects,
70+
internationalizationConfig: deployConfig.internationalization,
71+
trailingSlash
6872
})
6973

7074
this.viewerResponseLambdaEdge = new ViewerResponseLambdaEdge(this, `${id}-ViewerResponseLambdaEdge`, {
@@ -84,7 +88,7 @@ export class NextCloudfrontStack extends Stack {
8488
responseEdgeFunction: this.originResponseLambdaEdge.lambdaEdge,
8589
viewerResponseEdgeFunction: this.viewerResponseLambdaEdge.lambdaEdge,
8690
viewerRequestLambdaEdge: this.viewerRequestLambdaEdge.lambdaEdge,
87-
cacheConfig,
91+
cacheConfig: deployConfig.cache,
8892
imageTTL
8993
})
9094

src/commands/deploy.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export const deploy = async (config: DeployConfig) => {
8282
throw new Error('Was not able to find project settings.')
8383
}
8484

85-
const cacheConfig = await loadConfig()
85+
const deployConfig = await loadConfig()
8686

8787
const nextConfig = (await loadFile(projectSettings.nextConfigPath)) as NextConfig
8888
const nextRedirects = nextConfig.redirects ? await nextConfig.redirects() : undefined
@@ -148,8 +148,9 @@ export const deploy = async (config: DeployConfig) => {
148148
buildOutputPath: outputPath,
149149
crossRegionReferences: true,
150150
region,
151-
cacheConfig,
151+
deployConfig,
152152
imageTTL: nextConfig.imageTTL,
153+
trailingSlash: nextConfig.trailingSlash,
153154
redirects: nextRedirects,
154155
env: {
155156
region: AWS_EDGE_REGION // required since Edge can be deployed only here.

src/commands/helpers/createConfig.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import { findConfig } from './loadConfig'
44

55
const CONFIG_FILE_NAME = 'next-serverless.config.js'
66
const CONFIG_TEMPLATE = `/**
7-
* @type {import('@dbbs/next-serverless-deployment').CacheConfig}
7+
* @type {import('@dbbs/next-serverless-deployment').DeployConfig}
88
*/
99
const config = {
10-
noCacheRoutes: [],
11-
cacheCookies: [],
12-
cacheQueries: [],
13-
enableDeviceSplit: false
14-
}
10+
cache: {
11+
noCacheRoutes: [],
12+
cacheCookies: [],
13+
cacheQueries: [],
14+
enableDeviceSplit: false}
15+
}
1516
1617
module.exports = config
1718
`

src/commands/helpers/loadConfig.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
3-
import { CacheConfig } from '../../types'
3+
import type { DeployConfig } from '../../types'
44

55
export const findConfig = (configPath: string): string | undefined => {
66
return ['next-serverless.config.js', 'next-serverless.config.mjs', 'next-serverless.config.ts'].find((config) =>
77
fs.existsSync(path.join(configPath, config))
88
)
99
}
1010

11-
async function loadConfig(): Promise<CacheConfig> {
11+
async function loadConfig(): Promise<DeployConfig> {
1212
try {
1313
const serverConfig = findConfig(process.cwd())
1414

src/lambdas/viewerRequest.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { CloudFrontRequestCallback, Context, CloudFrontResponseEvent } from 'aws-lambda'
2-
import type { NextRedirects } from '../types'
2+
import type { NextRedirects, DeployConfig } from '../types'
3+
import path from 'node:path'
34

45
/**
56
* AWS Lambda@Edge Viewer Request handler for Next.js redirects
@@ -17,14 +18,53 @@ export const handler = async (
1718
) => {
1819
const request = event.Records[0].cf.request
1920
const redirectsConfig = process.env.REDIRECTS as unknown as NextRedirects
21+
const localesConfig = process.env.LOCALES_CONFIG as unknown as DeployConfig['internationalization'] | null
22+
const isTrailingSlash = process.env.IS_TRAILING_SLASH as unknown as boolean
23+
const pathHasTrailingSlash = request.uri.endsWith('/')
2024

21-
const redirect = redirectsConfig.find((r) => r.source === request.uri)
25+
if (pathHasTrailingSlash && !isTrailingSlash) {
26+
request.uri = request.uri.slice(0, -1)
27+
} else if (!pathHasTrailingSlash && isTrailingSlash) {
28+
request.uri += '/'
29+
}
30+
31+
let shouldRedirectWithLocale = false
32+
let pagePath = request.uri
33+
let locale = ''
34+
let redirectTo = ''
35+
let redirectStatus = '307'
36+
37+
if (localesConfig) {
38+
const [requestLocale, ...restPath] = request.uri.substring(1).split('/')
39+
shouldRedirectWithLocale = !localesConfig.locales.find((locale) => locale === requestLocale)
40+
41+
if (!shouldRedirectWithLocale) {
42+
pagePath = `/${restPath.join('/')}`
43+
locale = requestLocale
44+
} else {
45+
locale = localesConfig.defaultLocale
46+
}
47+
}
48+
49+
const redirect = redirectsConfig.find((r) => r.source === pagePath)
2250

2351
if (redirect) {
52+
redirectTo = locale ? `/${path.join(locale, redirect.destination)}` : redirect.destination
53+
redirectStatus = redirect.statusCode ? String(redirect.statusCode) : redirect.permanent ? '308' : '307'
54+
} else if (shouldRedirectWithLocale) {
55+
redirectTo = `/${path.join(locale, pagePath)}`
56+
}
57+
58+
if (redirectTo) {
2459
return callback(null, {
25-
status: redirect.statusCode ? String(redirect.statusCode) : redirect.permanent ? '308' : '307',
60+
status: redirectStatus,
2661
headers: {
27-
location: [{ key: 'Location', value: redirect.destination }]
62+
location: [
63+
{
64+
key: 'Location',
65+
value: `${redirectTo}${request.querystring ? `?${request.querystring}` : ''}`
66+
}
67+
]
2868
}
2969
})
3070
}

src/types/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,12 @@ export interface CacheConfig {
77
enableDeviceSplit?: boolean
88
}
99

10+
export interface DeployConfig {
11+
internationalization?: {
12+
locales: string[]
13+
defaultLocale: string
14+
}
15+
cache: CacheConfig
16+
}
17+
1018
export type NextRedirects = Awaited<ReturnType<Required<NextConfig>['redirects']>>

0 commit comments

Comments
 (0)