qlara 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aws.cjs +10 -9
- package/dist/aws.js +10 -9
- package/dist/cli.js +10 -9
- package/package.json +1 -1
- package/src/provider/aws/edge-handler.ts +55 -55
- package/src/provider/aws/renderer.ts +3 -3
package/dist/aws.cjs
CHANGED
|
@@ -69,12 +69,12 @@ function getContentType(filePath) {
|
|
|
69
69
|
const ext = (0, import_node_path.extname)(filePath).toLowerCase();
|
|
70
70
|
return CONTENT_TYPES[ext] || "application/octet-stream";
|
|
71
71
|
}
|
|
72
|
-
function getCacheControl(key) {
|
|
72
|
+
function getCacheControl(key, cacheTtl) {
|
|
73
73
|
if (key.includes("_next/static/") || key.includes(".chunk.")) {
|
|
74
74
|
return "public, max-age=31536000, immutable";
|
|
75
75
|
}
|
|
76
76
|
if (key.endsWith(".html")) {
|
|
77
|
-
return
|
|
77
|
+
return `public, max-age=0, s-maxage=${cacheTtl}, stale-while-revalidate=60`;
|
|
78
78
|
}
|
|
79
79
|
return "public, max-age=86400";
|
|
80
80
|
}
|
|
@@ -112,7 +112,7 @@ async function listAllKeys(client, bucketName) {
|
|
|
112
112
|
} while (continuationToken);
|
|
113
113
|
return keys;
|
|
114
114
|
}
|
|
115
|
-
async function syncToS3(client, bucketName, buildDir) {
|
|
115
|
+
async function syncToS3(client, bucketName, buildDir, cacheTtl = 3600) {
|
|
116
116
|
const files = listFiles(buildDir);
|
|
117
117
|
const newKeys = /* @__PURE__ */ new Set();
|
|
118
118
|
let uploaded = 0;
|
|
@@ -125,7 +125,7 @@ async function syncToS3(client, bucketName, buildDir) {
|
|
|
125
125
|
newKeys.add(key);
|
|
126
126
|
const body = (0, import_node_fs.readFileSync)(filePath);
|
|
127
127
|
const contentType = getContentType(filePath);
|
|
128
|
-
const cacheControl = getCacheControl(key);
|
|
128
|
+
const cacheControl = getCacheControl(key, cacheTtl);
|
|
129
129
|
await client.send(
|
|
130
130
|
new import_client_s3.PutObjectCommand({
|
|
131
131
|
Bucket: bucketName,
|
|
@@ -449,7 +449,7 @@ function buildTemplate(config) {
|
|
|
449
449
|
CachePolicyId: { Ref: "CachePolicy" },
|
|
450
450
|
LambdaFunctionAssociations: [
|
|
451
451
|
{
|
|
452
|
-
EventType: "origin-
|
|
452
|
+
EventType: "origin-request",
|
|
453
453
|
LambdaFunctionARN: { Ref: "EdgeHandlerVersion" },
|
|
454
454
|
IncludeBody: false
|
|
455
455
|
}
|
|
@@ -764,7 +764,7 @@ async function updateCloudFrontEdgeVersion(cf, distributionId, newVersionArn) {
|
|
|
764
764
|
const lambdaAssociations = config.DefaultCacheBehavior?.LambdaFunctionAssociations;
|
|
765
765
|
if (lambdaAssociations?.Items) {
|
|
766
766
|
for (const assoc of lambdaAssociations.Items) {
|
|
767
|
-
if (assoc.EventType === "origin-
|
|
767
|
+
if (assoc.EventType === "origin-request") {
|
|
768
768
|
assoc.LambdaFunctionARN = newVersionArn;
|
|
769
769
|
}
|
|
770
770
|
}
|
|
@@ -878,7 +878,7 @@ function aws(awsConfig = {}) {
|
|
|
878
878
|
console.log(`[qlara/aws] Generated ${fallbacks.length} fallback page(s)`);
|
|
879
879
|
console.log("[qlara/aws] Syncing build output to S3...");
|
|
880
880
|
const s3 = createS3Client(res.region);
|
|
881
|
-
const { uploaded, deleted } = await syncToS3(s3, res.bucketName, buildDir);
|
|
881
|
+
const { uploaded, deleted } = await syncToS3(s3, res.bucketName, buildDir, cacheTtl);
|
|
882
882
|
console.log(`[qlara/aws] Uploaded ${uploaded} files, deleted ${deleted} stale files`);
|
|
883
883
|
console.log("[qlara/aws] Bundling edge handler...");
|
|
884
884
|
const edgeZip = await bundleEdgeHandler({
|
|
@@ -896,7 +896,8 @@ function aws(awsConfig = {}) {
|
|
|
896
896
|
await lambda.send(
|
|
897
897
|
new import_client_lambda.UpdateFunctionConfigurationCommand({
|
|
898
898
|
FunctionName: res.edgeFunctionArn,
|
|
899
|
-
Timeout: 30
|
|
899
|
+
Timeout: 30,
|
|
900
|
+
MemorySize: 512
|
|
900
901
|
})
|
|
901
902
|
);
|
|
902
903
|
await (0, import_client_lambda.waitUntilFunctionUpdatedV2)(
|
|
@@ -983,7 +984,7 @@ function aws(awsConfig = {}) {
|
|
|
983
984
|
new import_client_lambda.UpdateFunctionConfigurationCommand({
|
|
984
985
|
FunctionName: res.rendererFunctionArn,
|
|
985
986
|
Layers: [],
|
|
986
|
-
MemorySize:
|
|
987
|
+
MemorySize: 512,
|
|
987
988
|
Timeout: 30,
|
|
988
989
|
Environment: {
|
|
989
990
|
Variables: config.env ?? {}
|
package/dist/aws.js
CHANGED
|
@@ -66,12 +66,12 @@ function getContentType(filePath) {
|
|
|
66
66
|
const ext = extname(filePath).toLowerCase();
|
|
67
67
|
return CONTENT_TYPES[ext] || "application/octet-stream";
|
|
68
68
|
}
|
|
69
|
-
function getCacheControl(key) {
|
|
69
|
+
function getCacheControl(key, cacheTtl) {
|
|
70
70
|
if (key.includes("_next/static/") || key.includes(".chunk.")) {
|
|
71
71
|
return "public, max-age=31536000, immutable";
|
|
72
72
|
}
|
|
73
73
|
if (key.endsWith(".html")) {
|
|
74
|
-
return
|
|
74
|
+
return `public, max-age=0, s-maxage=${cacheTtl}, stale-while-revalidate=60`;
|
|
75
75
|
}
|
|
76
76
|
return "public, max-age=86400";
|
|
77
77
|
}
|
|
@@ -109,7 +109,7 @@ async function listAllKeys(client, bucketName) {
|
|
|
109
109
|
} while (continuationToken);
|
|
110
110
|
return keys;
|
|
111
111
|
}
|
|
112
|
-
async function syncToS3(client, bucketName, buildDir) {
|
|
112
|
+
async function syncToS3(client, bucketName, buildDir, cacheTtl = 3600) {
|
|
113
113
|
const files = listFiles(buildDir);
|
|
114
114
|
const newKeys = /* @__PURE__ */ new Set();
|
|
115
115
|
let uploaded = 0;
|
|
@@ -122,7 +122,7 @@ async function syncToS3(client, bucketName, buildDir) {
|
|
|
122
122
|
newKeys.add(key);
|
|
123
123
|
const body = readFileSync(filePath);
|
|
124
124
|
const contentType = getContentType(filePath);
|
|
125
|
-
const cacheControl = getCacheControl(key);
|
|
125
|
+
const cacheControl = getCacheControl(key, cacheTtl);
|
|
126
126
|
await client.send(
|
|
127
127
|
new PutObjectCommand({
|
|
128
128
|
Bucket: bucketName,
|
|
@@ -446,7 +446,7 @@ function buildTemplate(config) {
|
|
|
446
446
|
CachePolicyId: { Ref: "CachePolicy" },
|
|
447
447
|
LambdaFunctionAssociations: [
|
|
448
448
|
{
|
|
449
|
-
EventType: "origin-
|
|
449
|
+
EventType: "origin-request",
|
|
450
450
|
LambdaFunctionARN: { Ref: "EdgeHandlerVersion" },
|
|
451
451
|
IncludeBody: false
|
|
452
452
|
}
|
|
@@ -760,7 +760,7 @@ async function updateCloudFrontEdgeVersion(cf, distributionId, newVersionArn) {
|
|
|
760
760
|
const lambdaAssociations = config.DefaultCacheBehavior?.LambdaFunctionAssociations;
|
|
761
761
|
if (lambdaAssociations?.Items) {
|
|
762
762
|
for (const assoc of lambdaAssociations.Items) {
|
|
763
|
-
if (assoc.EventType === "origin-
|
|
763
|
+
if (assoc.EventType === "origin-request") {
|
|
764
764
|
assoc.LambdaFunctionARN = newVersionArn;
|
|
765
765
|
}
|
|
766
766
|
}
|
|
@@ -874,7 +874,7 @@ function aws(awsConfig = {}) {
|
|
|
874
874
|
console.log(`[qlara/aws] Generated ${fallbacks.length} fallback page(s)`);
|
|
875
875
|
console.log("[qlara/aws] Syncing build output to S3...");
|
|
876
876
|
const s3 = createS3Client(res.region);
|
|
877
|
-
const { uploaded, deleted } = await syncToS3(s3, res.bucketName, buildDir);
|
|
877
|
+
const { uploaded, deleted } = await syncToS3(s3, res.bucketName, buildDir, cacheTtl);
|
|
878
878
|
console.log(`[qlara/aws] Uploaded ${uploaded} files, deleted ${deleted} stale files`);
|
|
879
879
|
console.log("[qlara/aws] Bundling edge handler...");
|
|
880
880
|
const edgeZip = await bundleEdgeHandler({
|
|
@@ -892,7 +892,8 @@ function aws(awsConfig = {}) {
|
|
|
892
892
|
await lambda.send(
|
|
893
893
|
new UpdateFunctionConfigurationCommand({
|
|
894
894
|
FunctionName: res.edgeFunctionArn,
|
|
895
|
-
Timeout: 30
|
|
895
|
+
Timeout: 30,
|
|
896
|
+
MemorySize: 512
|
|
896
897
|
})
|
|
897
898
|
);
|
|
898
899
|
await waitUntilFunctionUpdatedV2(
|
|
@@ -979,7 +980,7 @@ function aws(awsConfig = {}) {
|
|
|
979
980
|
new UpdateFunctionConfigurationCommand({
|
|
980
981
|
FunctionName: res.rendererFunctionArn,
|
|
981
982
|
Layers: [],
|
|
982
|
-
MemorySize:
|
|
983
|
+
MemorySize: 512,
|
|
983
984
|
Timeout: 30,
|
|
984
985
|
Environment: {
|
|
985
986
|
Variables: config.env ?? {}
|
package/dist/cli.js
CHANGED
|
@@ -77,12 +77,12 @@ function getContentType(filePath) {
|
|
|
77
77
|
const ext = extname(filePath).toLowerCase();
|
|
78
78
|
return CONTENT_TYPES[ext] || "application/octet-stream";
|
|
79
79
|
}
|
|
80
|
-
function getCacheControl(key) {
|
|
80
|
+
function getCacheControl(key, cacheTtl) {
|
|
81
81
|
if (key.includes("_next/static/") || key.includes(".chunk.")) {
|
|
82
82
|
return "public, max-age=31536000, immutable";
|
|
83
83
|
}
|
|
84
84
|
if (key.endsWith(".html")) {
|
|
85
|
-
return
|
|
85
|
+
return `public, max-age=0, s-maxage=${cacheTtl}, stale-while-revalidate=60`;
|
|
86
86
|
}
|
|
87
87
|
return "public, max-age=86400";
|
|
88
88
|
}
|
|
@@ -120,7 +120,7 @@ async function listAllKeys(client, bucketName) {
|
|
|
120
120
|
} while (continuationToken);
|
|
121
121
|
return keys;
|
|
122
122
|
}
|
|
123
|
-
async function syncToS3(client, bucketName, buildDir) {
|
|
123
|
+
async function syncToS3(client, bucketName, buildDir, cacheTtl = 3600) {
|
|
124
124
|
const files = listFiles(buildDir);
|
|
125
125
|
const newKeys = /* @__PURE__ */ new Set();
|
|
126
126
|
let uploaded = 0;
|
|
@@ -133,7 +133,7 @@ async function syncToS3(client, bucketName, buildDir) {
|
|
|
133
133
|
newKeys.add(key);
|
|
134
134
|
const body = readFileSync(filePath);
|
|
135
135
|
const contentType = getContentType(filePath);
|
|
136
|
-
const cacheControl = getCacheControl(key);
|
|
136
|
+
const cacheControl = getCacheControl(key, cacheTtl);
|
|
137
137
|
await client.send(
|
|
138
138
|
new PutObjectCommand({
|
|
139
139
|
Bucket: bucketName,
|
|
@@ -457,7 +457,7 @@ function buildTemplate(config) {
|
|
|
457
457
|
CachePolicyId: { Ref: "CachePolicy" },
|
|
458
458
|
LambdaFunctionAssociations: [
|
|
459
459
|
{
|
|
460
|
-
EventType: "origin-
|
|
460
|
+
EventType: "origin-request",
|
|
461
461
|
LambdaFunctionARN: { Ref: "EdgeHandlerVersion" },
|
|
462
462
|
IncludeBody: false
|
|
463
463
|
}
|
|
@@ -768,7 +768,7 @@ async function updateCloudFrontEdgeVersion(cf, distributionId, newVersionArn) {
|
|
|
768
768
|
const lambdaAssociations = config.DefaultCacheBehavior?.LambdaFunctionAssociations;
|
|
769
769
|
if (lambdaAssociations?.Items) {
|
|
770
770
|
for (const assoc of lambdaAssociations.Items) {
|
|
771
|
-
if (assoc.EventType === "origin-
|
|
771
|
+
if (assoc.EventType === "origin-request") {
|
|
772
772
|
assoc.LambdaFunctionARN = newVersionArn;
|
|
773
773
|
}
|
|
774
774
|
}
|
|
@@ -882,7 +882,7 @@ function aws(awsConfig = {}) {
|
|
|
882
882
|
console.log(`[qlara/aws] Generated ${fallbacks.length} fallback page(s)`);
|
|
883
883
|
console.log("[qlara/aws] Syncing build output to S3...");
|
|
884
884
|
const s3 = createS3Client(res.region);
|
|
885
|
-
const { uploaded, deleted } = await syncToS3(s3, res.bucketName, buildDir);
|
|
885
|
+
const { uploaded, deleted } = await syncToS3(s3, res.bucketName, buildDir, cacheTtl);
|
|
886
886
|
console.log(`[qlara/aws] Uploaded ${uploaded} files, deleted ${deleted} stale files`);
|
|
887
887
|
console.log("[qlara/aws] Bundling edge handler...");
|
|
888
888
|
const edgeZip = await bundleEdgeHandler({
|
|
@@ -900,7 +900,8 @@ function aws(awsConfig = {}) {
|
|
|
900
900
|
await lambda.send(
|
|
901
901
|
new UpdateFunctionConfigurationCommand({
|
|
902
902
|
FunctionName: res.edgeFunctionArn,
|
|
903
|
-
Timeout: 30
|
|
903
|
+
Timeout: 30,
|
|
904
|
+
MemorySize: 512
|
|
904
905
|
})
|
|
905
906
|
);
|
|
906
907
|
await waitUntilFunctionUpdatedV2(
|
|
@@ -987,7 +988,7 @@ function aws(awsConfig = {}) {
|
|
|
987
988
|
new UpdateFunctionConfigurationCommand({
|
|
988
989
|
FunctionName: res.rendererFunctionArn,
|
|
989
990
|
Layers: [],
|
|
990
|
-
MemorySize:
|
|
991
|
+
MemorySize: 512,
|
|
991
992
|
Timeout: 30,
|
|
992
993
|
Environment: {
|
|
993
994
|
Variables: config.env ?? {}
|
package/package.json
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lambda@Edge origin-
|
|
2
|
+
* Lambda@Edge origin-request handler for Qlara.
|
|
3
3
|
*
|
|
4
4
|
* This file is bundled into a self-contained ZIP and deployed to Lambda@Edge.
|
|
5
5
|
* It does NOT run in the developer's Node.js — it runs at CloudFront edge locations.
|
|
6
6
|
*
|
|
7
7
|
* Config values are injected at bundle time via esbuild `define` (Lambda@Edge has no env vars).
|
|
8
8
|
*
|
|
9
|
+
* Runs as an **origin-request** trigger so that generated responses are cached by CloudFront.
|
|
10
|
+
* (Origin-response generated responses are NOT cached — documented AWS behavior.)
|
|
11
|
+
*
|
|
9
12
|
* Flow for a request to /product/5:
|
|
10
13
|
* 1. CloudFront viewer-request rewrites /product/5 → /product/5.html
|
|
11
|
-
* 2.
|
|
12
|
-
* 3.
|
|
13
|
-
* a.
|
|
14
|
-
* b.
|
|
15
|
-
*
|
|
16
|
-
* d. Edge handler serves the fully rendered HTML (first request gets full SEO)
|
|
17
|
-
* e. Subsequent requests hit S3 directly (page is cached)
|
|
14
|
+
* 2. CloudFront checks edge cache → miss → fires this origin-request handler
|
|
15
|
+
* 3. Handler checks S3 for product/5.html:
|
|
16
|
+
* a. File exists → return it directly (CloudFront caches it)
|
|
17
|
+
* b. File doesn't exist → invoke renderer → return rendered HTML (CloudFront caches it)
|
|
18
|
+
* 4. Subsequent requests hit CloudFront edge cache directly (~10-30ms)
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
@@ -48,18 +49,21 @@ interface CloudFrontResponse {
|
|
|
48
49
|
statusDescription: string;
|
|
49
50
|
headers: Record<string, Array<{ key: string; value: string }>>;
|
|
50
51
|
body?: string;
|
|
52
|
+
bodyEncoding?: string;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
interface CloudFrontRequest {
|
|
54
56
|
uri: string;
|
|
55
57
|
querystring: string;
|
|
58
|
+
method: string;
|
|
59
|
+
headers: Record<string, Array<{ key: string; value: string }>>;
|
|
60
|
+
origin?: Record<string, unknown>;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
interface
|
|
63
|
+
interface CloudFrontRequestEvent {
|
|
59
64
|
Records: Array<{
|
|
60
65
|
cf: {
|
|
61
66
|
request: CloudFrontRequest;
|
|
62
|
-
response: CloudFrontResponse;
|
|
63
67
|
};
|
|
64
68
|
}>;
|
|
65
69
|
}
|
|
@@ -227,66 +231,62 @@ async function invokeRenderer(uri: string, match: RouteMatch): Promise<string |
|
|
|
227
231
|
|
|
228
232
|
// ── Response builder ─────────────────────────────────────────────
|
|
229
233
|
|
|
230
|
-
|
|
231
|
-
const READ_ONLY_HEADERS = ['transfer-encoding', 'via'];
|
|
232
|
-
|
|
233
|
-
function buildHtmlResponse(
|
|
234
|
-
html: string,
|
|
235
|
-
originalResponse: CloudFrontResponse
|
|
236
|
-
): CloudFrontResponse {
|
|
237
|
-
const headers: Record<string, Array<{ key: string; value: string }>> = {
|
|
238
|
-
'content-type': [
|
|
239
|
-
{ key: 'Content-Type', value: 'text/html; charset=utf-8' },
|
|
240
|
-
],
|
|
241
|
-
'cache-control': [
|
|
242
|
-
{ key: 'Cache-Control', value: `public, max-age=0, s-maxage=${__QLARA_CACHE_TTL__}, stale-while-revalidate=60` },
|
|
243
|
-
],
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
// Preserve read-only headers from the original response to avoid 502
|
|
247
|
-
for (const headerName of READ_ONLY_HEADERS) {
|
|
248
|
-
if (originalResponse.headers[headerName]) {
|
|
249
|
-
headers[headerName] = originalResponse.headers[headerName];
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
234
|
+
function buildHtmlResponse(html: string): CloudFrontResponse {
|
|
253
235
|
return {
|
|
254
236
|
status: '200',
|
|
255
237
|
statusDescription: 'OK',
|
|
256
|
-
headers
|
|
238
|
+
headers: {
|
|
239
|
+
'content-type': [
|
|
240
|
+
{ key: 'Content-Type', value: 'text/html; charset=utf-8' },
|
|
241
|
+
],
|
|
242
|
+
'cache-control': [
|
|
243
|
+
{ key: 'Cache-Control', value: `public, max-age=0, s-maxage=${__QLARA_CACHE_TTL__}, stale-while-revalidate=60` },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
257
246
|
body: html,
|
|
247
|
+
bodyEncoding: 'text',
|
|
258
248
|
};
|
|
259
249
|
}
|
|
260
250
|
|
|
261
251
|
// ── Handler ──────────────────────────────────────────────────────
|
|
262
252
|
|
|
263
253
|
export async function handler(
|
|
264
|
-
event:
|
|
265
|
-
): Promise<CloudFrontResponse> {
|
|
266
|
-
const
|
|
267
|
-
const response = record.response;
|
|
268
|
-
const request = record.request;
|
|
254
|
+
event: CloudFrontRequestEvent
|
|
255
|
+
): Promise<CloudFrontRequest | CloudFrontResponse> {
|
|
256
|
+
const request = event.Records[0].cf.request;
|
|
269
257
|
const uri = request.uri;
|
|
270
|
-
const status = parseInt(response.status, 10);
|
|
271
258
|
|
|
272
|
-
// 1.
|
|
273
|
-
|
|
274
|
-
return response;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// At this point, the file does NOT exist in S3 (403 from OAC or 404)
|
|
278
|
-
|
|
279
|
-
// 2. Skip non-HTML file requests — these are never dynamic routes
|
|
280
|
-
// e.g. /product/20.txt (RSC flight data), /product/20.json, etc.
|
|
259
|
+
// 1. Non-HTML file requests → forward to S3 origin (handled normally by CloudFront)
|
|
260
|
+
// e.g. /product/20.txt (RSC flight data), /product/20.json, JS, CSS, images, etc.
|
|
281
261
|
const nonHtmlExt = uri.match(/\.([a-z0-9]+)$/)?.[1];
|
|
282
262
|
if (nonHtmlExt && nonHtmlExt !== 'html') {
|
|
283
|
-
return
|
|
263
|
+
return request;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 2. Try to get the file from S3 directly
|
|
267
|
+
const s3Key = uri.replace(/^\//, '');
|
|
268
|
+
if (s3Key) {
|
|
269
|
+
try {
|
|
270
|
+
const s3Response = await s3.send(
|
|
271
|
+
new GetObjectCommand({
|
|
272
|
+
Bucket: __QLARA_BUCKET_NAME__,
|
|
273
|
+
Key: s3Key,
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
const body = await s3Response.Body?.transformToString('utf-8');
|
|
277
|
+
if (body) {
|
|
278
|
+
// File exists in S3 → return it directly (CloudFront will cache this)
|
|
279
|
+
return buildHtmlResponse(body);
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
// File doesn't exist in S3 — continue to dynamic route handling
|
|
283
|
+
}
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
// 3. Fetch manifest and check if this URL matches a Qlara dynamic route
|
|
287
|
-
const manifest = await getManifest();
|
|
288
287
|
// Strip .html suffix that the URL rewrite function adds before matching
|
|
289
288
|
const cleanUri = uri.replace(/\.html$/, '');
|
|
289
|
+
const manifest = await getManifest();
|
|
290
290
|
const match = manifest ? matchRoute(cleanUri, manifest.routes) : null;
|
|
291
291
|
|
|
292
292
|
// 4. If route matches: invoke renderer synchronously to get fully rendered HTML
|
|
@@ -295,17 +295,17 @@ export async function handler(
|
|
|
295
295
|
const renderedHtml = await invokeRenderer(cleanUri, match);
|
|
296
296
|
|
|
297
297
|
if (renderedHtml) {
|
|
298
|
-
return buildHtmlResponse(renderedHtml
|
|
298
|
+
return buildHtmlResponse(renderedHtml);
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
// Renderer failed — fall back to unpatched fallback HTML (no SEO, but page still works)
|
|
302
302
|
const fallbackHtml = await getFallbackHtml(match.route);
|
|
303
303
|
if (fallbackHtml) {
|
|
304
304
|
const patchedHtml = patchFallback(fallbackHtml, match.params);
|
|
305
|
-
return buildHtmlResponse(patchedHtml
|
|
305
|
+
return buildHtmlResponse(patchedHtml);
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
// 5. No match or no fallback — return
|
|
310
|
-
return
|
|
309
|
+
// 5. No match or no fallback — forward to origin (S3 will return 403/404)
|
|
310
|
+
return request;
|
|
311
311
|
}
|
|
@@ -79,6 +79,9 @@ interface RendererResult {
|
|
|
79
79
|
|
|
80
80
|
const FALLBACK_PLACEHOLDER = '__QLARA_FALLBACK__';
|
|
81
81
|
|
|
82
|
+
// Module-scope S3 client — reused across warm invocations (avoids recreating TCP/TLS connections)
|
|
83
|
+
const s3 = new S3Client({ region: process.env.AWS_REGION || 'us-east-1' });
|
|
84
|
+
|
|
82
85
|
/**
|
|
83
86
|
* Derive the S3 key for a rendered page.
|
|
84
87
|
* Matches the Next.js static export convention: /product/42 → product/42.html
|
|
@@ -763,9 +766,6 @@ export async function handler(event: RendererEvent & { warmup?: boolean }): Prom
|
|
|
763
766
|
}
|
|
764
767
|
|
|
765
768
|
const { uri, bucket, routePattern, params } = event;
|
|
766
|
-
const region = process.env.AWS_REGION || 'us-east-1';
|
|
767
|
-
|
|
768
|
-
const s3 = new S3Client({ region });
|
|
769
769
|
|
|
770
770
|
try {
|
|
771
771
|
// 0. Check if already rendered + read fallback in parallel
|