@decocms/start 0.23.0 → 0.24.0
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/.cursor/skills/deco-cms-route-config/SKILL.md +75 -28
- package/.cursor/skills/deco-edge-caching/SKILL.md +112 -0
- package/package.json +1 -1
- package/src/admin/cors.ts +23 -3
- package/src/admin/index.ts +1 -1
- package/src/admin/meta.ts +4 -8
- package/src/admin/render.ts +4 -25
- package/src/admin/setup.ts +3 -3
- package/src/cms/index.ts +2 -0
- package/src/cms/loader.ts +2 -6
- package/src/cms/registry.ts +4 -3
- package/src/cms/resolve.ts +94 -61
- package/src/cms/sectionLoaders.ts +2 -9
- package/src/hooks/DecoPageRenderer.tsx +2 -9
- package/src/matchers/countryNames.ts +15 -0
- package/src/sdk/cacheHeaders.ts +26 -0
- package/src/sdk/djb2.ts +20 -0
- package/src/sdk/htmlShell.ts +55 -0
- package/src/sdk/index.ts +10 -0
- package/src/sdk/redirects.ts +8 -0
- package/src/sdk/requestContext.ts +3 -2
- package/src/sdk/sitemap.ts +21 -4
- package/src/sdk/urlUtils.ts +17 -0
- package/src/sdk/useDevice.ts +11 -2
- package/src/sdk/workerEntry.ts +68 -54
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -25,14 +25,15 @@
|
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import { getRenderShellConfig } from "../admin/setup";
|
|
29
28
|
import {
|
|
30
29
|
type CacheProfile,
|
|
31
30
|
cacheHeaders,
|
|
32
31
|
detectCacheProfile,
|
|
33
32
|
getCacheProfileConfig,
|
|
34
33
|
} from "./cacheHeaders";
|
|
34
|
+
import { buildHtmlShell } from "./htmlShell";
|
|
35
35
|
import { cleanPathForCacheKey } from "./urlUtils";
|
|
36
|
+
import { isMobileUA } from "./useDevice";
|
|
36
37
|
|
|
37
38
|
// ---------------------------------------------------------------------------
|
|
38
39
|
// Types
|
|
@@ -68,6 +69,13 @@ export interface SegmentKey {
|
|
|
68
69
|
loggedIn?: boolean;
|
|
69
70
|
/** Commerce sales channel / price list. */
|
|
70
71
|
salesChannel?: string;
|
|
72
|
+
/**
|
|
73
|
+
* VTEX region ID for regionalized pricing/availability.
|
|
74
|
+
* When present, cache entries are segmented per region.
|
|
75
|
+
* Sites without regionalization should omit this field
|
|
76
|
+
* to avoid unnecessary cache fragmentation.
|
|
77
|
+
*/
|
|
78
|
+
regionId?: string;
|
|
71
79
|
/** Sorted list of active A/B flag names for cache cohort splitting. */
|
|
72
80
|
flags?: string[];
|
|
73
81
|
}
|
|
@@ -127,6 +135,10 @@ export interface DecoWorkerEntryOptions {
|
|
|
127
135
|
* device: /mobile|android|iphone/i.test(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop",
|
|
128
136
|
* loggedIn: vtx.isLoggedIn,
|
|
129
137
|
* salesChannel: vtx.salesChannel,
|
|
138
|
+
* // Include regionId only if the site uses VTEX regionalization.
|
|
139
|
+
* // When present, cache entries split by region; omit it for
|
|
140
|
+
* // non-regionalized sites to maximize cache sharing.
|
|
141
|
+
* regionId: vtx.regionId ?? undefined,
|
|
130
142
|
* };
|
|
131
143
|
* },
|
|
132
144
|
* });
|
|
@@ -244,34 +256,7 @@ const PREVIEW_SHELL_SCRIPT = `(function() {
|
|
|
244
256
|
})();`;
|
|
245
257
|
|
|
246
258
|
function buildPreviewShell(): string {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const themeAttr = themeName ? ` data-theme="${themeName}"` : "";
|
|
250
|
-
const langAttr = htmlLang ? ` lang="${htmlLang}"` : "";
|
|
251
|
-
const bodyAttr = bodyClass ? ` class="${bodyClass}"` : "";
|
|
252
|
-
|
|
253
|
-
const stylesheets = [
|
|
254
|
-
...fontHrefs.map((href) => `<link rel="stylesheet" href="${href}" />`),
|
|
255
|
-
cssHref ? `<link rel="stylesheet" href="${cssHref}" />` : "",
|
|
256
|
-
]
|
|
257
|
-
.filter(Boolean)
|
|
258
|
-
.join("\n ");
|
|
259
|
-
|
|
260
|
-
return `<!DOCTYPE html>
|
|
261
|
-
<html${langAttr}${themeAttr}>
|
|
262
|
-
<head>
|
|
263
|
-
<meta charset="utf-8" />
|
|
264
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
265
|
-
<title>Preview</title>
|
|
266
|
-
${stylesheets}
|
|
267
|
-
<script>${PREVIEW_SHELL_SCRIPT}</script>
|
|
268
|
-
</head>
|
|
269
|
-
<body${bodyAttr}>
|
|
270
|
-
<div id="preview-root" style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui;color:#666;">
|
|
271
|
-
Loading preview...
|
|
272
|
-
</div>
|
|
273
|
-
</body>
|
|
274
|
-
</html>`;
|
|
259
|
+
return buildHtmlShell({ script: PREVIEW_SHELL_SCRIPT });
|
|
275
260
|
}
|
|
276
261
|
|
|
277
262
|
// ---------------------------------------------------------------------------
|
|
@@ -316,7 +301,6 @@ export function injectGeoCookies(request: Request): Request {
|
|
|
316
301
|
return new Request(request, { headers });
|
|
317
302
|
}
|
|
318
303
|
|
|
319
|
-
const MOBILE_RE = /mobile|android|iphone|ipad|ipod/i;
|
|
320
304
|
const ONE_YEAR = 31536000;
|
|
321
305
|
|
|
322
306
|
const DEFAULT_BYPASS_PATHS = ["/_server", "/_build", "/deco/", "/live/", "/.decofile"];
|
|
@@ -393,6 +377,7 @@ export function createDecoWorkerEntry(
|
|
|
393
377
|
const parts: string[] = [seg.device];
|
|
394
378
|
if (seg.loggedIn) parts.push("auth");
|
|
395
379
|
if (seg.salesChannel) parts.push(`sc=${seg.salesChannel}`);
|
|
380
|
+
if (seg.regionId) parts.push(`r=${seg.regionId}`);
|
|
396
381
|
if (seg.flags?.length) parts.push(`f=${seg.flags.sort().join(",")}`);
|
|
397
382
|
return parts.join("|");
|
|
398
383
|
}
|
|
@@ -433,7 +418,7 @@ export function createDecoWorkerEntry(
|
|
|
433
418
|
}
|
|
434
419
|
|
|
435
420
|
if (deviceSpecificKeys) {
|
|
436
|
-
const device =
|
|
421
|
+
const device = isMobileUA(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop";
|
|
437
422
|
url.searchParams.set("__cf_device", device);
|
|
438
423
|
}
|
|
439
424
|
|
|
@@ -442,6 +427,32 @@ export function createDecoWorkerEntry(
|
|
|
442
427
|
|
|
443
428
|
// -- Purge handler ----------------------------------------------------------
|
|
444
429
|
|
|
430
|
+
interface PurgeRequestBody {
|
|
431
|
+
paths?: string[];
|
|
432
|
+
countries?: string[];
|
|
433
|
+
/** Sales channels to include in segment combos. Defaults to ["1"]. */
|
|
434
|
+
salesChannels?: string[];
|
|
435
|
+
/** Region IDs to include in segment combos. Each ID generates additional entries. */
|
|
436
|
+
regionIds?: string[];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function buildPurgeSegments(body: PurgeRequestBody): SegmentKey[] {
|
|
440
|
+
const devices: Array<"mobile" | "desktop"> = ["mobile", "desktop"];
|
|
441
|
+
const channels = body.salesChannels ?? ["1"];
|
|
442
|
+
const regions: Array<string | undefined> = [undefined, ...(body.regionIds ?? [])];
|
|
443
|
+
|
|
444
|
+
const segments: SegmentKey[] = [];
|
|
445
|
+
for (const device of devices) {
|
|
446
|
+
for (const salesChannel of channels) {
|
|
447
|
+
for (const regionId of regions) {
|
|
448
|
+
segments.push({ device, salesChannel, regionId });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
segments.push({ device });
|
|
452
|
+
}
|
|
453
|
+
return segments;
|
|
454
|
+
}
|
|
455
|
+
|
|
445
456
|
async function handlePurge(request: Request, env: Record<string, unknown>): Promise<Response> {
|
|
446
457
|
if (purgeTokenEnv === false) {
|
|
447
458
|
return new Response("Purge disabled", { status: 404 });
|
|
@@ -452,7 +463,7 @@ export function createDecoWorkerEntry(
|
|
|
452
463
|
return new Response("Unauthorized", { status: 401 });
|
|
453
464
|
}
|
|
454
465
|
|
|
455
|
-
let body:
|
|
466
|
+
let body: PurgeRequestBody;
|
|
456
467
|
try {
|
|
457
468
|
body = await request.json();
|
|
458
469
|
} catch {
|
|
@@ -464,10 +475,6 @@ export function createDecoWorkerEntry(
|
|
|
464
475
|
return new Response('Body must include "paths": ["/", "/page"]', { status: 400 });
|
|
465
476
|
}
|
|
466
477
|
|
|
467
|
-
// Geo strings to purge location-specific cache variants.
|
|
468
|
-
// Pass ["BR", "BR|São Paulo|Curitiba", ...] to purge specific geo variants.
|
|
469
|
-
// Each string must match the __cf_geo param format: "country|region|city".
|
|
470
|
-
// When omitted, only the non-geo cache entry is purged.
|
|
471
478
|
const geoVariants = body.countries ?? [];
|
|
472
479
|
|
|
473
480
|
const cache =
|
|
@@ -482,25 +489,18 @@ export function createDecoWorkerEntry(
|
|
|
482
489
|
const baseUrl = new URL(request.url).origin;
|
|
483
490
|
const purged: string[] = [];
|
|
484
491
|
|
|
485
|
-
// If using segment-based keys, purge requires known segment combos.
|
|
486
|
-
// For simplicity, purge common combos: both devices, default sales channel.
|
|
487
|
-
const segments: SegmentKey[] = buildSegment
|
|
488
|
-
? [
|
|
489
|
-
{ device: "mobile" },
|
|
490
|
-
{ device: "desktop" },
|
|
491
|
-
{ device: "mobile", salesChannel: "1" },
|
|
492
|
-
{ device: "desktop", salesChannel: "1" },
|
|
493
|
-
]
|
|
494
|
-
: [];
|
|
495
|
-
|
|
496
|
-
// Purge both without geo (non-geo-targeted) and with each specified geo variant
|
|
497
492
|
const geoKeys: (string | null)[] = [null, ...geoVariants];
|
|
498
493
|
|
|
499
494
|
for (const p of paths) {
|
|
500
|
-
if (buildSegment
|
|
495
|
+
if (buildSegment) {
|
|
496
|
+
const segments = buildPurgeSegments(body);
|
|
501
497
|
for (const seg of segments) {
|
|
502
498
|
for (const cc of geoKeys) {
|
|
503
499
|
const url = new URL(p, baseUrl);
|
|
500
|
+
if (cacheVersionEnv !== false) {
|
|
501
|
+
const version = (env[cacheVersionEnv] as string) || "";
|
|
502
|
+
if (version) url.searchParams.set("__v", version);
|
|
503
|
+
}
|
|
504
504
|
url.searchParams.set("__seg", hashSegment(seg));
|
|
505
505
|
if (cc) url.searchParams.set("__cf_geo", cc);
|
|
506
506
|
const key = new Request(url.toString(), { method: "GET" });
|
|
@@ -520,6 +520,10 @@ export function createDecoWorkerEntry(
|
|
|
520
520
|
for (const device of devices) {
|
|
521
521
|
for (const cc of geoKeys) {
|
|
522
522
|
const url = new URL(p, baseUrl);
|
|
523
|
+
if (cacheVersionEnv !== false) {
|
|
524
|
+
const version = (env[cacheVersionEnv] as string) || "";
|
|
525
|
+
if (version) url.searchParams.set("__v", version);
|
|
526
|
+
}
|
|
523
527
|
if (device) url.searchParams.set("__cf_device", device);
|
|
524
528
|
if (cc) url.searchParams.set("__cf_geo", cc);
|
|
525
529
|
const key = new Request(url.toString(), { method: "GET" });
|
|
@@ -670,16 +674,22 @@ export function createDecoWorkerEntry(
|
|
|
670
674
|
const origin = await serverEntry.fetch(request, env, ctx);
|
|
671
675
|
const profile = getProfile(url);
|
|
672
676
|
|
|
673
|
-
// If the profile is private/none/cart, strip any public cache headers
|
|
674
|
-
// the route may have set (prevents the search caching bug)
|
|
675
677
|
if (profile === "private" || profile === "none" || profile === "cart") {
|
|
676
678
|
const resp = new Response(origin.body, origin);
|
|
677
679
|
resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
|
678
680
|
resp.headers.delete("CDN-Cache-Control");
|
|
681
|
+
resp.headers.set("X-Cache", "BYPASS");
|
|
682
|
+
resp.headers.set("X-Cache-Reason", `non-cacheable:${profile}`);
|
|
679
683
|
return resp;
|
|
680
684
|
}
|
|
681
685
|
|
|
682
|
-
|
|
686
|
+
const resp = new Response(origin.body, origin);
|
|
687
|
+
const reason = request.method !== "GET"
|
|
688
|
+
? `method:${request.method}`
|
|
689
|
+
: "bypass-path";
|
|
690
|
+
resp.headers.set("X-Cache", "BYPASS");
|
|
691
|
+
resp.headers.set("X-Cache-Reason", reason);
|
|
692
|
+
return resp;
|
|
683
693
|
}
|
|
684
694
|
|
|
685
695
|
// Cacheable request — build segment-aware cache key
|
|
@@ -723,7 +733,10 @@ export function createDecoWorkerEntry(
|
|
|
723
733
|
const origin = await serverEntry.fetch(request, env, ctx);
|
|
724
734
|
|
|
725
735
|
if (origin.status !== 200) {
|
|
726
|
-
|
|
736
|
+
const resp = new Response(origin.body, origin);
|
|
737
|
+
resp.headers.set("X-Cache", "BYPASS");
|
|
738
|
+
resp.headers.set("X-Cache-Reason", `status:${origin.status}`);
|
|
739
|
+
return resp;
|
|
727
740
|
}
|
|
728
741
|
|
|
729
742
|
// Responses with Set-Cookie must never be cached — they carry
|
|
@@ -744,10 +757,11 @@ export function createDecoWorkerEntry(
|
|
|
744
757
|
const profile = getProfile(url);
|
|
745
758
|
const profileConfig = getCacheProfileConfig(profile);
|
|
746
759
|
|
|
747
|
-
// Don't cache non-public profiles
|
|
748
760
|
if (!profileConfig.isPublic || profileConfig.sMaxAge === 0) {
|
|
749
761
|
const resp = new Response(origin.body, origin);
|
|
750
762
|
resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
|
763
|
+
resp.headers.set("X-Cache", "BYPASS");
|
|
764
|
+
resp.headers.set("X-Cache-Reason", `profile:${profile}`);
|
|
751
765
|
return resp;
|
|
752
766
|
}
|
|
753
767
|
|