@shop-on/theme-server 0.4.0 → 0.6.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/dist/account.js +7 -4
- package/dist/analytics-proxy.d.ts +2 -1
- package/dist/analytics-proxy.js +2 -1
- package/dist/blog.d.ts +26 -0
- package/dist/blog.js +65 -1
- package/dist/cart.d.ts +7 -7
- package/dist/checkout.js +1 -1
- package/dist/collection.d.ts +41 -4
- package/dist/collection.js +66 -5
- package/dist/content-version.d.ts +12 -6
- package/dist/content-version.js +25 -4
- package/dist/graphql-env.d.ts +435 -29
- package/dist/graphql.js +8 -2
- package/dist/marketing-cookies.d.ts +6 -4
- package/dist/marketing-cookies.js +35 -14
- package/dist/metafields.d.ts +7 -0
- package/dist/metafields.js +33 -0
- package/dist/page.d.ts +15 -0
- package/dist/page.js +35 -1
- package/dist/product.d.ts +16 -1
- package/dist/product.js +44 -34
- package/dist/recommendations.d.ts +3 -0
- package/dist/recommendations.js +8 -3
- package/dist/schema.storefront.graphql +434 -20
- package/dist/search.d.ts +3 -0
- package/dist/search.js +5 -0
- package/package.json +3 -3
|
@@ -1,26 +1,47 @@
|
|
|
1
1
|
//#region src/marketing-cookies.ts
|
|
2
2
|
const MAX_LENGTH = 256;
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
const MAX_COOKIES_LENGTH = 2048;
|
|
4
|
+
const ALLOWED_NAMES = /* @__PURE__ */ new Set([
|
|
5
|
+
"_fbp",
|
|
6
|
+
"_fbc",
|
|
7
|
+
"_ga",
|
|
8
|
+
"_gcl_aw",
|
|
9
|
+
"_gcl_au",
|
|
10
|
+
"_ttp"
|
|
11
|
+
]);
|
|
12
|
+
const GA_STREAM_NAME = /^_ga_[A-Za-z0-9]+$/;
|
|
13
|
+
function isAllowed(name) {
|
|
14
|
+
return ALLOWED_NAMES.has(name) || GA_STREAM_NAME.test(name);
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
|
-
* Parse
|
|
14
|
-
* header is missing or carries
|
|
15
|
-
* all rather than a hash of nulls.
|
|
17
|
+
* Parse the attribution cookies out of a raw `Cookie` header. Returns `{}` when
|
|
18
|
+
* the header is missing or carries none — the caller then sends no attribution
|
|
19
|
+
* at all rather than a hash of nulls.
|
|
16
20
|
*/
|
|
17
21
|
function parseMarketingCookies(cookie) {
|
|
18
22
|
if (!cookie) return {};
|
|
23
|
+
const found = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const pair of cookie.split(";")) {
|
|
25
|
+
const separator = pair.indexOf("=");
|
|
26
|
+
if (separator === -1) continue;
|
|
27
|
+
const name = pair.slice(0, separator).trim();
|
|
28
|
+
const value = pair.slice(separator + 1).trim();
|
|
29
|
+
if (!isAllowed(name) || found.has(name)) continue;
|
|
30
|
+
if (!value || value.length > MAX_LENGTH) continue;
|
|
31
|
+
found.set(name, value);
|
|
32
|
+
}
|
|
19
33
|
const result = {};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
const fbp = found.get("_fbp");
|
|
35
|
+
const fbc = found.get("_fbc");
|
|
36
|
+
if (fbp) result.fbp = fbp;
|
|
37
|
+
if (fbc) result.fbc = fbc;
|
|
38
|
+
let cookies = "";
|
|
39
|
+
for (const [name, value] of found) {
|
|
40
|
+
const next = cookies ? `${cookies}; ${name}=${value}` : `${name}=${value}`;
|
|
41
|
+
if (next.length > MAX_COOKIES_LENGTH) break;
|
|
42
|
+
cookies = next;
|
|
23
43
|
}
|
|
44
|
+
if (cookies) result.cookies = cookies;
|
|
24
45
|
return result;
|
|
25
46
|
}
|
|
26
47
|
//#endregion
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { memoTTL } from "./cache.js";
|
|
2
|
+
import { getContentVersion } from "./content-version.js";
|
|
3
|
+
//#region src/metafields.ts
|
|
4
|
+
/** Metafield nodes as the `"namespace.key"`-keyed map every reader uses. */
|
|
5
|
+
function toMetafieldMap(nodes) {
|
|
6
|
+
return Object.fromEntries(nodes.map((m) => [`${m.namespace}.${m.key}`, {
|
|
7
|
+
type: m.type,
|
|
8
|
+
value: m.value
|
|
9
|
+
}]));
|
|
10
|
+
}
|
|
11
|
+
/** The cached metafield read behind each route resource's `*_metafield`
|
|
12
|
+
* dynamic source. `read` returns the resource's metafield nodes, or `null`
|
|
13
|
+
* when the resource is missing. A resource with no published fields is `{}`
|
|
14
|
+
* and IS cached; a missing resource or a failed read is retried next time.
|
|
15
|
+
* Either way the caller gets `{}`, so the template renders its literal. */
|
|
16
|
+
function metafieldsReader(name, read) {
|
|
17
|
+
const cached = memoTTL(async (ref, locale) => {
|
|
18
|
+
try {
|
|
19
|
+
const nodes = await read(ref, locale);
|
|
20
|
+
return nodes ? toMetafieldMap(nodes) : null;
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.warn(`[${name}] failed:`, err);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}, {
|
|
26
|
+
key: (ref, locale) => `${name}|${ref}|${locale ?? ""}`,
|
|
27
|
+
shouldCache: (v) => v !== null,
|
|
28
|
+
version: getContentVersion
|
|
29
|
+
});
|
|
30
|
+
return async (ref, locale) => await cached(ref, locale) ?? {};
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { metafieldsReader, toMetafieldMap };
|
package/dist/page.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MetafieldMap } from "./metafields.js";
|
|
1
2
|
//#region src/page.d.ts
|
|
2
3
|
export type PageData = {
|
|
3
4
|
id: string;
|
|
@@ -25,4 +26,18 @@ export type PageRef = {
|
|
|
25
26
|
* `page(id:)` applies the same `public_visible` gate as the handle path, so a
|
|
26
27
|
* missing/draft/cross-shop page returns null — the caller falls back. */
|
|
27
28
|
export declare const fetchPageById: (id: string, locale?: string) => Promise<PageRef | null>;
|
|
29
|
+
/** The page's storefront metafields keyed `"namespace.key"` — the
|
|
30
|
+
* `page_metafield` dynamic source. Its own read, not a field on `fetchPage`,
|
|
31
|
+
* so a page whose template binds none never asks for them. `{}` on a miss or
|
|
32
|
+
* a failure, so the literal renders. */
|
|
33
|
+
export declare const fetchPageMetafields: (ref: string, locale?: string) => Promise<MetafieldMap>;
|
|
34
|
+
/** The route CMS page as the theme loader's dynamic sources read it
|
|
35
|
+
* (`DynamicSourcePage` in `@shop-on/theme-loader`): the title off the page
|
|
36
|
+
* read the theme already made, and a lazy provider the loader calls only when
|
|
37
|
+
* a template binds a page field. One builder so a theme picks up a new
|
|
38
|
+
* provider on upgrade instead of re-listing them. */
|
|
39
|
+
export declare function routePageSource(page: Pick<PageData, "handle" | "title">, locale?: string): {
|
|
40
|
+
title: string;
|
|
41
|
+
getMetafields: () => Promise<MetafieldMap>;
|
|
42
|
+
};
|
|
28
43
|
//#endregion
|
package/dist/page.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { memoTTL } from "./cache.js";
|
|
2
2
|
import { graphql, request } from "./graphql.js";
|
|
3
3
|
import { getContentVersion } from "./content-version.js";
|
|
4
|
+
import { metafieldsReader } from "./metafields.js";
|
|
4
5
|
//#region src/page.ts
|
|
5
6
|
function parseRef(ref) {
|
|
6
7
|
return ref.startsWith("gid://") ? { id: ref } : { handle: ref };
|
|
@@ -90,5 +91,38 @@ const fetchPageById = async (id, locale) => {
|
|
|
90
91
|
return null;
|
|
91
92
|
}
|
|
92
93
|
};
|
|
94
|
+
const FetchPageMetafieldsQuery = graphql(`
|
|
95
|
+
query FetchPageMetafields($handle: String, $id: ID) {
|
|
96
|
+
page(handle: $handle, id: $id) {
|
|
97
|
+
# Storefront-visible only (the API gates on the storefront scope).
|
|
98
|
+
metafields(first: 50) {
|
|
99
|
+
nodes {
|
|
100
|
+
namespace
|
|
101
|
+
key
|
|
102
|
+
type
|
|
103
|
+
value
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
`);
|
|
109
|
+
/** The page's storefront metafields keyed `"namespace.key"` — the
|
|
110
|
+
* `page_metafield` dynamic source. Its own read, not a field on `fetchPage`,
|
|
111
|
+
* so a page whose template binds none never asks for them. `{}` on a miss or
|
|
112
|
+
* a failure, so the literal renders. */
|
|
113
|
+
const fetchPageMetafields = metafieldsReader("fetchPageMetafields", async (handle, locale) => {
|
|
114
|
+
return (await request(FetchPageMetafieldsQuery, parseRef(handle), locale ? { acceptLanguage: locale } : void 0)).page?.metafields.nodes ?? null;
|
|
115
|
+
});
|
|
116
|
+
/** The route CMS page as the theme loader's dynamic sources read it
|
|
117
|
+
* (`DynamicSourcePage` in `@shop-on/theme-loader`): the title off the page
|
|
118
|
+
* read the theme already made, and a lazy provider the loader calls only when
|
|
119
|
+
* a template binds a page field. One builder so a theme picks up a new
|
|
120
|
+
* provider on upgrade instead of re-listing them. */
|
|
121
|
+
function routePageSource(page, locale) {
|
|
122
|
+
return {
|
|
123
|
+
title: page.title,
|
|
124
|
+
getMetafields: () => fetchPageMetafields(page.handle, locale)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
93
127
|
//#endregion
|
|
94
|
-
export { fetchPage, fetchPageById };
|
|
128
|
+
export { fetchPage, fetchPageById, fetchPageMetafields, routePageSource };
|
package/dist/product.d.ts
CHANGED
|
@@ -48,6 +48,21 @@ export declare const BadgeTagsFragment: import("gql.tada").TadaDocumentNode<{
|
|
|
48
48
|
on: "ShopProduct";
|
|
49
49
|
masked: false;
|
|
50
50
|
}>;
|
|
51
|
+
/** `"namespace.key"` names as the card queries' metafield variables. Nothing
|
|
52
|
+
* named ⇒ the selection stays `@include`d out and the variables at their
|
|
53
|
+
* defaults, so a caller naming no keys sends exactly the query it always did.
|
|
54
|
+
* A name without a dot (or with an empty side) names no key and is dropped. */
|
|
55
|
+
export declare function metafieldKeysVariables(keys: readonly string[] | undefined): {
|
|
56
|
+
withMetafields?: true;
|
|
57
|
+
metafieldFirst?: number;
|
|
58
|
+
metafieldKeys?: Array<{
|
|
59
|
+
namespace: string;
|
|
60
|
+
key: string;
|
|
61
|
+
}>;
|
|
62
|
+
};
|
|
63
|
+
/** The cache-key segment for a key list: sorted, so two callers naming the
|
|
64
|
+
* same keys in a different order share one entry. */
|
|
65
|
+
export declare function metafieldKeysKey(keys: readonly string[] | undefined): string;
|
|
51
66
|
/** Lean fetch for product cards (collection grids, social tiles, shop-the-look).
|
|
52
67
|
* Skips description and caps media at 5. Used by the `product_list` resolver.
|
|
53
68
|
* `includeOptions` (default true) gates the variant-options payload — pass
|
|
@@ -55,7 +70,7 @@ export declare const BadgeTagsFragment: import("gql.tada").TadaDocumentNode<{
|
|
|
55
70
|
export declare function fetchProduct(ref: string, opts?: {
|
|
56
71
|
includeOptions?: boolean;
|
|
57
72
|
locale?: string;
|
|
58
|
-
|
|
73
|
+
metafieldKeys?: readonly string[];
|
|
59
74
|
}): Promise<ProductData | null>;
|
|
60
75
|
/** Lean card fetch with a plain-text description — same footprint as
|
|
61
76
|
* {@link fetchProduct}, plus `descriptionPlain`; no options/variants. For cards
|
package/dist/product.js
CHANGED
|
@@ -2,6 +2,7 @@ import { memoTTL } from "./cache.js";
|
|
|
2
2
|
import { graphql, request } from "./graphql.js";
|
|
3
3
|
import { getContentVersion } from "./content-version.js";
|
|
4
4
|
import { fetchFileById, toFileInfo } from "./files.js";
|
|
5
|
+
import { toMetafieldMap } from "./metafields.js";
|
|
5
6
|
//#region src/product.ts
|
|
6
7
|
const BadgeTagsFragment = graphql(`
|
|
7
8
|
fragment BadgeTags on ShopProduct @_unmask {
|
|
@@ -127,9 +128,9 @@ const FetchProductCardQuery = graphql(`
|
|
|
127
128
|
$handle: String
|
|
128
129
|
$id: ID
|
|
129
130
|
$includeOptions: Boolean!
|
|
130
|
-
$
|
|
131
|
-
$
|
|
132
|
-
$
|
|
131
|
+
$withMetafields: Boolean = false
|
|
132
|
+
$metafieldFirst: Int = 0
|
|
133
|
+
$metafieldKeys: [MetafieldKeyInput!] = []
|
|
133
134
|
) {
|
|
134
135
|
product(handle: $handle, id: $id) {
|
|
135
136
|
id
|
|
@@ -162,14 +163,16 @@ const FetchProductCardQuery = graphql(`
|
|
|
162
163
|
options @include(if: $includeOptions) {
|
|
163
164
|
...ProductOptionNode
|
|
164
165
|
}
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
metafield
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
166
|
+
# The storefront-visible metafields a card names and no more (a
|
|
167
|
+
# theme's product-variations swatches name one; a closest product
|
|
168
|
+
# metafield source bound in the picker's scope names each it reads).
|
|
169
|
+
metafields(first: $metafieldFirst, keys: $metafieldKeys) @include(if: $withMetafields) {
|
|
170
|
+
nodes {
|
|
171
|
+
namespace
|
|
172
|
+
key
|
|
173
|
+
type
|
|
174
|
+
value
|
|
175
|
+
}
|
|
173
176
|
}
|
|
174
177
|
media(first: 5) {
|
|
175
178
|
nodes {
|
|
@@ -306,25 +309,38 @@ function toMoney(m) {
|
|
|
306
309
|
currencyCode: m.currencyCode
|
|
307
310
|
};
|
|
308
311
|
}
|
|
309
|
-
/** `namespace.key` as the
|
|
310
|
-
*
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
/** `"namespace.key"` names as the card queries' metafield variables. Nothing
|
|
313
|
+
* named ⇒ the selection stays `@include`d out and the variables at their
|
|
314
|
+
* defaults, so a caller naming no keys sends exactly the query it always did.
|
|
315
|
+
* A name without a dot (or with an empty side) names no key and is dropped. */
|
|
316
|
+
function metafieldKeysVariables(keys) {
|
|
317
|
+
const parsed = [];
|
|
318
|
+
for (const ref of keys ?? []) {
|
|
319
|
+
const dot = ref.indexOf(".");
|
|
320
|
+
if (dot <= 0 || dot === ref.length - 1) continue;
|
|
321
|
+
parsed.push({
|
|
322
|
+
namespace: ref.slice(0, dot),
|
|
323
|
+
key: ref.slice(dot + 1)
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (parsed.length === 0) return {};
|
|
314
327
|
return {
|
|
315
|
-
|
|
316
|
-
|
|
328
|
+
withMetafields: true,
|
|
329
|
+
metafieldFirst: parsed.length,
|
|
330
|
+
metafieldKeys: parsed
|
|
317
331
|
};
|
|
318
332
|
}
|
|
319
|
-
|
|
320
|
-
|
|
333
|
+
/** The cache-key segment for a key list: sorted, so two callers naming the
|
|
334
|
+
* same keys in a different order share one entry. */
|
|
335
|
+
function metafieldKeysKey(keys) {
|
|
336
|
+
return keys?.length ? [...keys].sort().join(",") : "";
|
|
337
|
+
}
|
|
338
|
+
const fetchProductCardCached = memoTTL(async (ref, includeOptions, locale, metafieldKeys) => {
|
|
321
339
|
try {
|
|
322
340
|
const p = (await request(FetchProductCardQuery, {
|
|
323
341
|
...parseRef(ref),
|
|
324
342
|
includeOptions,
|
|
325
|
-
|
|
326
|
-
metafieldNamespace: lookup?.namespace ?? "",
|
|
327
|
-
metafieldKey: lookup?.key ?? ""
|
|
343
|
+
...metafieldKeysVariables(metafieldKeys)
|
|
328
344
|
}, locale ? { acceptLanguage: locale } : void 0)).product;
|
|
329
345
|
if (!p) return null;
|
|
330
346
|
return {
|
|
@@ -343,17 +359,14 @@ const fetchProductCardCached = memoTTL(async (ref, includeOptions, locale, metaf
|
|
|
343
359
|
compare_at: p.defaultVariant.compareAtV2 ? toMoney(p.defaultVariant.compareAtV2) : null,
|
|
344
360
|
media: toMedia(p.media.nodes, 480),
|
|
345
361
|
...p.options ? { options: p.options.map(toOption) } : {},
|
|
346
|
-
...p.
|
|
347
|
-
type: p.metafield.type,
|
|
348
|
-
value: p.metafield.value
|
|
349
|
-
} } } : {}
|
|
362
|
+
...p.metafields ? { metafields: toMetafieldMap(p.metafields.nodes) } : {}
|
|
350
363
|
};
|
|
351
364
|
} catch (err) {
|
|
352
365
|
console.warn("[fetchProduct] failed:", err);
|
|
353
366
|
return null;
|
|
354
367
|
}
|
|
355
368
|
}, {
|
|
356
|
-
key: (ref, includeOptions, locale,
|
|
369
|
+
key: (ref, includeOptions, locale, metafieldKeys) => `card|${ref}|${includeOptions}|${locale ?? ""}|${metafieldKeysKey(metafieldKeys)}`,
|
|
357
370
|
shouldCache: (v) => v !== null,
|
|
358
371
|
version: getContentVersion
|
|
359
372
|
});
|
|
@@ -362,7 +375,7 @@ const fetchProductCardCached = memoTTL(async (ref, includeOptions, locale, metaf
|
|
|
362
375
|
* `includeOptions` (default true) gates the variant-options payload — pass
|
|
363
376
|
* false when the consuming cards render no swatches. */
|
|
364
377
|
function fetchProduct(ref, opts) {
|
|
365
|
-
return fetchProductCardCached(ref, opts?.includeOptions ?? true, opts?.locale, opts?.
|
|
378
|
+
return fetchProductCardCached(ref, opts?.includeOptions ?? true, opts?.locale, opts?.metafieldKeys);
|
|
366
379
|
}
|
|
367
380
|
const FetchProductShowcaseQuery = graphql(`
|
|
368
381
|
query FetchProductShowcase($handle: String, $id: ID) {
|
|
@@ -464,10 +477,7 @@ async function loadProductFull(handle, locale) {
|
|
|
464
477
|
title: p.seo.title,
|
|
465
478
|
description: p.seo.description
|
|
466
479
|
},
|
|
467
|
-
metafields:
|
|
468
|
-
type: m.type,
|
|
469
|
-
value: m.value
|
|
470
|
-
}]))
|
|
480
|
+
metafields: toMetafieldMap(p.metafields.nodes)
|
|
471
481
|
};
|
|
472
482
|
} catch (err) {
|
|
473
483
|
console.warn("[fetchProductFull] failed:", err);
|
|
@@ -657,4 +667,4 @@ const fetchProductShowcaseMedia = memoTTL(async ({ handle, namespace, imageKey,
|
|
|
657
667
|
version: getContentVersion
|
|
658
668
|
});
|
|
659
669
|
//#endregion
|
|
660
|
-
export { BadgeTagsFragment, fetchProduct, fetchProductFull, fetchProductListDetail, fetchProductShowcase, fetchProductShowcaseMedia, fetchVariantMediaMap };
|
|
670
|
+
export { BadgeTagsFragment, fetchProduct, fetchProductFull, fetchProductListDetail, fetchProductShowcase, fetchProductShowcaseMedia, fetchVariantMediaMap, metafieldKeysKey, metafieldKeysVariables };
|
|
@@ -22,5 +22,8 @@ export declare const fetchProductRecommendations: (data: {
|
|
|
22
22
|
* swatches switch the card to it. Needs `includeOptions`. */
|
|
23
23
|
includeVariants?: boolean;
|
|
24
24
|
locale?: string;
|
|
25
|
+
/** Storefront-visible metafields (`"namespace.key"`) each card carries, for
|
|
26
|
+
* the `closest_product_metafield` sources bound on it. Omitted ⇒ none. */
|
|
27
|
+
metafieldKeys?: readonly string[];
|
|
25
28
|
}) => Promise<RecommendationsData>;
|
|
26
29
|
//#endregion
|
package/dist/recommendations.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { memoTTL } from "./cache.js";
|
|
2
2
|
import { graphql, request } from "./graphql.js";
|
|
3
3
|
import { getContentVersion } from "./content-version.js";
|
|
4
|
+
import { metafieldKeysKey, metafieldKeysVariables } from "./product.js";
|
|
4
5
|
import { CollectionProductNodeFragment, toCollectionProduct } from "./collection.js";
|
|
5
6
|
//#region src/recommendations.ts
|
|
6
7
|
const FetchProductRecommendationsQuery = graphql(`
|
|
@@ -10,6 +11,9 @@ const FetchProductRecommendationsQuery = graphql(`
|
|
|
10
11
|
$includeOptions: Boolean!
|
|
11
12
|
$includeVariants: Boolean!
|
|
12
13
|
$mediaFirst: Int!
|
|
14
|
+
$withMetafields: Boolean = false
|
|
15
|
+
$metafieldFirst: Int = 0
|
|
16
|
+
$metafieldKeys: [MetafieldKeyInput!] = []
|
|
13
17
|
) {
|
|
14
18
|
reference: product(handle: $productHandle) {
|
|
15
19
|
id
|
|
@@ -21,13 +25,14 @@ const FetchProductRecommendationsQuery = graphql(`
|
|
|
21
25
|
`, [CollectionProductNodeFragment]);
|
|
22
26
|
/** A recommendation card's media: its image and the ones a hover gallery shows. */
|
|
23
27
|
const MEDIA_FIRST = 5;
|
|
24
|
-
const fetchRecommendationsCached = memoTTL(async ({ productHandle, intent, includeOptions, includeVariants, locale }) => {
|
|
28
|
+
const fetchRecommendationsCached = memoTTL(async ({ productHandle, intent, includeOptions, includeVariants, locale, metafieldKeys }) => {
|
|
25
29
|
const res = await request(FetchProductRecommendationsQuery, {
|
|
26
30
|
productHandle,
|
|
27
31
|
intent,
|
|
28
32
|
includeOptions,
|
|
29
33
|
includeVariants,
|
|
30
|
-
mediaFirst: MEDIA_FIRST
|
|
34
|
+
mediaFirst: MEDIA_FIRST,
|
|
35
|
+
...metafieldKeysVariables(metafieldKeys)
|
|
31
36
|
}, locale ? { acceptLanguage: locale } : void 0);
|
|
32
37
|
const products = res.productRecommendations.map(toCollectionProduct);
|
|
33
38
|
return {
|
|
@@ -37,7 +42,7 @@ const fetchRecommendationsCached = memoTTL(async ({ productHandle, intent, inclu
|
|
|
37
42
|
products
|
|
38
43
|
};
|
|
39
44
|
}, {
|
|
40
|
-
key: ({ productHandle, intent, includeOptions, includeVariants, locale }) => `${intent}|${productHandle}|${includeOptions}|${includeVariants}|${locale ?? ""}`,
|
|
45
|
+
key: ({ productHandle, intent, includeOptions, includeVariants, locale, metafieldKeys }) => `${intent}|${productHandle}|${includeOptions}|${includeVariants}|${locale ?? ""}|${metafieldKeysKey(metafieldKeys)}`,
|
|
41
46
|
version: getContentVersion
|
|
42
47
|
});
|
|
43
48
|
/** Server fetch for the storefront `productRecommendations` field. Returns an
|