@decocms/apps 0.23.3 → 0.25.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 (111) 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 +24 -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/productListShelf.ts +159 -0
  64. package/vtex/inline-loaders/productListingPage.ts +10 -34
  65. package/vtex/inline-loaders/relatedProducts.ts +1 -3
  66. package/vtex/inline-loaders/suggestions.ts +36 -39
  67. package/vtex/inline-loaders/workflowProducts.ts +45 -49
  68. package/vtex/invoke.ts +159 -194
  69. package/vtex/loaders/address.ts +49 -54
  70. package/vtex/loaders/brands.ts +19 -26
  71. package/vtex/loaders/cart.ts +24 -21
  72. package/vtex/loaders/catalog.ts +51 -53
  73. package/vtex/loaders/collections.ts +25 -27
  74. package/vtex/loaders/legacy.ts +487 -534
  75. package/vtex/loaders/logistics.ts +33 -37
  76. package/vtex/loaders/navbar.ts +5 -8
  77. package/vtex/loaders/orders.ts +28 -39
  78. package/vtex/loaders/pageType.ts +41 -35
  79. package/vtex/loaders/payment.ts +27 -37
  80. package/vtex/loaders/profile.ts +38 -38
  81. package/vtex/loaders/promotion.ts +5 -8
  82. package/vtex/loaders/search.ts +56 -59
  83. package/vtex/loaders/session.ts +22 -30
  84. package/vtex/loaders/user.ts +39 -41
  85. package/vtex/loaders/wishlist.ts +35 -35
  86. package/vtex/loaders/wishlistProducts.ts +3 -15
  87. package/vtex/loaders/workflow.ts +220 -227
  88. package/vtex/middleware.ts +116 -119
  89. package/vtex/types.ts +201 -201
  90. package/vtex/utils/batch.ts +13 -16
  91. package/vtex/utils/cookies.ts +76 -80
  92. package/vtex/utils/enrichment.ts +62 -42
  93. package/vtex/utils/fetchCache.ts +1 -4
  94. package/vtex/utils/index.ts +6 -6
  95. package/vtex/utils/intelligentSearch.ts +48 -57
  96. package/vtex/utils/legacy.ts +108 -124
  97. package/vtex/utils/pickAndOmit.ts +15 -20
  98. package/vtex/utils/proxy.ts +136 -146
  99. package/vtex/utils/resourceRange.ts +3 -3
  100. package/vtex/utils/segment.ts +100 -111
  101. package/vtex/utils/similars.ts +1 -2
  102. package/vtex/utils/sitemap.ts +91 -91
  103. package/vtex/utils/slugCache.ts +2 -6
  104. package/vtex/utils/slugify.ts +9 -9
  105. package/vtex/utils/transform.ts +1178 -1105
  106. package/vtex/utils/types.ts +1381 -1381
  107. package/vtex/utils/vtexId.ts +44 -47
  108. package/.github/workflows/release.yml +0 -34
  109. package/.releaserc.json +0 -28
  110. package/knip.json +0 -19
  111. package/tsconfig.json +0 -11
@@ -32,6 +32,28 @@ import { pickSku, toInventories, toProduct, toReview } from "./transform";
32
32
  import type { LegacyProduct } from "./types";
33
33
  import { buildAuthCookieHeader, VTEX_AUTH_COOKIE } from "./vtexId";
34
34
 
35
+ // -------------------------------------------------------------------------
36
+ // Constants
37
+ // -------------------------------------------------------------------------
38
+
39
+ /** VTEX prices come in cents — divide by this to get the currency value. */
40
+ const CENTS_DIVISOR = 100;
41
+
42
+ /** Default number of products per simulation API call. */
43
+ const DEFAULT_SIMULATION_BATCH_SIZE = 50;
44
+
45
+ /** Maximum wishlist items to fetch in a single query. */
46
+ const WISHLIST_MAX_ITEMS = 500;
47
+
48
+ /** Batch size for kit-item product lookups. */
49
+ const KIT_ITEMS_BATCH_SIZE = 10;
50
+
51
+ /** Batch size for variant product lookups. */
52
+ const VARIANTS_BATCH_SIZE = 15;
53
+
54
+ /** Number of reviews to fetch per product. */
55
+ const REVIEWS_PAGE_SIZE = 10;
56
+
35
57
  // -------------------------------------------------------------------------
36
58
  // Types
37
59
  // -------------------------------------------------------------------------
@@ -47,10 +69,7 @@ export interface EnrichmentContext {
47
69
  * A product enricher takes a list of products and returns an enriched list.
48
70
  * Enrichers are composed via `createProductPipeline`.
49
71
  */
50
- export type ProductEnricher = (
51
- products: Product[],
52
- ctx: EnrichmentContext,
53
- ) => Promise<Product[]>;
72
+ export type ProductEnricher = (products: Product[], ctx: EnrichmentContext) => Promise<Product[]>;
54
73
 
55
74
  // -------------------------------------------------------------------------
56
75
  // Pipeline
@@ -63,9 +82,7 @@ export type ProductEnricher = (
63
82
  * This is intentional: some enrichers depend on previous enrichments
64
83
  * (e.g., wishlist may need SKU IDs added by simulation).
65
84
  */
66
- export function createProductPipeline(
67
- ...enrichers: ProductEnricher[]
68
- ): ProductEnricher {
85
+ export function createProductPipeline(...enrichers: ProductEnricher[]): ProductEnricher {
69
86
  return async (products, ctx) => {
70
87
  if (!products.length) return products;
71
88
 
@@ -115,20 +132,15 @@ interface SimulationResult {
115
132
  *
116
133
  * @param options.batchSize - Max products per simulation call. @default 50
117
134
  */
118
- export function withSimulation(options?: {
119
- batchSize?: number;
120
- }): ProductEnricher {
121
- const batchSize = options?.batchSize ?? 50;
135
+ export function withSimulation(options?: { batchSize?: number }): ProductEnricher {
136
+ const batchSize = options?.batchSize ?? DEFAULT_SIMULATION_BATCH_SIZE;
122
137
 
123
138
  return async (products, ctx) => {
124
139
  const config = getVtexConfig();
125
140
  const sc = ctx.salesChannel ?? config.salesChannel ?? "1";
126
141
 
127
142
  const skuItems: SimulationItem[] = [];
128
- const skuToProductIndex = new Map<
129
- string,
130
- { productIdx: number; offerIdx: number }
131
- >();
143
+ const skuToProductIndex = new Map<string, { productIdx: number; offerIdx: number }>();
132
144
 
133
145
  for (let pi = 0; pi < products.length; pi++) {
134
146
  const product = products[pi];
@@ -191,7 +203,7 @@ export function withSimulation(options?: {
191
203
  const offers = [...aggOffer.offers];
192
204
  const offer = { ...offers[mapping.offerIdx] };
193
205
 
194
- offer.price = simItem.sellingPrice / 100;
206
+ offer.price = simItem.sellingPrice / CENTS_DIVISOR;
195
207
  if (simItem.listPrice) {
196
208
  (offer as any).priceSpecification = [
197
209
  ...(Array.isArray((offer as any).priceSpecification)
@@ -199,10 +211,10 @@ export function withSimulation(options?: {
199
211
  : []),
200
212
  ].map((spec: any) => {
201
213
  if (spec?.priceType === "https://schema.org/ListPrice") {
202
- return { ...spec, price: simItem.listPrice / 100 };
214
+ return { ...spec, price: simItem.listPrice / CENTS_DIVISOR };
203
215
  }
204
216
  if (spec?.priceType === "https://schema.org/SalePrice") {
205
- return { ...spec, price: simItem.sellingPrice / 100 };
217
+ return { ...spec, price: simItem.sellingPrice / CENTS_DIVISOR };
206
218
  }
207
219
  return spec;
208
220
  });
@@ -217,10 +229,7 @@ export function withSimulation(options?: {
217
229
  result[mapping.productIdx] = product;
218
230
  }
219
231
  } catch (error) {
220
- console.error(
221
- "[Simulation] Batch failed:",
222
- error instanceof Error ? error.message : error,
223
- );
232
+ console.error("[Simulation] Batch failed:", error instanceof Error ? error.message : error);
224
233
  }
225
234
  }
226
235
 
@@ -275,9 +284,7 @@ export function withWishlist(): ProductEnricher {
275
284
  try {
276
285
  const parts = authCookie.split(".");
277
286
  if (parts.length === 3) {
278
- const payload = JSON.parse(
279
- atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")),
280
- );
287
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
281
288
  email = payload.sub ?? payload.userId;
282
289
  }
283
290
  } catch {
@@ -290,7 +297,7 @@ export function withWishlist(): ProductEnricher {
290
297
  const data = await vtexIOGraphQL<WishlistData>(
291
298
  {
292
299
  query: WISHLIST_QUERY,
293
- variables: { shopperId: email, name: "Wishlist", from: 0, to: 500 },
300
+ variables: { shopperId: email, name: "Wishlist", from: 0, to: WISHLIST_MAX_ITEMS },
294
301
  },
295
302
  { Cookie: buildAuthCookieHeader(authCookie, getVtexConfig().account) },
296
303
  );
@@ -369,7 +376,7 @@ export function withKitItems(): ProductEnricher {
369
376
  ? `https://${config.publicUrl}`
370
377
  : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
371
378
 
372
- const batches = batch([...productIDs], 10);
379
+ const batches = batch([...productIDs], KIT_ITEMS_BATCH_SIZE);
373
380
  const productsById = new Map<string, ProductLeaf>();
374
381
 
375
382
  for (const ids of batches) {
@@ -389,10 +396,7 @@ export function withKitItems(): ProductEnricher {
389
396
  }
390
397
  }
391
398
  } catch (e) {
392
- console.error(
393
- "[KitItems] Batch failed:",
394
- e instanceof Error ? e.message : e,
395
- );
399
+ console.error("[KitItems] Batch failed:", e instanceof Error ? e.message : e);
396
400
  }
397
401
  }
398
402
 
@@ -428,7 +432,7 @@ export function withVariants(): ProductEnricher {
428
432
  ? `https://${config.publicUrl}`
429
433
  : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
430
434
 
431
- const batches = batch([...productIDs], 15);
435
+ const batches = batch([...productIDs], VARIANTS_BATCH_SIZE);
432
436
  const productsById = new Map<string, Product>();
433
437
 
434
438
  for (const ids of batches) {
@@ -446,10 +450,7 @@ export function withVariants(): ProductEnricher {
446
450
  productsById.set(product.productID, product);
447
451
  }
448
452
  } catch (e) {
449
- console.error(
450
- "[Variants] Batch failed:",
451
- e instanceof Error ? e.message : e,
452
- );
453
+ console.error("[Variants] Batch failed:", e instanceof Error ? e.message : e);
453
454
  }
454
455
  }
455
456
 
@@ -476,14 +477,28 @@ export function withReviews(): ProductEnricher {
476
477
 
477
478
  const reviewPromises = products.map((product) =>
478
479
  vtexFetch<any>(
479
- `https://${myHost}/reviews-and-ratings/api/reviews?product_id=${product.inProductGroupWithID ?? ""}&from=0&to=10&status=true`,
480
- ).catch(() => ({})),
480
+ `https://${myHost}/reviews-and-ratings/api/reviews?product_id=${product.inProductGroupWithID ?? ""}&from=0&to=${REVIEWS_PAGE_SIZE}&status=true`,
481
+ ).catch((error) => {
482
+ console.error(
483
+ "[Reviews] Failed for product",
484
+ product.inProductGroupWithID,
485
+ error instanceof Error ? error.message : error,
486
+ );
487
+ return {};
488
+ }),
481
489
  );
482
490
 
483
491
  const ratingPromises = products.map((product) =>
484
492
  vtexFetch<any>(
485
493
  `https://${myHost}/reviews-and-ratings/api/rating/${product.inProductGroupWithID ?? ""}`,
486
- ).catch(() => ({})),
494
+ ).catch((error) => {
495
+ console.error(
496
+ "[Ratings] Failed for product",
497
+ product.inProductGroupWithID,
498
+ error instanceof Error ? error.message : error,
499
+ );
500
+ return {};
501
+ }),
487
502
  );
488
503
 
489
504
  const [reviews, ratings] = await Promise.all([
@@ -508,9 +523,14 @@ export function withInventory(): ProductEnricher {
508
523
  const inventories = await Promise.all(
509
524
  products.map((product) => {
510
525
  if (!product.sku) return Promise.resolve({});
511
- return vtexFetch<any>(
512
- `/api/logistics/pvt/inventory/skus/${product.sku}`,
513
- ).catch(() => ({}));
526
+ return vtexFetch<any>(`/api/logistics/pvt/inventory/skus/${product.sku}`).catch((error) => {
527
+ console.error(
528
+ "[Inventory] Failed for SKU",
529
+ product.sku,
530
+ error instanceof Error ? error.message : error,
531
+ );
532
+ return {};
533
+ });
514
534
  }),
515
535
  );
516
536
 
@@ -36,9 +36,7 @@ const inflight = new Map<string, Promise<CacheEntry>>();
36
36
 
37
37
  function evictIfNeeded() {
38
38
  if (store.size <= DEFAULT_MAX_ENTRIES) return;
39
- const sorted = [...store.entries()].sort(
40
- (a, b) => a[1].createdAt - b[1].createdAt,
41
- );
39
+ const sorted = [...store.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
42
40
  const toRemove = sorted.slice(0, store.size - DEFAULT_MAX_ENTRIES);
43
41
  for (const [key] of toRemove) store.delete(key);
44
42
  }
@@ -92,7 +90,6 @@ async function executeFetch(
92
90
  `[vtex-fetch] attempt ${attempt + 1}/${attempts} failed — ${url}: ${lastError.message}`,
93
91
  );
94
92
  await sleep(RETRY_DELAYS[attempt] ?? 400);
95
- continue;
96
93
  }
97
94
  }
98
95
  }
@@ -1,17 +1,17 @@
1
1
  export * from "./batch";
2
2
  export * from "./cookies";
3
+ export * from "./enrichment";
4
+ export * from "./fetchCache";
3
5
  export * from "./intelligentSearch";
4
6
  export * from "./legacy";
5
7
  export * from "./pickAndOmit";
8
+ export * from "./proxy";
9
+ export * from "./resourceRange";
6
10
  export * from "./segment";
7
11
  export * from "./similars";
12
+ export * from "./sitemap";
13
+ export * from "./slugCache";
8
14
  export * from "./slugify";
9
15
  export * from "./transform";
10
16
  export * from "./types";
11
- export * from "./proxy";
12
17
  export * from "./vtexId";
13
- export * from "./sitemap";
14
- export * from "./enrichment";
15
- export * from "./resourceRange";
16
- export * from "./fetchCache";
17
- export * from "./slugCache";
@@ -1,84 +1,75 @@
1
1
  import { vtexFetch } from "../client";
2
- import type {
3
- PageType,
4
- SelectedFacet,
5
- SimulationBehavior,
6
- Sort,
7
- } from "./types";
2
+ import type { PageType, SelectedFacet, SimulationBehavior, Sort } from "./types";
8
3
 
9
4
  export const SESSION_COOKIE = "vtex_is_session";
10
5
  export const ANONYMOUS_COOKIE = "vtex_is_anonymous";
11
6
 
12
- export const withDefaultFacets = (
13
- allFacets: readonly SelectedFacet[],
14
- ) => {
15
- return [...allFacets];
7
+ export const withDefaultFacets = (allFacets: readonly SelectedFacet[]) => {
8
+ return [...allFacets];
16
9
  };
17
10
 
18
11
  export const toPath = (facets: SelectedFacet[]) =>
19
- facets.map(({ key, value }) => key ? `${key}/${value}` : value).join("/");
12
+ facets.map(({ key, value }) => (key ? `${key}/${value}` : value)).join("/");
20
13
 
21
14
  interface Params {
22
- query: string;
23
- page: number;
24
- count: number;
25
- sort: Sort;
26
- fuzzy: string;
27
- locale: string;
28
- hideUnavailableItems: boolean;
29
- simulationBehavior: SimulationBehavior;
15
+ query: string;
16
+ page: number;
17
+ count: number;
18
+ sort: Sort;
19
+ fuzzy: string;
20
+ locale: string;
21
+ hideUnavailableItems: boolean;
22
+ simulationBehavior: SimulationBehavior;
30
23
  }
31
24
 
32
25
  export const withDefaultParams = ({
33
- query = "",
34
- page = 0,
35
- count = 12,
36
- sort = "",
37
- fuzzy = "auto",
38
- locale = "pt-BR",
39
- hideUnavailableItems,
40
- simulationBehavior = "default",
26
+ query = "",
27
+ page = 0,
28
+ count = 12,
29
+ sort = "",
30
+ fuzzy = "auto",
31
+ locale = "pt-BR",
32
+ hideUnavailableItems,
33
+ simulationBehavior = "default",
41
34
  }: Partial<Params>) => ({
42
- page: page + 1,
43
- count,
44
- query,
45
- sort,
46
- ...(fuzzy ? { fuzzy } : {}),
47
- locale,
48
- hideUnavailableItems: hideUnavailableItems ?? false,
49
- simulationBehavior,
35
+ page: page + 1,
36
+ count,
37
+ query,
38
+ sort,
39
+ ...(fuzzy ? { fuzzy } : {}),
40
+ locale,
41
+ hideUnavailableItems: hideUnavailableItems ?? false,
42
+ simulationBehavior,
50
43
  });
51
44
 
52
- export const isFilterParam = (keyFilter: string): boolean =>
53
- keyFilter.startsWith("filter.");
45
+ export const isFilterParam = (keyFilter: string): boolean => keyFilter.startsWith("filter.");
54
46
 
55
47
  const segmentsFromTerm = (term: string) => term.split("/").filter(Boolean);
56
48
 
57
49
  const segmentsFromSearchParams = (url: string) => {
58
- const searchParams = new URLSearchParams(url).entries();
50
+ const searchParams = new URLSearchParams(url).entries();
59
51
 
60
- const categories = Array.from(searchParams).sort()
61
- .reduce((acc, [key, value]) => {
62
- if (key.includes("filter.category")) {
63
- acc.push(value);
64
- }
52
+ const categories = Array.from(searchParams)
53
+ .sort()
54
+ .reduce((acc, [key, value]) => {
55
+ if (key.includes("filter.category")) {
56
+ acc.push(value);
57
+ }
65
58
 
66
- return acc;
67
- }, [] as string[]);
59
+ return acc;
60
+ }, [] as string[]);
68
61
 
69
- return categories.length ? categories : segmentsFromTerm(url);
62
+ return categories.length ? categories : segmentsFromTerm(url);
70
63
  };
71
64
 
72
- export const pageTypesFromUrl = async (
73
- url: string,
74
- ): Promise<PageType[]> => {
75
- const segments = segmentsFromSearchParams(url);
65
+ export const pageTypesFromUrl = async (url: string): Promise<PageType[]> => {
66
+ const segments = segmentsFromSearchParams(url);
76
67
 
77
- return await Promise.all(
78
- segments.map((_, index) =>
79
- vtexFetch<PageType>(
80
- `/api/catalog_system/pub/portal/pagetype/${segments.slice(0, index + 1).join("/")}`,
81
- )
82
- ),
83
- );
68
+ return await Promise.all(
69
+ segments.map((_, index) =>
70
+ vtexFetch<PageType>(
71
+ `/api/catalog_system/pub/portal/pagetype/${segments.slice(0, index + 1).join("/")}`,
72
+ ),
73
+ ),
74
+ );
84
75
  };
@@ -1,155 +1,139 @@
1
1
  import type { Seo } from "../../commerce/types/commerce";
2
2
  import { vtexFetch } from "../client";
3
+ import type { WrappedSegment } from "./segment";
3
4
  import { slugify } from "./slugify";
4
5
  import type { PageType } from "./types";
5
- import { WrappedSegment } from "./segment";
6
6
 
7
7
  const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
8
8
 
9
- export const toSegmentParams = (
10
- { payload: segment }: WrappedSegment,
11
- ) => (Object.fromEntries(
12
- Object.entries({
13
- utmi_campaign: segment.utmi_campaign ?? undefined,
14
- utm_campaign: segment.utm_campaign ?? undefined,
15
- utm_source: segment.utm_source ?? undefined,
16
- sc: segment.channel ?? undefined,
17
- }).filter(([_, v]) => v != undefined),
18
- ));
9
+ export const toSegmentParams = ({ payload: segment }: WrappedSegment) =>
10
+ Object.fromEntries(
11
+ Object.entries({
12
+ utmi_campaign: segment.utmi_campaign ?? undefined,
13
+ utm_campaign: segment.utm_campaign ?? undefined,
14
+ utm_source: segment.utm_source ?? undefined,
15
+ sc: segment.channel ?? undefined,
16
+ }).filter(([_, v]) => v !== undefined),
17
+ );
19
18
 
20
19
  const PAGE_TYPE_TO_MAP_PARAM = {
21
- Brand: "b",
22
- Category: "c",
23
- Department: "c",
24
- SubCategory: "c",
25
- Collection: "productClusterIds",
26
- Cluster: "productClusterIds",
27
- Search: "ft",
28
- FullText: "ft",
29
- Product: "p",
30
- NotFound: null,
20
+ Brand: "b",
21
+ Category: "c",
22
+ Department: "c",
23
+ SubCategory: "c",
24
+ Collection: "productClusterIds",
25
+ Cluster: "productClusterIds",
26
+ Search: "ft",
27
+ FullText: "ft",
28
+ Product: "p",
29
+ NotFound: null,
31
30
  };
32
31
 
33
32
  const segmentsFromTerm = (term: string) => term.split("/").filter(Boolean);
34
33
 
35
34
  export const getValidTypesFromPageTypes = (pagetypes: PageType[]) => {
36
- return pagetypes
37
- .filter((type) => PAGE_TYPE_TO_MAP_PARAM[type.pageType]);
35
+ return pagetypes.filter((type) => PAGE_TYPE_TO_MAP_PARAM[type.pageType]);
38
36
  };
39
37
 
40
- export const pageTypesFromPathname = async (
41
- term: string,
42
- ): Promise<PageType[]> => {
43
- const segments = segmentsFromTerm(term);
44
-
45
- return await Promise.all(
46
- segments.map((_, index) =>
47
- vtexFetch<PageType>(
48
- `/api/catalog_system/pub/portal/pagetype/${segments.slice(0, index + 1).join("/")}`,
49
- )
50
- ),
51
- );
38
+ export const pageTypesFromPathname = async (term: string): Promise<PageType[]> => {
39
+ const segments = segmentsFromTerm(term);
40
+
41
+ return await Promise.all(
42
+ segments.map((_, index) =>
43
+ vtexFetch<PageType>(
44
+ `/api/catalog_system/pub/portal/pagetype/${segments.slice(0, index + 1).join("/")}`,
45
+ ),
46
+ ),
47
+ );
52
48
  };
53
49
 
54
- export const getMapAndTerm = (
55
- pageTypes: PageType[],
56
- ) => {
57
- const term = pageTypes
58
- .map((type, index) =>
59
- type.url
60
- ? segmentsFromTerm(
61
- new URL(`http://${type.url}`).pathname,
62
- )[index]
63
- : null
64
- )
65
- .filter(Boolean)
66
- .join("/");
67
-
68
- const map = pageTypes
69
- .map((type) => PAGE_TYPE_TO_MAP_PARAM[type.pageType])
70
- .filter(Boolean)
71
- .join(",");
72
-
73
- if (map === "ft" && term === "s") {
74
- return ["", ""];
75
- }
76
-
77
- return [map, term];
50
+ export const getMapAndTerm = (pageTypes: PageType[]) => {
51
+ const term = pageTypes
52
+ .map((type, index) =>
53
+ type.url ? segmentsFromTerm(new URL(`http://${type.url}`).pathname)[index] : null,
54
+ )
55
+ .filter(Boolean)
56
+ .join("/");
57
+
58
+ const map = pageTypes
59
+ .map((type) => PAGE_TYPE_TO_MAP_PARAM[type.pageType])
60
+ .filter(Boolean)
61
+ .join(",");
62
+
63
+ if (map === "ft" && term === "s") {
64
+ return ["", ""];
65
+ }
66
+
67
+ return [map, term];
78
68
  };
79
69
 
80
- export const pageTypesToBreadcrumbList = (
81
- pages: PageType[],
82
- baseUrl: string,
83
- ) => {
84
- const filteredPages = pages
85
- .filter(({ pageType }) =>
86
- pageType === "Category" || pageType === "Department" ||
87
- pageType === "SubCategory"
88
- );
89
-
90
- return filteredPages.map((page, index) => {
91
- const position = index + 1;
92
- const slug = filteredPages.slice(0, position).map((x) => slugify(x.name!));
93
-
94
- return ({
95
- "@type": "ListItem" as const,
96
- name: page.name!,
97
- item: new URL(`/${slug.join("/")}`, baseUrl).href,
98
- position,
99
- });
100
- });
70
+ export const pageTypesToBreadcrumbList = (pages: PageType[], baseUrl: string) => {
71
+ const filteredPages = pages.filter(
72
+ ({ pageType }) =>
73
+ pageType === "Category" || pageType === "Department" || pageType === "SubCategory",
74
+ );
75
+
76
+ return filteredPages.map((page, index) => {
77
+ const position = index + 1;
78
+ const slug = filteredPages.slice(0, position).map((x) => slugify(x.name!));
79
+
80
+ return {
81
+ "@type": "ListItem" as const,
82
+ name: page.name!,
83
+ item: new URL(`/${slug.join("/")}`, baseUrl).href,
84
+ position,
85
+ };
86
+ });
101
87
  };
102
88
 
103
89
  export const pageTypesToSeo = (
104
- pages: PageType[],
105
- baseUrl: string,
106
- currentPage?: number,
90
+ pages: PageType[],
91
+ baseUrl: string,
92
+ currentPage?: number,
107
93
  ): Seo | null => {
108
- const current = pages.at(-1);
109
- const url = new URL(baseUrl);
110
- const fullTextSearch = url.searchParams.get("q");
111
- const hasMapTermOrSkuId =
112
- !!(url.searchParams.get("map") || url.searchParams.get("skuId"));
113
-
114
- if (
115
- (!current || current.pageType === "Search" ||
116
- current.pageType === "FullText") && fullTextSearch
117
- ) {
118
- return {
119
- title: capitalize(fullTextSearch),
120
- description: capitalize(fullTextSearch),
121
- canonical: url.href,
122
- noIndexing: hasMapTermOrSkuId,
123
- };
124
- }
125
-
126
- if (!current) {
127
- return null;
128
- }
129
-
130
- return {
131
- title: current.title || current.name || "",
132
- description: current.metaTagDescription!,
133
- noIndexing: hasMapTermOrSkuId,
134
- canonical: toCanonical(
135
- new URL(
136
- (current.url && current.pageType !== "Collection")
137
- ? current.url.replace(/^[^/]*\//, "/")
138
- .toLowerCase()
139
- : url,
140
- url,
141
- ),
142
- currentPage,
143
- ),
144
- };
94
+ const current = pages.at(-1);
95
+ const url = new URL(baseUrl);
96
+ const fullTextSearch = url.searchParams.get("q");
97
+ const hasMapTermOrSkuId = !!(url.searchParams.get("map") || url.searchParams.get("skuId"));
98
+
99
+ if (
100
+ (!current || current.pageType === "Search" || current.pageType === "FullText") &&
101
+ fullTextSearch
102
+ ) {
103
+ return {
104
+ title: capitalize(fullTextSearch),
105
+ description: capitalize(fullTextSearch),
106
+ canonical: url.href,
107
+ noIndexing: hasMapTermOrSkuId,
108
+ };
109
+ }
110
+
111
+ if (!current) {
112
+ return null;
113
+ }
114
+
115
+ return {
116
+ title: current.title || current.name || "",
117
+ description: current.metaTagDescription!,
118
+ noIndexing: hasMapTermOrSkuId,
119
+ canonical: toCanonical(
120
+ new URL(
121
+ current.url && current.pageType !== "Collection"
122
+ ? current.url.replace(/^[^/]*\//, "/").toLowerCase()
123
+ : url,
124
+ url,
125
+ ),
126
+ currentPage,
127
+ ),
128
+ };
145
129
  };
146
130
 
147
131
  function toCanonical(url: URL, page?: number) {
148
- if (typeof page === "number") {
149
- url.searchParams.set("page", `${page}`);
150
- }
132
+ if (typeof page === "number") {
133
+ url.searchParams.set("page", `${page}`);
134
+ }
151
135
 
152
- return url.href;
136
+ return url.href;
153
137
  }
154
138
 
155
139
  export { isFilterParam } from "./intelligentSearch";