@decocms/apps 0.23.2 → 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.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/commerce/components/Image.tsx +129 -143
  3. package/commerce/components/JsonLd.tsx +192 -201
  4. package/commerce/components/Picture.tsx +65 -75
  5. package/commerce/sdk/analytics.ts +15 -15
  6. package/commerce/sdk/formatPrice.ts +13 -16
  7. package/commerce/sdk/url.ts +7 -7
  8. package/commerce/sdk/useOffer.ts +46 -57
  9. package/commerce/sdk/useVariantPossibilities.ts +25 -25
  10. package/commerce/types/commerce.ts +868 -875
  11. package/commerce/utils/canonical.ts +5 -8
  12. package/commerce/utils/constants.ts +5 -6
  13. package/commerce/utils/filters.ts +4 -4
  14. package/commerce/utils/productToAnalyticsItem.ts +52 -56
  15. package/commerce/utils/stateByZip.ts +42 -42
  16. package/package.json +23 -4
  17. package/shopify/actions/cart/addItems.ts +24 -25
  18. package/shopify/actions/cart/updateCoupons.ts +19 -20
  19. package/shopify/actions/cart/updateItems.ts +19 -20
  20. package/shopify/actions/user/signIn.ts +25 -30
  21. package/shopify/actions/user/signUp.ts +19 -24
  22. package/shopify/client.ts +24 -24
  23. package/shopify/index.ts +20 -18
  24. package/shopify/init.ts +18 -21
  25. package/shopify/loaders/ProductDetailsPage.ts +16 -20
  26. package/shopify/loaders/ProductList.ts +66 -69
  27. package/shopify/loaders/ProductListingPage.ts +150 -158
  28. package/shopify/loaders/RelatedProducts.ts +24 -27
  29. package/shopify/loaders/cart.ts +53 -52
  30. package/shopify/loaders/shop.ts +22 -27
  31. package/shopify/loaders/user.ts +27 -32
  32. package/shopify/utils/admin/admin.ts +33 -34
  33. package/shopify/utils/admin/queries.ts +2 -2
  34. package/shopify/utils/cart.ts +18 -14
  35. package/shopify/utils/cookies.ts +62 -65
  36. package/shopify/utils/enums.ts +424 -424
  37. package/shopify/utils/graphql.ts +44 -55
  38. package/shopify/utils/storefront/queries.ts +24 -29
  39. package/shopify/utils/storefront/storefront.graphql.gen.ts +55 -55
  40. package/shopify/utils/transform.ts +370 -376
  41. package/shopify/utils/types.ts +118 -118
  42. package/shopify/utils/user.ts +11 -11
  43. package/shopify/utils/utils.ts +135 -140
  44. package/vtex/actions/address.ts +86 -86
  45. package/vtex/actions/auth.ts +14 -27
  46. package/vtex/actions/checkout.ts +36 -49
  47. package/vtex/actions/masterData.ts +10 -27
  48. package/vtex/actions/misc.ts +101 -111
  49. package/vtex/actions/newsletter.ts +48 -52
  50. package/vtex/actions/orders.ts +13 -16
  51. package/vtex/actions/profile.ts +55 -55
  52. package/vtex/actions/session.ts +36 -35
  53. package/vtex/actions/trigger.ts +25 -25
  54. package/vtex/actions/wishlist.ts +51 -53
  55. package/vtex/client.ts +14 -42
  56. package/vtex/hooks/index.ts +4 -4
  57. package/vtex/hooks/useAutocomplete.ts +42 -48
  58. package/vtex/hooks/useCart.ts +153 -165
  59. package/vtex/hooks/useUser.ts +40 -40
  60. package/vtex/hooks/useWishlist.ts +70 -70
  61. package/vtex/inline-loaders/productDetailsPage.ts +1 -3
  62. package/vtex/inline-loaders/productList.ts +121 -127
  63. package/vtex/inline-loaders/productListingPage.ts +10 -34
  64. package/vtex/inline-loaders/relatedProducts.ts +1 -3
  65. package/vtex/inline-loaders/suggestions.ts +36 -39
  66. package/vtex/inline-loaders/workflowProducts.ts +45 -49
  67. package/vtex/invoke.ts +159 -194
  68. package/vtex/loaders/address.ts +49 -54
  69. package/vtex/loaders/brands.ts +19 -26
  70. package/vtex/loaders/cart.ts +24 -21
  71. package/vtex/loaders/catalog.ts +51 -53
  72. package/vtex/loaders/collections.ts +25 -27
  73. package/vtex/loaders/legacy.ts +487 -534
  74. package/vtex/loaders/logistics.ts +33 -37
  75. package/vtex/loaders/navbar.ts +5 -8
  76. package/vtex/loaders/orders.ts +28 -39
  77. package/vtex/loaders/pageType.ts +41 -35
  78. package/vtex/loaders/payment.ts +27 -37
  79. package/vtex/loaders/profile.ts +38 -38
  80. package/vtex/loaders/promotion.ts +5 -8
  81. package/vtex/loaders/search.ts +56 -59
  82. package/vtex/loaders/session.ts +22 -30
  83. package/vtex/loaders/user.ts +39 -41
  84. package/vtex/loaders/wishlist.ts +35 -35
  85. package/vtex/loaders/wishlistProducts.ts +3 -15
  86. package/vtex/loaders/workflow.ts +220 -227
  87. package/vtex/middleware.ts +116 -119
  88. package/vtex/types.ts +201 -201
  89. package/vtex/utils/batch.ts +13 -16
  90. package/vtex/utils/cookies.ts +76 -80
  91. package/vtex/utils/enrichment.ts +62 -42
  92. package/vtex/utils/fetchCache.ts +1 -4
  93. package/vtex/utils/index.ts +6 -6
  94. package/vtex/utils/intelligentSearch.ts +48 -57
  95. package/vtex/utils/legacy.ts +108 -124
  96. package/vtex/utils/pickAndOmit.ts +15 -20
  97. package/vtex/utils/proxy.ts +136 -146
  98. package/vtex/utils/resourceRange.ts +3 -3
  99. package/vtex/utils/segment.ts +100 -111
  100. package/vtex/utils/similars.ts +1 -2
  101. package/vtex/utils/sitemap.ts +91 -91
  102. package/vtex/utils/slugCache.ts +2 -6
  103. package/vtex/utils/slugify.ts +9 -9
  104. package/vtex/utils/transform.ts +1012 -1105
  105. package/vtex/utils/types.ts +1381 -1381
  106. package/vtex/utils/vtexId.ts +44 -47
  107. package/.github/workflows/release.yml +0 -34
  108. package/.releaserc.json +0 -28
  109. package/knip.json +0 -19
  110. package/tsconfig.json +0 -11
@@ -1,11 +1,11 @@
1
1
  import type {
2
- BreadcrumbList,
3
- Filter,
4
- ListItem,
5
- Product,
6
- ProductDetailsPage,
7
- PropertyValue,
8
- UnitPriceSpecification,
2
+ BreadcrumbList,
3
+ Filter,
4
+ ListItem,
5
+ Product,
6
+ ProductDetailsPage,
7
+ PropertyValue,
8
+ UnitPriceSpecification,
9
9
  } from "../../commerce/types/commerce";
10
10
  import { DEFAULT_IMAGE } from "../../commerce/utils/constants";
11
11
 
@@ -14,423 +14,417 @@ type ImageShopify = { url: string; altText?: string | null };
14
14
  type VideoSource = { url: string };
15
15
 
16
16
  interface MediaNode {
17
- alt?: string | null;
18
- previewImage?: ImageShopify | null;
19
- mediaContentType: string;
20
- sources?: VideoSource[];
17
+ alt?: string | null;
18
+ previewImage?: ImageShopify | null;
19
+ mediaContentType: string;
20
+ sources?: VideoSource[];
21
21
  }
22
22
 
23
23
  export type SkuShopify = {
24
- id: string;
25
- title: string;
26
- availableForSale: boolean;
27
- quantityAvailable?: number;
28
- barcode?: string | null;
29
- sku?: string | null;
30
- image?: ImageShopify | null;
31
- price: MoneyV2;
32
- compareAtPrice?: MoneyV2 | null;
33
- selectedOptions: Array<{ name: string; value: string }>;
24
+ id: string;
25
+ title: string;
26
+ availableForSale: boolean;
27
+ quantityAvailable?: number;
28
+ barcode?: string | null;
29
+ sku?: string | null;
30
+ image?: ImageShopify | null;
31
+ price: MoneyV2;
32
+ compareAtPrice?: MoneyV2 | null;
33
+ selectedOptions: Array<{ name: string; value: string }>;
34
34
  };
35
35
 
36
36
  export type CollectionNode = {
37
- handle: string;
38
- title: string;
39
- id?: string;
40
- description?: string;
41
- descriptionHtml?: string;
42
- image?: ImageShopify | null;
37
+ handle: string;
38
+ title: string;
39
+ id?: string;
40
+ description?: string;
41
+ descriptionHtml?: string;
42
+ image?: ImageShopify | null;
43
43
  };
44
44
 
45
45
  export type ProductShopify = {
46
- id: string;
47
- handle: string;
48
- title: string;
49
- description: string;
50
- descriptionHtml?: string;
51
- createdAt?: string;
52
- tags: string[];
53
- vendor: string;
54
- productType: string;
55
- seo?: { title?: string | null; description?: string | null };
56
- images: { nodes: ImageShopify[] };
57
- media: { nodes: MediaNode[] };
58
- variants: { nodes: SkuShopify[] };
59
- collections?: { nodes: CollectionNode[] };
60
- metafields?: Array<{
61
- key: string;
62
- value: string;
63
- namespace: string;
64
- type: string;
65
- description?: string | null;
66
- reference?: { image?: { url: string } } | null;
67
- references?: {
68
- edges: Array<{ node: { image?: { url: string } } }>;
69
- } | null;
70
- } | null>;
46
+ id: string;
47
+ handle: string;
48
+ title: string;
49
+ description: string;
50
+ descriptionHtml?: string;
51
+ createdAt?: string;
52
+ tags: string[];
53
+ vendor: string;
54
+ productType: string;
55
+ seo?: { title?: string | null; description?: string | null };
56
+ images: { nodes: ImageShopify[] };
57
+ media: { nodes: MediaNode[] };
58
+ variants: { nodes: SkuShopify[] };
59
+ collections?: { nodes: CollectionNode[] };
60
+ metafields?: Array<{
61
+ key: string;
62
+ value: string;
63
+ namespace: string;
64
+ type: string;
65
+ description?: string | null;
66
+ reference?: { image?: { url: string } } | null;
67
+ references?: {
68
+ edges: Array<{ node: { image?: { url: string } } }>;
69
+ } | null;
70
+ } | null>;
71
71
  };
72
72
 
73
73
  type FilterValue = { id: string; label: string; count: number; input: string };
74
74
  type FilterShopify = { id: string; label: string; type: string; values: FilterValue[] };
75
75
 
76
76
  const getPath = ({ handle }: ProductShopify, sku?: SkuShopify) =>
77
- sku
78
- ? `/products/${handle}-${getIdFromVariantId(sku.id)}`
79
- : `/products/${handle}`;
77
+ sku ? `/products/${handle}-${getIdFromVariantId(sku.id)}` : `/products/${handle}`;
80
78
 
81
79
  const getIdFromVariantId = (x: string) => {
82
- const splitted = x.split("/");
83
- return Number(splitted[splitted.length - 1]);
80
+ const splitted = x.split("/");
81
+ return Number(splitted[splitted.length - 1]);
84
82
  };
85
83
 
86
84
  const getVariantIdFromId = (id: number) => `gid://shopify/ProductVariant/${id}`;
87
85
 
88
86
  const nonEmptyArray = <T>(array: T[] | null | undefined) =>
89
- Array.isArray(array) && array.length > 0 ? array : null;
87
+ Array.isArray(array) && array.length > 0 ? array : null;
90
88
 
91
89
  export const toProductPage = (
92
- product: ProductShopify,
93
- url: URL,
94
- maybeSkuId?: number,
90
+ product: ProductShopify,
91
+ url: URL,
92
+ maybeSkuId?: number,
95
93
  ): ProductDetailsPage => {
96
- const skuId = maybeSkuId
97
- ? getVariantIdFromId(maybeSkuId)
98
- : product.variants.nodes[0]?.id;
99
- let sku = product.variants.nodes.find((node) => node.id === skuId);
100
-
101
- if (!sku) {
102
- sku = product.variants.nodes[0];
103
- }
104
-
105
- return {
106
- "@type": "ProductDetailsPage",
107
- breadcrumbList: toBreadcrumbList(product, sku),
108
- product: toProduct(product, sku, url),
109
- seo: {
110
- title: product.seo?.title ?? product.title,
111
- description: product.seo?.description ?? product.description,
112
- canonical: `${url.origin}${getPath(product, sku)}`,
113
- },
114
- };
94
+ const skuId = maybeSkuId ? getVariantIdFromId(maybeSkuId) : product.variants.nodes[0]?.id;
95
+ let sku = product.variants.nodes.find((node) => node.id === skuId);
96
+
97
+ if (!sku) {
98
+ sku = product.variants.nodes[0];
99
+ }
100
+
101
+ return {
102
+ "@type": "ProductDetailsPage",
103
+ breadcrumbList: toBreadcrumbList(product, sku),
104
+ product: toProduct(product, sku, url),
105
+ seo: {
106
+ title: product.seo?.title ?? product.title,
107
+ description: product.seo?.description ?? product.description,
108
+ canonical: `${url.origin}${getPath(product, sku)}`,
109
+ },
110
+ };
115
111
  };
116
112
 
117
113
  export const toBreadcrumbItem = ({
118
- name,
119
- position,
120
- item,
114
+ name,
115
+ position,
116
+ item,
121
117
  }: {
122
- name: string;
123
- position: number;
124
- item: string;
118
+ name: string;
119
+ position: number;
120
+ item: string;
125
121
  }): ListItem => ({
126
- "@type": "ListItem",
127
- name: decodeURI(name),
128
- position,
129
- item,
122
+ "@type": "ListItem",
123
+ name: decodeURI(name),
124
+ position,
125
+ item,
130
126
  });
131
127
 
132
- export const toBreadcrumbList = (
133
- product: ProductShopify,
134
- sku: SkuShopify,
135
- ): BreadcrumbList => {
136
- let list: ListItem[] = [];
137
- const collection = product.collections?.nodes[0];
138
-
139
- if (collection) {
140
- list = [
141
- toBreadcrumbItem({
142
- name: collection.title,
143
- position: 1,
144
- item: `/${collection.handle}`,
145
- }),
146
- toBreadcrumbItem({
147
- name: product.title,
148
- position: 2,
149
- item: getPath(product, sku),
150
- }),
151
- ];
152
- } else {
153
- list = [
154
- toBreadcrumbItem({
155
- name: product.title,
156
- position: 2,
157
- item: getPath(product, sku),
158
- }),
159
- ];
160
- }
161
-
162
- return {
163
- "@type": "BreadcrumbList",
164
- numberOfItems: list.length,
165
- itemListElement: list,
166
- };
128
+ export const toBreadcrumbList = (product: ProductShopify, sku: SkuShopify): BreadcrumbList => {
129
+ let list: ListItem[] = [];
130
+ const collection = product.collections?.nodes[0];
131
+
132
+ if (collection) {
133
+ list = [
134
+ toBreadcrumbItem({
135
+ name: collection.title,
136
+ position: 1,
137
+ item: `/${collection.handle}`,
138
+ }),
139
+ toBreadcrumbItem({
140
+ name: product.title,
141
+ position: 2,
142
+ item: getPath(product, sku),
143
+ }),
144
+ ];
145
+ } else {
146
+ list = [
147
+ toBreadcrumbItem({
148
+ name: product.title,
149
+ position: 2,
150
+ item: getPath(product, sku),
151
+ }),
152
+ ];
153
+ }
154
+
155
+ return {
156
+ "@type": "BreadcrumbList",
157
+ numberOfItems: list.length,
158
+ itemListElement: list,
159
+ };
167
160
  };
168
161
 
169
162
  export const toProduct = (
170
- product: ProductShopify,
171
- sku: SkuShopify,
172
- url: URL,
173
- level = 0,
163
+ product: ProductShopify,
164
+ sku: SkuShopify,
165
+ url: URL,
166
+ level = 0,
174
167
  ): Product => {
175
- const {
176
- createdAt,
177
- description,
178
- images,
179
- media,
180
- id: productGroupID,
181
- variants,
182
- vendor,
183
- productType,
184
- } = product;
185
- const {
186
- id: productID,
187
- barcode,
188
- selectedOptions,
189
- image,
190
- price,
191
- availableForSale,
192
- quantityAvailable,
193
- compareAtPrice,
194
- } = sku;
195
-
196
- const descriptionHtml: PropertyValue = {
197
- "@type": "PropertyValue",
198
- name: "descriptionHtml",
199
- value: product.descriptionHtml,
200
- };
201
-
202
- const productTypeValue: PropertyValue = {
203
- "@type": "PropertyValue",
204
- name: "productType",
205
- value: productType,
206
- };
207
-
208
- const metafields = (product.metafields ?? [])
209
- .filter((metafield): metafield is NonNullable<typeof metafield> =>
210
- metafield != null && metafield.key != null && metafield.value != null
211
- )
212
- .map((metafield): PropertyValue => {
213
- const { key, value, reference, references } = metafield;
214
- const hasReferenceImage = reference && "image" in reference;
215
- const referenceImageUrl = hasReferenceImage ? reference.image?.url : null;
216
-
217
- const hasEdges = references?.edges && references.edges.length > 0;
218
- const edgeImages = hasEdges
219
- ? references!.edges.map((edge) =>
220
- edge.node && "image" in edge.node ? edge.node.image?.url : null
221
- )
222
- : null;
223
-
224
- const rawValue = referenceImageUrl || edgeImages || value;
225
- const valueToReturn = Array.isArray(rawValue)
226
- ? JSON.stringify(rawValue)
227
- : rawValue ?? undefined;
228
-
229
- return {
230
- "@type": "PropertyValue",
231
- name: key,
232
- value: valueToReturn,
233
- };
234
- });
235
-
236
- const additionalProperty: PropertyValue[] = selectedOptions
237
- .map(toPropertyValue)
238
- .concat(descriptionHtml)
239
- .concat(productTypeValue)
240
- .concat(metafields);
241
-
242
- const skuImages = nonEmptyArray([image]);
243
- const hasVariant = level < 1 &&
244
- variants.nodes.map((variant) => toProduct(product, variant, url, 1));
245
- const priceSpec: UnitPriceSpecification[] = [{
246
- "@type": "UnitPriceSpecification",
247
- priceType: "https://schema.org/SalePrice",
248
- price: Number(price.amount),
249
- }];
250
-
251
- if (compareAtPrice) {
252
- priceSpec.push({
253
- "@type": "UnitPriceSpecification",
254
- priceType: "https://schema.org/ListPrice",
255
- price: Number(compareAtPrice.amount),
256
- });
257
- }
258
-
259
- const collectionNodes = product.collections?.nodes ?? [];
260
-
261
- return {
262
- "@type": "Product",
263
- productID,
264
- url: `${url.origin}${getPath(product, sku)}`,
265
- name: sku.title,
266
- description,
267
- sku: productID,
268
- gtin: barcode ?? undefined,
269
- brand: { "@type": "Brand", name: vendor },
270
- releaseDate: createdAt,
271
- additionalProperty,
272
- isVariantOf: {
273
- "@type": "ProductGroup",
274
- productGroupID,
275
- hasVariant: hasVariant || [],
276
- url: `${url.origin}${getPath(product)}`,
277
- name: product.title,
278
- additionalProperty: [
279
- ...(product.tags ?? []).map((value) =>
280
- toPropertyValue({ name: "TAG", value })
281
- ),
282
- ...collectionNodes.map((col) =>
283
- toPropertyValue({
284
- "@id": col.id,
285
- name: "COLLECTION",
286
- value: col.title,
287
- valueReference: col.handle,
288
- description: col.description,
289
- disambiguatingDescription: col.descriptionHtml,
290
- ...(col.image && {
291
- image: [{
292
- "@type": "ImageObject" as const,
293
- encodingFormat: "image",
294
- alternateName: col.image.altText ?? "",
295
- url: col.image.url,
296
- }],
297
- }),
298
- })
299
- ),
300
- ],
301
- image: nonEmptyArray(images.nodes)?.map((img) => ({
302
- "@type": "ImageObject" as const,
303
- encodingFormat: "image",
304
- alternateName: img.altText ?? "",
305
- url: img.url,
306
- })),
307
- },
308
- image: skuImages?.map((img) => ({
309
- "@type": "ImageObject" as const,
310
- encodingFormat: "image",
311
- alternateName: img?.altText ?? "",
312
- url: img?.url ?? "",
313
- })) ?? [DEFAULT_IMAGE],
314
- video: media.nodes
315
- .filter((m) => m.mediaContentType === "VIDEO")
316
- .map((video) => ({
317
- "@type": "VideoObject" as const,
318
- contentUrl: video.sources?.[0]?.url,
319
- description: video.alt ?? undefined,
320
- thumbnailUrl: video.previewImage?.url,
321
- })),
322
- offers: {
323
- "@type": "AggregateOffer",
324
- priceCurrency: price.currencyCode,
325
- highPrice: compareAtPrice
326
- ? Number(compareAtPrice.amount)
327
- : Number(price.amount),
328
- lowPrice: Number(price.amount),
329
- offerCount: 1,
330
- offers: [{
331
- "@type": "Offer",
332
- price: Number(price.amount),
333
- availability: availableForSale
334
- ? "https://schema.org/InStock"
335
- : "https://schema.org/OutOfStock",
336
- inventoryLevel: { value: quantityAvailable ?? 0 },
337
- priceSpecification: priceSpec,
338
- }],
339
- },
340
- };
168
+ const {
169
+ createdAt,
170
+ description,
171
+ images,
172
+ media,
173
+ id: productGroupID,
174
+ variants,
175
+ vendor,
176
+ productType,
177
+ } = product;
178
+ const {
179
+ id: productID,
180
+ barcode,
181
+ selectedOptions,
182
+ image,
183
+ price,
184
+ availableForSale,
185
+ quantityAvailable,
186
+ compareAtPrice,
187
+ } = sku;
188
+
189
+ const descriptionHtml: PropertyValue = {
190
+ "@type": "PropertyValue",
191
+ name: "descriptionHtml",
192
+ value: product.descriptionHtml,
193
+ };
194
+
195
+ const productTypeValue: PropertyValue = {
196
+ "@type": "PropertyValue",
197
+ name: "productType",
198
+ value: productType,
199
+ };
200
+
201
+ const metafields = (product.metafields ?? [])
202
+ .filter(
203
+ (metafield): metafield is NonNullable<typeof metafield> =>
204
+ metafield != null && metafield.key != null && metafield.value != null,
205
+ )
206
+ .map((metafield): PropertyValue => {
207
+ const { key, value, reference, references } = metafield;
208
+ const hasReferenceImage = reference && "image" in reference;
209
+ const referenceImageUrl = hasReferenceImage ? reference.image?.url : null;
210
+
211
+ const hasEdges = references?.edges && references.edges.length > 0;
212
+ const edgeImages = hasEdges
213
+ ? references!.edges.map((edge) =>
214
+ edge.node && "image" in edge.node ? edge.node.image?.url : null,
215
+ )
216
+ : null;
217
+
218
+ const rawValue = referenceImageUrl || edgeImages || value;
219
+ const valueToReturn = Array.isArray(rawValue)
220
+ ? JSON.stringify(rawValue)
221
+ : (rawValue ?? undefined);
222
+
223
+ return {
224
+ "@type": "PropertyValue",
225
+ name: key,
226
+ value: valueToReturn,
227
+ };
228
+ });
229
+
230
+ const additionalProperty: PropertyValue[] = selectedOptions
231
+ .map(toPropertyValue)
232
+ .concat(descriptionHtml)
233
+ .concat(productTypeValue)
234
+ .concat(metafields);
235
+
236
+ const skuImages = nonEmptyArray([image]);
237
+ const hasVariant =
238
+ level < 1 && variants.nodes.map((variant) => toProduct(product, variant, url, 1));
239
+ const priceSpec: UnitPriceSpecification[] = [
240
+ {
241
+ "@type": "UnitPriceSpecification",
242
+ priceType: "https://schema.org/SalePrice",
243
+ price: Number(price.amount),
244
+ },
245
+ ];
246
+
247
+ if (compareAtPrice) {
248
+ priceSpec.push({
249
+ "@type": "UnitPriceSpecification",
250
+ priceType: "https://schema.org/ListPrice",
251
+ price: Number(compareAtPrice.amount),
252
+ });
253
+ }
254
+
255
+ const collectionNodes = product.collections?.nodes ?? [];
256
+
257
+ return {
258
+ "@type": "Product",
259
+ productID,
260
+ url: `${url.origin}${getPath(product, sku)}`,
261
+ name: sku.title,
262
+ description,
263
+ sku: productID,
264
+ gtin: barcode ?? undefined,
265
+ brand: { "@type": "Brand", name: vendor },
266
+ releaseDate: createdAt,
267
+ additionalProperty,
268
+ isVariantOf: {
269
+ "@type": "ProductGroup",
270
+ productGroupID,
271
+ hasVariant: hasVariant || [],
272
+ url: `${url.origin}${getPath(product)}`,
273
+ name: product.title,
274
+ additionalProperty: [
275
+ ...(product.tags ?? []).map((value) => toPropertyValue({ name: "TAG", value })),
276
+ ...collectionNodes.map((col) =>
277
+ toPropertyValue({
278
+ "@id": col.id,
279
+ name: "COLLECTION",
280
+ value: col.title,
281
+ valueReference: col.handle,
282
+ description: col.description,
283
+ disambiguatingDescription: col.descriptionHtml,
284
+ ...(col.image && {
285
+ image: [
286
+ {
287
+ "@type": "ImageObject" as const,
288
+ encodingFormat: "image",
289
+ alternateName: col.image.altText ?? "",
290
+ url: col.image.url,
291
+ },
292
+ ],
293
+ }),
294
+ }),
295
+ ),
296
+ ],
297
+ image: nonEmptyArray(images.nodes)?.map((img) => ({
298
+ "@type": "ImageObject" as const,
299
+ encodingFormat: "image",
300
+ alternateName: img.altText ?? "",
301
+ url: img.url,
302
+ })),
303
+ },
304
+ image: skuImages?.map((img) => ({
305
+ "@type": "ImageObject" as const,
306
+ encodingFormat: "image",
307
+ alternateName: img?.altText ?? "",
308
+ url: img?.url ?? "",
309
+ })) ?? [DEFAULT_IMAGE],
310
+ video: media.nodes
311
+ .filter((m) => m.mediaContentType === "VIDEO")
312
+ .map((video) => ({
313
+ "@type": "VideoObject" as const,
314
+ contentUrl: video.sources?.[0]?.url,
315
+ description: video.alt ?? undefined,
316
+ thumbnailUrl: video.previewImage?.url,
317
+ })),
318
+ offers: {
319
+ "@type": "AggregateOffer",
320
+ priceCurrency: price.currencyCode,
321
+ highPrice: compareAtPrice ? Number(compareAtPrice.amount) : Number(price.amount),
322
+ lowPrice: Number(price.amount),
323
+ offerCount: 1,
324
+ offers: [
325
+ {
326
+ "@type": "Offer",
327
+ price: Number(price.amount),
328
+ availability: availableForSale
329
+ ? "https://schema.org/InStock"
330
+ : "https://schema.org/OutOfStock",
331
+ inventoryLevel: { value: quantityAvailable ?? 0 },
332
+ priceSpecification: priceSpec,
333
+ },
334
+ ],
335
+ },
336
+ };
341
337
  };
342
338
 
343
- const toPropertyValue = (
344
- option: Omit<PropertyValue, "@type">,
345
- ): PropertyValue => ({
346
- "@type": "PropertyValue",
347
- ...option,
339
+ const toPropertyValue = (option: Omit<PropertyValue, "@type">): PropertyValue => ({
340
+ "@type": "PropertyValue",
341
+ ...option,
348
342
  });
349
343
 
350
344
  const isSelectedFilter = (filterValue: FilterValue, url: URL) => {
351
- let isSelected = false;
352
- const label = getFilterValue(filterValue);
353
-
354
- url.searchParams.forEach((value, key) => {
355
- if (!key?.startsWith("filter")) return;
356
- if (value === label) isSelected = true;
357
- });
358
- return isSelected;
345
+ let isSelected = false;
346
+ const label = getFilterValue(filterValue);
347
+
348
+ url.searchParams.forEach((value, key) => {
349
+ if (!key?.startsWith("filter")) return;
350
+ if (value === label) isSelected = true;
351
+ });
352
+ return isSelected;
359
353
  };
360
354
 
361
355
  export const toFilter = (filter: FilterShopify, url: URL): Filter => {
362
- if (!filter.type.includes("RANGE")) {
363
- return {
364
- "@type": "FilterToggle",
365
- label: filter.label,
366
- key: filter.id,
367
- values: filter.values.map((value) => ({
368
- quantity: value.count,
369
- label: value.label,
370
- value: value.label,
371
- selected: isSelectedFilter(value, url),
372
- url: filtersURL(filter, value, url),
373
- })),
374
- quantity: filter.values.length,
375
- };
376
- } else {
377
- const min = JSON.parse(filter.values[0].input).min;
378
- const max = JSON.parse(filter.values[0].input).max;
379
- return {
380
- "@type": "FilterRange",
381
- label: filter.label,
382
- key: filter.id,
383
- values: { min, max },
384
- };
385
- }
356
+ if (!filter.type.includes("RANGE")) {
357
+ return {
358
+ "@type": "FilterToggle",
359
+ label: filter.label,
360
+ key: filter.id,
361
+ values: filter.values.map((value) => ({
362
+ quantity: value.count,
363
+ label: value.label,
364
+ value: value.label,
365
+ selected: isSelectedFilter(value, url),
366
+ url: filtersURL(filter, value, url),
367
+ })),
368
+ quantity: filter.values.length,
369
+ };
370
+ } else {
371
+ const min = JSON.parse(filter.values[0].input).min;
372
+ const max = JSON.parse(filter.values[0].input).max;
373
+ return {
374
+ "@type": "FilterRange",
375
+ label: filter.label,
376
+ key: filter.id,
377
+ values: { min, max },
378
+ };
379
+ }
386
380
  };
387
381
 
388
382
  const filtersURL = (filter: FilterShopify, value: FilterValue, _url: URL) => {
389
- const url = new URL(_url.href);
390
- const params = new URLSearchParams(url.search);
391
- params.delete("page");
392
- params.delete("startCursor");
393
- params.delete("endCursor");
394
-
395
- const label = getFilterValue(value);
396
-
397
- if (params.has(filter.id, label)) {
398
- params.delete(filter.id, label);
399
- } else {
400
- params.append(filter.id, label);
401
- }
402
-
403
- url.search = params.toString();
404
- return url.toString();
383
+ const url = new URL(_url.href);
384
+ const params = new URLSearchParams(url.search);
385
+ params.delete("page");
386
+ params.delete("startCursor");
387
+ params.delete("endCursor");
388
+
389
+ const label = getFilterValue(value);
390
+
391
+ if (params.has(filter.id, label)) {
392
+ params.delete(filter.id, label);
393
+ } else {
394
+ params.append(filter.id, label);
395
+ }
396
+
397
+ url.search = params.toString();
398
+ return url.toString();
405
399
  };
406
400
 
407
401
  const getFilterValue = (value: FilterValue) => {
408
- try {
409
- const parsed = JSON.parse(value.input);
410
-
411
- const fieldsToCheck = [
412
- ["productMetafield", "value"],
413
- ["taxonomyMetafield", "value"],
414
- ["productVendor"],
415
- ["productType"],
416
- ["category", "id"],
417
- ];
418
-
419
- for (const path of fieldsToCheck) {
420
- let current: any = parsed;
421
- for (const key of path) {
422
- if (current && typeof current === "object" && key in current) {
423
- current = current[key];
424
- } else {
425
- current = null;
426
- break;
427
- }
428
- }
429
- if (current != null) return current;
430
- }
431
- } catch (error) {
432
- console.error("Error parsing input JSON:", error);
433
- }
434
-
435
- return value.label;
402
+ try {
403
+ const parsed = JSON.parse(value.input);
404
+
405
+ const fieldsToCheck = [
406
+ ["productMetafield", "value"],
407
+ ["taxonomyMetafield", "value"],
408
+ ["productVendor"],
409
+ ["productType"],
410
+ ["category", "id"],
411
+ ];
412
+
413
+ for (const path of fieldsToCheck) {
414
+ let current: any = parsed;
415
+ for (const key of path) {
416
+ if (current && typeof current === "object" && key in current) {
417
+ current = current[key];
418
+ } else {
419
+ current = null;
420
+ break;
421
+ }
422
+ }
423
+ if (current != null) return current;
424
+ }
425
+ } catch (error) {
426
+ console.error("Error parsing input JSON:", error);
427
+ }
428
+
429
+ return value.label;
436
430
  };