@decocms/blocks 7.62.2 → 7.62.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/blocks",
3
- "version": "7.62.2",
3
+ "version": "7.62.3",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -88,6 +88,35 @@ describe("canonicalizeServerFnPayloadForCacheKey — variant-param cache key", (
88
88
  expect(a).toBe(b);
89
89
  });
90
90
 
91
+ it("strips tracking params — paid traffic shares the canonical entry", () => {
92
+ const paid = canonicalizeServerFnPayloadForCacheKey(
93
+ envelope("/aneis?filter.color=ouro&utm_source=google&gclid=abc&srsltid=xyz"),
94
+ );
95
+ const canonical = canonicalizeServerFnPayloadForCacheKey(
96
+ envelope("/aneis?filter.color=ouro"),
97
+ );
98
+ expect(paid).toBe(canonical);
99
+ });
100
+
101
+ it("canonicalizes absolute URLs too, not just paths", () => {
102
+ // Sites embed one in the payload (the deferred-section `pageUrl` is built
103
+ // with `new URL(...)`), and the old guard skipped anything not starting
104
+ // with "/" — so not even skuId was stripped there.
105
+ const messy = canonicalizeServerFnPayloadForCacheKey(
106
+ envelope("https://store.com/mala-preta/p?skuId=148940&utm_source=ads"),
107
+ );
108
+ const canonical = canonicalizeServerFnPayloadForCacheKey(
109
+ envelope("https://store.com/mala-preta/p"),
110
+ );
111
+ expect(messy).toBe(canonical);
112
+ });
113
+
114
+ it("leaves non-URL strings alone", () => {
115
+ const a = canonicalizeServerFnPayloadForCacheKey(envelope("mala preta ?utm_source=x"));
116
+ expect(a).toBe(canonicalizeServerFnPayloadForCacheKey(envelope("mala preta ?utm_source=x")));
117
+ expect(a).toContain("utm_source");
118
+ });
119
+
91
120
  it("keeps PLP filter/search params (they DO change the response)", () => {
92
121
  const filtered = canonicalizeServerFnPayloadForCacheKey(
93
122
  envelope("/c/shoes?filter.size=40"),
@@ -17,6 +17,8 @@
17
17
  * ```
18
18
  */
19
19
 
20
+ import { isTrackingParam } from "./urlUtils";
21
+
20
22
  // ---------------------------------------------------------------------------
21
23
  // Types
22
24
  // ---------------------------------------------------------------------------
@@ -520,9 +522,11 @@ export const DEFAULT_SERVERFN_CACHE_IGNORE_PARAMS = ["skuId", "idsku"];
520
522
  /**
521
523
  * Canonicalize a TanStack GET server-fn `payload` for use in the edge cache
522
524
  * key: re-serialize it (so equivalent requests always produce byte-identical
523
- * keys) and strip the given cache-irrelevant search params from any embedded
524
- * route path. This makes `/product/p?skuId=148940` and `/product/p` share one
525
- * cache entry, since `loadCmsPage` resolves both to the same page.
525
+ * keys) and strip the cache-irrelevant search params — `ignoreParams` plus every
526
+ * known tracking param — from any embedded route path or absolute URL. This
527
+ * makes `/product/p?skuId=148940`, `/product/p?utm_source=ads` and `/product/p`
528
+ * share one cache entry, since `loadCmsPage` resolves all three to the same
529
+ * page.
526
530
  *
527
531
  * Returns the original payload unchanged on any parse failure (fail-safe: the
528
532
  * caller keeps the raw payload, never worse than before).
@@ -532,13 +536,34 @@ export function canonicalizeServerFnPayloadForCacheKey(
532
536
  ignoreParams: string[] = DEFAULT_SERVERFN_CACHE_IGNORE_PARAMS,
533
537
  ): string {
534
538
  try {
539
+ const drop = (sp: URLSearchParams): void => {
540
+ for (const p of ignoreParams) sp.delete(p);
541
+ // Tracking params never change what the server fn resolves, but they DO
542
+ // change this key — so a shopper arriving from paid media mints a brand
543
+ // new entry on every SPA interaction. Measured on a production
544
+ // storefront: `/_serverFn/:id` at 84% MISS, 1401ms average.
545
+ for (const k of [...sp.keys()]) if (isTrackingParam(k)) sp.delete(k);
546
+ };
535
547
  const stripQuery = (s: string): string => {
548
+ // Absolute URL: sites embed one in the payload (the deferred-section
549
+ // `pageUrl` is built with `new URL(...)`), and it fragments the key
550
+ // exactly like a relative path does — but the guard below skipped it, so
551
+ // not even `skuId` was canonicalized there.
552
+ if (/^https?:\/\//i.test(s)) {
553
+ try {
554
+ const u = new URL(s);
555
+ drop(u.searchParams);
556
+ return u.toString();
557
+ } catch {
558
+ return s;
559
+ }
560
+ }
536
561
  if (!s.startsWith("/") || s.startsWith("//")) return s;
537
562
  const qIdx = s.indexOf("?");
538
563
  if (qIdx < 0) return s;
539
564
  const path = s.slice(0, qIdx);
540
565
  const sp = new URLSearchParams(s.slice(qIdx + 1));
541
- for (const p of ignoreParams) sp.delete(p);
566
+ drop(sp);
542
567
  const rest = sp.toString();
543
568
  return rest ? `${path}?${rest}` : path;
544
569
  };
@@ -44,6 +44,17 @@ export function registerTrackingParams(params: string[]): void {
44
44
  }
45
45
  }
46
46
 
47
+ /**
48
+ * Whether a search param is a known tracking/attribution param.
49
+ *
50
+ * Exposed so cache-key builders can filter params in place without having to
51
+ * round-trip through a `URL` (server-fn payloads carry bare relative paths) and
52
+ * without keeping a second copy of the list.
53
+ */
54
+ export function isTrackingParam(param: string): boolean {
55
+ return UTM_PARAMS.has(param.toLowerCase());
56
+ }
57
+
47
58
  /**
48
59
  * Strip UTM and tracking parameters from a URL.
49
60
  *