@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.
@@ -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
- const { cssHref, fontHrefs, themeName, bodyClass, htmlLang } = getRenderShellConfig();
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 = MOBILE_RE.test(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop";
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: { paths?: string[]; countries?: string[] };
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 && segments.length > 0) {
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
- return origin;
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
- return origin;
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