@decocms/tanstack 7.12.1 → 7.15.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/package.json +4 -4
- package/src/sdk/workerEntry.test.ts +211 -2
- package/src/sdk/workerEntry.ts +141 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/tanstack",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco framework binding for TanStack Start + Cloudflare Workers",
|
|
6
6
|
"repository": {
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"lint:unused": "knip"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@decocms/blocks": "7.
|
|
28
|
-
"@decocms/blocks-admin": "7.
|
|
29
|
-
"@decocms/blocks-cli": "7.
|
|
27
|
+
"@decocms/blocks": "7.15.0",
|
|
28
|
+
"@decocms/blocks-admin": "7.15.0",
|
|
29
|
+
"@decocms/blocks-cli": "7.15.0",
|
|
30
30
|
"@deco-cx/warp-node": "^0.3.16",
|
|
31
31
|
"fast-json-patch": "^3.1.0",
|
|
32
32
|
"ws": "^8.18.0"
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { setBlocks } from "@decocms/blocks/cms";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { buildGeoCacheParam, createDecoWorkerEntry, detectLocationMatcher, injectGeoCookies } from "./workerEntry";
|
|
4
|
+
import { __resetKvHydrationStateForTests } from "./kvHydration";
|
|
5
|
+
|
|
6
|
+
const EMPTY_ENV = {};
|
|
7
|
+
const MOCK_CTX = { waitUntil: (_p: Promise<unknown>) => {} };
|
|
8
|
+
const MOCK_SERVER_ENTRY = {
|
|
9
|
+
fetch: async (_req: Request) => new Response("page content", { status: 200 }),
|
|
10
|
+
};
|
|
3
11
|
|
|
4
12
|
function parseCookies(header: string): Record<string, string> {
|
|
5
13
|
return Object.fromEntries(
|
|
@@ -104,3 +112,204 @@ describe("injectGeoCookies", () => {
|
|
|
104
112
|
expect(out.headers.get("cf-ray")).toBe("9ff5b26cf9bc067a");
|
|
105
113
|
});
|
|
106
114
|
});
|
|
115
|
+
|
|
116
|
+
describe("buildGeoCacheParam", () => {
|
|
117
|
+
const cf = { country: "BR", region: "São Paulo", city: "SP" };
|
|
118
|
+
|
|
119
|
+
it("returns undefined when granularity is off", () => {
|
|
120
|
+
expect(buildGeoCacheParam(cf, "off")).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns undefined when cf is undefined", () => {
|
|
124
|
+
expect(buildGeoCacheParam(undefined, "city")).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("returns country only when granularity is country", () => {
|
|
128
|
+
expect(buildGeoCacheParam(cf, "country")).toBe("BR");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("returns country|region when granularity is region", () => {
|
|
132
|
+
expect(buildGeoCacheParam(cf, "region")).toBe("BR|São Paulo");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("returns country|region|city when granularity is city", () => {
|
|
136
|
+
expect(buildGeoCacheParam(cf, "city")).toBe("BR|São Paulo|SP");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("omits missing fields gracefully", () => {
|
|
140
|
+
expect(buildGeoCacheParam({ country: "BR" }, "city")).toBe("BR");
|
|
141
|
+
expect(buildGeoCacheParam({ country: "BR", region: "MG" }, "city")).toBe("BR|MG");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("returns undefined when cf has none of country/region/city", () => {
|
|
145
|
+
expect(buildGeoCacheParam({ asn: "12345" }, "city")).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("detectLocationMatcher", () => {
|
|
150
|
+
it("returns true when decofile has a website/matchers/location.ts __resolveType", () => {
|
|
151
|
+
const blocks = {
|
|
152
|
+
"audiences/geo-audience.json": {
|
|
153
|
+
"__resolveType": "website/flags/audience.ts",
|
|
154
|
+
"matcher": { "__resolveType": "website/matchers/location.ts", "includeLocations": [{ "country": "BR" }] },
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
expect(detectLocationMatcher(blocks)).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("returns false when decofile has no location matcher", () => {
|
|
161
|
+
const blocks = {
|
|
162
|
+
"audiences/device-audience.json": {
|
|
163
|
+
"__resolveType": "website/flags/audience.ts",
|
|
164
|
+
"matcher": { "__resolveType": "website/matchers/device.ts" },
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
expect(detectLocationMatcher(blocks)).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("returns false for an empty decofile", () => {
|
|
171
|
+
expect(detectLocationMatcher({})).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("returns true when the matcher is nested deeply", () => {
|
|
175
|
+
const blocks = {
|
|
176
|
+
"pages/home.json": {
|
|
177
|
+
"variant": {
|
|
178
|
+
"matcher": { "__resolveType": "website/matchers/location.ts" },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
expect(detectLocationMatcher(blocks)).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("returns false when location.ts appears only in a non-resolveType string value (no false positive)", () => {
|
|
186
|
+
const blocks = {
|
|
187
|
+
"content/help.json": {
|
|
188
|
+
"__resolveType": "website/sections/RichText.tsx",
|
|
189
|
+
"body": "This page is controlled by website/matchers/location.ts for geo targeting.",
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
expect(detectLocationMatcher(blocks)).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("CMS redirects", () => {
|
|
197
|
+
afterEach(() => {
|
|
198
|
+
setBlocks({});
|
|
199
|
+
__resetKvHydrationStateForTests();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("returns a 301 redirect for a permanent redirect block", async () => {
|
|
203
|
+
setBlocks({
|
|
204
|
+
"redirect-1": {
|
|
205
|
+
__resolveType: "website/loaders/redirect.ts",
|
|
206
|
+
redirects: [{ from: "/old", to: "/new", type: "permanent" }],
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
|
|
210
|
+
const res = await worker.fetch(
|
|
211
|
+
new Request("https://example.com/old"),
|
|
212
|
+
EMPTY_ENV,
|
|
213
|
+
MOCK_CTX,
|
|
214
|
+
);
|
|
215
|
+
expect(res.status).toBe(301);
|
|
216
|
+
expect(res.headers.get("Location")).toBe("/new");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("redirects ?asJson requests too (does not fall through to page resolver)", async () => {
|
|
220
|
+
setBlocks({
|
|
221
|
+
"redirect-1": {
|
|
222
|
+
__resolveType: "website/loaders/redirect.ts",
|
|
223
|
+
redirects: [{ from: "/old", to: "/new", type: "permanent" }],
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
|
|
227
|
+
const res = await worker.fetch(
|
|
228
|
+
new Request("https://example.com/old?asJson"),
|
|
229
|
+
EMPTY_ENV,
|
|
230
|
+
MOCK_CTX,
|
|
231
|
+
);
|
|
232
|
+
expect(res.status).toBe(301);
|
|
233
|
+
expect(res.headers.get("Location")).toBe("/new");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("URI-encodes the Location header for non-ASCII destinations", async () => {
|
|
237
|
+
setBlocks({
|
|
238
|
+
"redirect-1": {
|
|
239
|
+
__resolveType: "website/loaders/redirect.ts",
|
|
240
|
+
redirects: [{ from: "/promo", to: "/promoção", type: "temporary" }],
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
|
|
244
|
+
const res = await worker.fetch(
|
|
245
|
+
new Request("https://example.com/promo"),
|
|
246
|
+
EMPTY_ENV,
|
|
247
|
+
MOCK_CTX,
|
|
248
|
+
);
|
|
249
|
+
expect(res.status).toBe(302);
|
|
250
|
+
expect(res.headers.get("Location")).toBe("/promo%C3%A7%C3%A3o");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("returns a 302 redirect for a temporary redirect block", async () => {
|
|
254
|
+
setBlocks({
|
|
255
|
+
"redirect-1": {
|
|
256
|
+
__resolveType: "website/loaders/redirect.ts",
|
|
257
|
+
redirects: [{ from: "/promo", to: "/sale", type: "temporary" }],
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
|
|
261
|
+
const res = await worker.fetch(
|
|
262
|
+
new Request("https://example.com/promo"),
|
|
263
|
+
EMPTY_ENV,
|
|
264
|
+
MOCK_CTX,
|
|
265
|
+
);
|
|
266
|
+
expect(res.status).toBe(302);
|
|
267
|
+
expect(res.headers.get("Location")).toBe("/sale");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("falls through to serverEntry for paths with no redirect", async () => {
|
|
271
|
+
setBlocks({
|
|
272
|
+
"redirect-1": {
|
|
273
|
+
__resolveType: "website/loaders/redirect.ts",
|
|
274
|
+
redirects: [{ from: "/old", to: "/new", type: "permanent" }],
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
|
|
278
|
+
const res = await worker.fetch(
|
|
279
|
+
new Request("https://example.com/other"),
|
|
280
|
+
EMPTY_ENV,
|
|
281
|
+
MOCK_CTX,
|
|
282
|
+
);
|
|
283
|
+
expect(res.status).toBe(200);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("picks up new redirects after setBlocks hot-reload", async () => {
|
|
287
|
+
setBlocks({});
|
|
288
|
+
const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
|
|
289
|
+
|
|
290
|
+
// First request with no redirects — falls through
|
|
291
|
+
const res1 = await worker.fetch(
|
|
292
|
+
new Request("https://example.com/v1"),
|
|
293
|
+
EMPTY_ENV,
|
|
294
|
+
MOCK_CTX,
|
|
295
|
+
);
|
|
296
|
+
expect(res1.status).toBe(200);
|
|
297
|
+
|
|
298
|
+
// Hot-reload: add a redirect
|
|
299
|
+
setBlocks({
|
|
300
|
+
"redirect-1": {
|
|
301
|
+
__resolveType: "website/loaders/redirect.ts",
|
|
302
|
+
redirects: [{ from: "/v1", to: "/v2", type: "permanent" }],
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Same path should now redirect
|
|
307
|
+
const res2 = await worker.fetch(
|
|
308
|
+
new Request("https://example.com/v1"),
|
|
309
|
+
EMPTY_ENV,
|
|
310
|
+
MOCK_CTX,
|
|
311
|
+
);
|
|
312
|
+
expect(res2.status).toBe(301);
|
|
313
|
+
expect(res2.headers.get("Location")).toBe("/v2");
|
|
314
|
+
});
|
|
315
|
+
});
|
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -30,12 +30,15 @@ import { getAppMiddleware } from "@decocms/blocks-admin/sdk/setupApps";
|
|
|
30
30
|
import {
|
|
31
31
|
isBot,
|
|
32
32
|
loadBlocks,
|
|
33
|
+
getRevision,
|
|
33
34
|
type MatcherContext,
|
|
35
|
+
onChange,
|
|
34
36
|
resolveDecoPage,
|
|
35
37
|
runSectionLoaders,
|
|
36
38
|
runSingleSectionLoader,
|
|
37
39
|
} from "@decocms/blocks/cms";
|
|
38
40
|
import { DECO_MATCHERS_OVERRIDE_PARAM } from "@decocms/blocks/matchers/override";
|
|
41
|
+
import { loadRedirects, matchRedirect, type RedirectMap } from "@decocms/blocks/sdk/redirects";
|
|
39
42
|
import {
|
|
40
43
|
type CacheProfileName,
|
|
41
44
|
cacheHeaders,
|
|
@@ -363,6 +366,29 @@ export interface DecoWorkerEntryOptions {
|
|
|
363
366
|
*/
|
|
364
367
|
autoInjectGeoCookies?: boolean;
|
|
365
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Controls how Cloudflare geo data is folded into edge cache keys.
|
|
371
|
+
*
|
|
372
|
+
* - `"auto"` (default): inspects the loaded decofile on every `setBlocks()`
|
|
373
|
+
* call (startup + fast-deploy swaps). If any block is configured with
|
|
374
|
+
* `website/matchers/location.ts`, geo keying is activated at `"region"`
|
|
375
|
+
* granularity (country+state); otherwise it is set to `"off"`. `"region"`
|
|
376
|
+
* covers VTEX regionalization and most geo matchers without city-level
|
|
377
|
+
* fragmentation. Sites that need city precision should set `"city"`
|
|
378
|
+
* explicitly. No manual configuration needed for the common case.
|
|
379
|
+
* - `"city"`: full `country|region|city` key — maximum isolation for sites
|
|
380
|
+
* with city-level location matchers.
|
|
381
|
+
* - `"region"`: `country|region` — good for region/state matchers (e.g.
|
|
382
|
+
* VTEX regionalization), avoids city-level fragmentation.
|
|
383
|
+
* - `"country"`: country only.
|
|
384
|
+
* - `"off"`: no `__cf_geo` param is added and the automatic `regionId`
|
|
385
|
+
* backfill from CF geo is disabled. Use for single-catalog storefronts
|
|
386
|
+
* that do not vary content by location — recovers the full edge HIT rate.
|
|
387
|
+
*
|
|
388
|
+
* @default "auto"
|
|
389
|
+
*/
|
|
390
|
+
geoCacheKey?: "auto" | "off" | "country" | "region" | "city";
|
|
391
|
+
|
|
366
392
|
/**
|
|
367
393
|
* Cookie names considered "safe" for caching — these are public/anonymous
|
|
368
394
|
* cookies that do not carry per-user session or auth data.
|
|
@@ -501,6 +527,43 @@ function buildPreviewShell(): string {
|
|
|
501
527
|
* };
|
|
502
528
|
* ```
|
|
503
529
|
*/
|
|
530
|
+
/**
|
|
531
|
+
* Returns `true` when the decofile contains at least one block whose
|
|
532
|
+
* `__resolveType` is `website/matchers/location.ts`. Used by
|
|
533
|
+
* `geoCacheKey: "auto"` to decide whether geo cache key fragmentation
|
|
534
|
+
* should be active.
|
|
535
|
+
*
|
|
536
|
+
* Matches only resolver-type references (not arbitrary string values) to
|
|
537
|
+
* avoid false positives from content/label fields that happen to mention
|
|
538
|
+
* the path.
|
|
539
|
+
*/
|
|
540
|
+
export function detectLocationMatcher(blocks: Record<string, unknown>): boolean {
|
|
541
|
+
return JSON.stringify(blocks).includes('"__resolveType":"website/matchers/location.ts"');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Build the `__cf_geo` cache-key fragment from Cloudflare's `request.cf`
|
|
546
|
+
* object at the requested granularity.
|
|
547
|
+
*
|
|
548
|
+
* Returns `undefined` when `granularity` is `"off"`, `cf` is absent, or no
|
|
549
|
+
* relevant fields are present.
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* buildGeoCacheParam({ country: "BR", region: "São Paulo", city: "SP" }, "region")
|
|
553
|
+
* // → "BR|São Paulo"
|
|
554
|
+
*/
|
|
555
|
+
export function buildGeoCacheParam(
|
|
556
|
+
cf: Record<string, string> | undefined,
|
|
557
|
+
granularity: "off" | "country" | "region" | "city",
|
|
558
|
+
): string | undefined {
|
|
559
|
+
if (granularity === "off" || !cf) return undefined;
|
|
560
|
+
const parts: string[] = [];
|
|
561
|
+
if (cf.country) parts.push(cf.country);
|
|
562
|
+
if (granularity !== "country" && cf.region) parts.push(cf.region);
|
|
563
|
+
if (granularity === "city" && cf.city) parts.push(cf.city);
|
|
564
|
+
return parts.length ? parts.join("|") : undefined;
|
|
565
|
+
}
|
|
566
|
+
|
|
504
567
|
export function injectGeoCookies(request: Request): Request {
|
|
505
568
|
const cf = (request as unknown as { cf?: Record<string, string> }).cf;
|
|
506
569
|
if (!cf) return request;
|
|
@@ -677,6 +740,28 @@ function deduplicateSetCookies(response: Response): void {
|
|
|
677
740
|
|
|
678
741
|
const FINGERPRINTED_ASSET_RE = /(?:\/_build)?\/assets\/.*-[a-zA-Z0-9_-]{8,}\.\w+$/;
|
|
679
742
|
|
|
743
|
+
// ---------------------------------------------------------------------------
|
|
744
|
+
// Auto geo-key detection (module-level singleton)
|
|
745
|
+
// ---------------------------------------------------------------------------
|
|
746
|
+
// Registered once so that multiple createDecoWorkerEntry calls in the same
|
|
747
|
+
// isolate (e.g. integration tests) don't accumulate listeners. Uses "region"
|
|
748
|
+
// (country+state) as the auto granularity — covers VTEX regionalization and
|
|
749
|
+
// most geo matchers without the city-level fragmentation (~hundreds of buckets).
|
|
750
|
+
// Sites that need city precision should set geoCacheKey: "city" explicitly.
|
|
751
|
+
|
|
752
|
+
let _autoGeoKey: "off" | "region" = "off";
|
|
753
|
+
let _autoGeoListenerInstalled = false;
|
|
754
|
+
|
|
755
|
+
function installAutoGeoListener(): void {
|
|
756
|
+
if (_autoGeoListenerInstalled) return;
|
|
757
|
+
_autoGeoListenerInstalled = true;
|
|
758
|
+
const update = (blocks: Record<string, unknown>) => {
|
|
759
|
+
_autoGeoKey = detectLocationMatcher(blocks) ? "region" : "off";
|
|
760
|
+
};
|
|
761
|
+
onChange(update);
|
|
762
|
+
update(loadBlocks());
|
|
763
|
+
}
|
|
764
|
+
|
|
680
765
|
const IMMUTABLE_HEADERS: Record<string, string> = {
|
|
681
766
|
"Cache-Control": `public, max-age=${ONE_YEAR}, immutable`,
|
|
682
767
|
Vary: "Accept-Encoding",
|
|
@@ -691,6 +776,14 @@ async function hashText(text: string): Promise<string> {
|
|
|
691
776
|
.join("");
|
|
692
777
|
}
|
|
693
778
|
|
|
779
|
+
// ---------------------------------------------------------------------------
|
|
780
|
+
// CMS redirect cache — keyed by revision so cross-bundle setBlocks() calls
|
|
781
|
+
// (Vite server-function split) are detected via globalThis.__deco.revision.
|
|
782
|
+
// ---------------------------------------------------------------------------
|
|
783
|
+
|
|
784
|
+
let _redirectMap: RedirectMap | null = null;
|
|
785
|
+
let _redirectMapRevision: string | null = null;
|
|
786
|
+
|
|
694
787
|
// ---------------------------------------------------------------------------
|
|
695
788
|
// Factory
|
|
696
789
|
// ---------------------------------------------------------------------------
|
|
@@ -724,6 +817,7 @@ export function createDecoWorkerEntry(
|
|
|
724
817
|
securityHeaders: securityHeadersOpt,
|
|
725
818
|
csp: cspOpt,
|
|
726
819
|
autoInjectGeoCookies: geoOpt = true,
|
|
820
|
+
geoCacheKey: geoCacheKeyOpt = "auto",
|
|
727
821
|
safeCookies: safeCookiesOpt = DEFAULT_SAFE_COOKIES,
|
|
728
822
|
staticPaths: staticPathsOpt = DEFAULT_STATIC_PATHS,
|
|
729
823
|
cdnCacheControl: cdnCacheControlOpt = "no-store",
|
|
@@ -740,6 +834,19 @@ export function createDecoWorkerEntry(
|
|
|
740
834
|
installDefaultUserAgent(outboundUserAgentOpt);
|
|
741
835
|
}
|
|
742
836
|
|
|
837
|
+
// When geoCacheKey is "auto", install the module-level listener that watches
|
|
838
|
+
// the decofile for website/matchers/location.ts. The listener is registered
|
|
839
|
+
// only once regardless of how many times this factory is called, so there is
|
|
840
|
+
// no listener accumulation in tests or dynamic worker-composition scenarios.
|
|
841
|
+
if (geoCacheKeyOpt === "auto") {
|
|
842
|
+
installAutoGeoListener();
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** Resolve the effective geo granularity for the current request. */
|
|
846
|
+
function effectiveGeoKey(): "off" | "country" | "region" | "city" {
|
|
847
|
+
return geoCacheKeyOpt === "auto" ? _autoGeoKey : geoCacheKeyOpt;
|
|
848
|
+
}
|
|
849
|
+
|
|
743
850
|
// Backfill `regionId` from Cloudflare geo when the consumer's buildSegment
|
|
744
851
|
// doesn't set one. Without this, sites using website/matchers/location.ts
|
|
745
852
|
// get a single cached response per device that leaks across regions: the
|
|
@@ -760,6 +867,7 @@ export function createDecoWorkerEntry(
|
|
|
760
867
|
? (request: Request): SegmentKey => {
|
|
761
868
|
const seg = rawBuildSegment(request);
|
|
762
869
|
if (seg.regionId) return seg;
|
|
870
|
+
if (effectiveGeoKey() === "off") return seg;
|
|
763
871
|
const region = readRegionFromRequest(request);
|
|
764
872
|
return region ? { ...seg, regionId: region } : seg;
|
|
765
873
|
}
|
|
@@ -913,17 +1021,14 @@ export function createDecoWorkerEntry(
|
|
|
913
1021
|
}
|
|
914
1022
|
|
|
915
1023
|
// Include CF geo data in cache key so location matcher results don't leak
|
|
916
|
-
// across different geos.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
url.searchParams.set("__cf_geo", geoParts.join("|"));
|
|
925
|
-
}
|
|
926
|
-
}
|
|
1024
|
+
// across different geos. Granularity is controlled by `geoCacheKey`:
|
|
1025
|
+
// "city" (default) → country|region|city, "region" → country|region,
|
|
1026
|
+
// "country" → country only, "off" → omitted entirely.
|
|
1027
|
+
const geoParam = buildGeoCacheParam(
|
|
1028
|
+
(request as unknown as { cf?: Record<string, string> }).cf,
|
|
1029
|
+
effectiveGeoKey(),
|
|
1030
|
+
);
|
|
1031
|
+
if (geoParam) url.searchParams.set("__cf_geo", geoParam);
|
|
927
1032
|
|
|
928
1033
|
// Bots render every section eagerly (shouldDeferSection short-circuits in
|
|
929
1034
|
// resolve.ts), producing a ~10x larger HTML payload (all eager-section
|
|
@@ -1449,6 +1554,23 @@ export function createDecoWorkerEntry(
|
|
|
1449
1554
|
return handlePurge(request, env);
|
|
1450
1555
|
}
|
|
1451
1556
|
|
|
1557
|
+
// CMS redirects (website/loaders/redirect.ts blocks) — checked before
|
|
1558
|
+
// ?asJson and commerce proxy so every request path respects redirects.
|
|
1559
|
+
// Revision-keyed so cross-bundle setBlocks() calls are detected even when
|
|
1560
|
+
// onChange listeners don't fire across Vite split-bundle module instances.
|
|
1561
|
+
const currentRevision = getRevision();
|
|
1562
|
+
if (_redirectMapRevision !== currentRevision) {
|
|
1563
|
+
_redirectMap = loadRedirects(loadBlocks());
|
|
1564
|
+
_redirectMapRevision = currentRevision;
|
|
1565
|
+
}
|
|
1566
|
+
const cmsRedirect = matchRedirect(url.pathname, _redirectMap!);
|
|
1567
|
+
if (cmsRedirect) {
|
|
1568
|
+
return new Response(null, {
|
|
1569
|
+
status: cmsRedirect.status,
|
|
1570
|
+
headers: { Location: encodeURI(cmsRedirect.to) },
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1452
1574
|
// ?asJson — return resolved page data as JSON (legacy deco compat)
|
|
1453
1575
|
if (url.searchParams.has("asJson") && request.method === "GET") {
|
|
1454
1576
|
const basePath = url.pathname;
|
|
@@ -1644,15 +1766,14 @@ export function createDecoWorkerEntry(
|
|
|
1644
1766
|
const device = isMobileUA(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop";
|
|
1645
1767
|
cacheKeyUrl.searchParams.set("__cf_device", device);
|
|
1646
1768
|
}
|
|
1647
|
-
// Include CF geo data so location-based content doesn't leak across geos
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
}
|
|
1769
|
+
// Include CF geo data so location-based content doesn't leak across geos.
|
|
1770
|
+
// Uses the same effectiveGeoKey() as the GET path so geoCacheKey config
|
|
1771
|
+
// is respected consistently for both HTML and server-fn responses.
|
|
1772
|
+
const sfnGeoParam = buildGeoCacheParam(
|
|
1773
|
+
(request as unknown as { cf?: Record<string, string> }).cf,
|
|
1774
|
+
effectiveGeoKey(),
|
|
1775
|
+
);
|
|
1776
|
+
if (sfnGeoParam) cacheKeyUrl.searchParams.set("__cf_geo", sfnGeoParam);
|
|
1656
1777
|
const sfnCacheKey = new Request(cacheKeyUrl.toString(), { method: "GET" });
|
|
1657
1778
|
|
|
1658
1779
|
// Use "listing" profile for server function responses
|