@decocms/apps-vtex 7.66.4 → 7.66.5

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/apps-vtex",
3
- "version": "7.66.4",
3
+ "version": "7.66.5",
4
4
  "type": "module",
5
5
  "description": "Deco commerce app: VTEX integration",
6
6
  "repository": {
@@ -51,10 +51,10 @@
51
51
  "lint:unused": "knip"
52
52
  },
53
53
  "dependencies": {
54
- "@decocms/blocks": "7.66.4",
55
- "@decocms/apps-commerce": "7.66.4",
56
- "@decocms/apps-website": "7.66.4",
57
- "@decocms/tanstack": "7.66.4"
54
+ "@decocms/blocks": "7.66.5",
55
+ "@decocms/apps-commerce": "7.66.5",
56
+ "@decocms/apps-website": "7.66.5",
57
+ "@decocms/tanstack": "7.66.5"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@tanstack/react-query": ">=5.0.0",
package/src/client.ts CHANGED
@@ -206,6 +206,55 @@ export function vtexHost(environment: string = "vtexcommercestable", config?: Vt
206
206
  return `${c.account}.${environment}.${domain}`;
207
207
  }
208
208
 
209
+ /**
210
+ * Origin every product/SKU/breadcrumb URL in the schema.org payload is built
211
+ * from.
212
+ *
213
+ * Order matters, and the first branch is the whole point: a storefront answers
214
+ * on the host the shopper actually typed, and that is the host its canonical
215
+ * URLs, JSON-LD and sitemap entries have to name. deco-cx/apps got this from
216
+ * the request too (`vtex/loaders/intelligentSearch/productList.ts` passes
217
+ * `baseUrl: url`), and dropping it here is what made a migrated storefront
218
+ * publish structured data pointing somewhere else.
219
+ *
220
+ * `publicUrl` is only a fallback. It is the VTEX *checkout* host
221
+ * (`secure.<brand>.com`), which is a different site from the storefront, and
222
+ * it is stored with whatever shape the admin typed — commonly a full URL
223
+ * (`https://secure.example.com/`). Interpolating that into `https://${...}`
224
+ * produced `https://https://secure.example.com/`, which `new URL()` parses
225
+ * with host `https`, so every product ended up at `https://https/<slug>/p`.
226
+ * Normalising through `URL` removes that whole class of bug.
227
+ *
228
+ * Returns no trailing slash, so `new URL(path, baseUrl)` behaves the same for
229
+ * every branch.
230
+ */
231
+ export function storefrontBaseUrl(config?: VtexConfig): string {
232
+ const c = config ?? getVtexConfig();
233
+
234
+ // 1. The host serving this request -- the storefront's own origin.
235
+ const request = RequestContext.current?.request;
236
+ if (request) {
237
+ try {
238
+ return new URL(request.url).origin;
239
+ } catch {
240
+ // Fall through: a synthetic Request may carry an unparseable url.
241
+ }
242
+ }
243
+
244
+ // 2. Configured public URL, normalised whether or not it carries a scheme.
245
+ if (c.publicUrl) {
246
+ const normalised = /^https?:\/\//i.test(c.publicUrl) ? c.publicUrl : `https://${c.publicUrl}`;
247
+ try {
248
+ return new URL(normalised).origin;
249
+ } catch {
250
+ // Fall through to the account host rather than publish a broken origin.
251
+ }
252
+ }
253
+
254
+ // 3. The account's VTEX host. Always well-formed; never the public brand.
255
+ return `https://${vtexHost("vtexcommercestable", c)}`;
256
+ }
257
+
209
258
  function baseUrl(): string {
210
259
  return `https://${vtexHost()}`;
211
260
  }
@@ -4,7 +4,7 @@
4
4
  * Combines /autocomplete_suggestions/ and /product_search/ in parallel,
5
5
  * transforms IS products to schema.org via pickSku + toProduct.
6
6
  */
7
- import { getVtexConfig, intelligentSearch as vtexIS } from "../client";
7
+ import { getVtexConfig, intelligentSearch as vtexIS, storefrontBaseUrl } from "../client";
8
8
  import { pickSku, toProduct as toSchemaProduct } from "../utils/transform";
9
9
 
10
10
  export interface AutocompleteProps {
@@ -40,9 +40,7 @@ export async function autocompleteSearch(props: AutocompleteProps): Promise<Auto
40
40
  ]);
41
41
 
42
42
  const config = getVtexConfig();
43
- const baseUrl = config.publicUrl
44
- ? `https://${config.publicUrl}`
45
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
43
+ const baseUrl = storefrontBaseUrl(config);
46
44
 
47
45
  return {
48
46
  searches: suggestionsData.searches ?? [],
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { ProductDetailsPage } from "@decocms/apps-commerce/types";
8
- import { getVtexConfig, vtexCachedFetch } from "../../client";
8
+ import { getVtexConfig, storefrontBaseUrl, vtexCachedFetch } from "../../client";
9
9
  import PDPDefaultPath from "../paths/PDPDefaultPath";
10
10
  import { searchBySlug } from "../../utils/slugCache";
11
11
  import { pickSku, toProductPage } from "../../utils/transform";
@@ -74,9 +74,7 @@ export default async function vtexProductDetailsPage(
74
74
  }
75
75
 
76
76
  const product = products[0];
77
- const baseUrl = config.publicUrl
78
- ? `https://${config.publicUrl}`
79
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
77
+ const baseUrl = storefrontBaseUrl(config);
80
78
 
81
79
  const sku = pickSku(product, skuId);
82
80
 
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Product } from "@decocms/apps-commerce/types";
10
- import { getVtexConfig, intelligentSearch, toFacetPath } from "../../client";
10
+ import { getVtexConfig, intelligentSearch, storefrontBaseUrl, toFacetPath } from "../../client";
11
11
  import { pickSku, sortProducts, toProductShelf } from "../../utils/transform";
12
12
  import type { Product as ProductVTEX } from "../../utils/types";
13
13
 
@@ -136,9 +136,7 @@ export default async function vtexProductListShelf(
136
136
  const data = await intelligentSearch<{ products: ProductVTEX[] }>(endpoint, params);
137
137
 
138
138
  const vtexProducts = data.products ?? [];
139
- const baseUrl = config.publicUrl
140
- ? `https://${config.publicUrl}`
141
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
139
+ const baseUrl = storefrontBaseUrl(config);
142
140
 
143
141
  let products = vtexProducts.map((p) => {
144
142
  const fetchedSkus = ids ? new Set(ids) : null;
@@ -4,6 +4,7 @@ import {
4
4
  intelligentSearch,
5
5
  type PageType,
6
6
  pageTypesFromPath,
7
+ storefrontBaseUrl,
7
8
  toFacetPath,
8
9
  } from "../../client";
9
10
  import { pickSku, toProduct } from "../../utils/transform";
@@ -441,9 +442,7 @@ export default async function vtexProductListingPage(props: PLPProps): Promise<a
441
442
  const { products: vtexProducts, pagination, recordsFiltered } = productsResult;
442
443
 
443
444
  // 3. Transform products using shared transform pipeline (same as deco-cx/apps)
444
- const baseUrl = config.publicUrl
445
- ? `https://${config.publicUrl}`
446
- : `https://${config.account}.vtexcommercestable.com.br`;
445
+ const baseUrl = storefrontBaseUrl(config);
447
446
 
448
447
  const schemaProducts = (vtexProducts as ProductVTEX[]).map((p) => {
449
448
  const sku = pickSku(p);
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { Product, Suggestion } from "@decocms/apps-commerce/types";
7
- import { getVtexConfig, intelligentSearch } from "../../client";
7
+ import { getVtexConfig, intelligentSearch, storefrontBaseUrl } from "../../client";
8
8
  import { pickSku, toProduct } from "../../utils/transform";
9
9
  import type { Product as ProductVTEX } from "../../utils/types";
10
10
 
@@ -29,9 +29,7 @@ export default async function vtexSuggestions(props: SuggestionsProps): Promise<
29
29
  }));
30
30
 
31
31
  const config = getVtexConfig();
32
- const baseUrl = config.publicUrl
33
- ? `https://${config.publicUrl}`
34
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
32
+ const baseUrl = storefrontBaseUrl(config);
35
33
 
36
34
  const products: Product[] = (data.products ?? []).slice(0, props.count ?? 4).map((p) => {
37
35
  const sku = pickSku(p);
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { Product } from "@decocms/apps-commerce/types";
11
- import { getVtexConfig, vtexCachedFetch } from "../../client";
11
+ import { getVtexConfig, storefrontBaseUrl, vtexCachedFetch } from "../../client";
12
12
  import { resolveProductIdBySlug } from "../../utils/slugCache";
13
13
  import { pickSku, toProduct } from "../../utils/transform";
14
14
  import type { LegacyProduct } from "../../utils/types";
@@ -58,9 +58,7 @@ export default async function vtexRelatedProducts(
58
58
  if (!related?.length) return [];
59
59
 
60
60
  const config = getVtexConfig();
61
- const baseUrl = config.publicUrl
62
- ? `https://${config.publicUrl}`
63
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
61
+ const baseUrl = storefrontBaseUrl(config);
64
62
 
65
63
  let result = related.slice(0, count).map((p) => {
66
64
  const sku = pickSku(p);
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Product } from "@decocms/apps-commerce/types";
10
- import { getVtexConfig, intelligentSearch, toFacetPath } from "../client";
10
+ import { getVtexConfig, intelligentSearch, storefrontBaseUrl, toFacetPath } from "../client";
11
11
  import { pickSku, sortProducts, toProduct } from "../utils/transform";
12
12
  import type { Product as ProductVTEX } from "../utils/types";
13
13
 
@@ -135,9 +135,7 @@ export default async function vtexProductList(props: ProductListProps): Promise<
135
135
  const data = await intelligentSearch<{ products: ProductVTEX[] }>(endpoint, params);
136
136
 
137
137
  const vtexProducts = data.products ?? [];
138
- const baseUrl = config.publicUrl
139
- ? `https://${config.publicUrl}`
140
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
138
+ const baseUrl = storefrontBaseUrl(config);
141
139
 
142
140
  let products = vtexProducts.map((p) => {
143
141
  const fetchedSkus = ids ? new Set(ids) : null;
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { Product } from "@decocms/apps-commerce/types";
7
- import { getVtexConfig, intelligentSearch, toFacetPath } from "../../client";
7
+ import { getVtexConfig, intelligentSearch, storefrontBaseUrl, toFacetPath } from "../../client";
8
8
  import { pickSku, toProduct } from "../../utils/transform";
9
9
  import type { Product as ProductVTEX } from "../../utils/types";
10
10
 
@@ -49,9 +49,7 @@ export default async function vtexWorkflowProducts(
49
49
  const data = await intelligentSearch<{ products: ProductVTEX[] }>(endpoint, params);
50
50
 
51
51
  const products = data.products ?? [];
52
- const baseUrl = config.publicUrl
53
- ? `https://${config.publicUrl}`
54
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
52
+ const baseUrl = storefrontBaseUrl(config);
55
53
 
56
54
  return products.map((p) => {
57
55
  const sku = pickSku(p);
@@ -0,0 +1,61 @@
1
+ import { RequestContext } from "@decocms/blocks/sdk/requestContext";
2
+ import { beforeEach, describe, expect, it } from "vitest";
3
+
4
+ import { configureVtex, storefrontBaseUrl } from "../../client";
5
+
6
+ const ACCOUNT = { account: "examplestore" } as const;
7
+
8
+ describe("storefrontBaseUrl", () => {
9
+ beforeEach(() => {
10
+ configureVtex({ ...ACCOUNT });
11
+ });
12
+
13
+ it("uses the origin of the request being served", () => {
14
+ configureVtex({ ...ACCOUNT, publicUrl: "https://secure.example.com/" });
15
+
16
+ const origin = RequestContext.run(new Request("https://www.example.com/camisas?page=2"), () =>
17
+ storefrontBaseUrl(),
18
+ );
19
+
20
+ expect(origin).toBe("https://www.example.com");
21
+ });
22
+
23
+ it("never returns the double-scheme origin a full publicUrl used to produce", () => {
24
+ // `https://${publicUrl}` on "https://secure.example.com/" parsed with
25
+ // host "https", so every product URL came out as https://https/<slug>/p.
26
+ configureVtex({ ...ACCOUNT, publicUrl: "https://secure.example.com/" });
27
+
28
+ const origin = storefrontBaseUrl();
29
+
30
+ expect(origin).toBe("https://secure.example.com");
31
+ expect(new URL("/camisa-paris/p", origin).href).toBe("https://secure.example.com/camisa-paris/p");
32
+ });
33
+
34
+ it("accepts a publicUrl stored without a scheme", () => {
35
+ configureVtex({ ...ACCOUNT, publicUrl: "secure.example.com" });
36
+
37
+ expect(storefrontBaseUrl()).toBe("https://secure.example.com");
38
+ });
39
+
40
+ it("falls back to the account host when publicUrl is absent", () => {
41
+ expect(storefrontBaseUrl()).toBe("https://examplestore.vtexcommercestable.com.br");
42
+ });
43
+
44
+ it("honours a configured domain in the account-host fallback", () => {
45
+ configureVtex({ ...ACCOUNT, domain: "com" });
46
+
47
+ expect(storefrontBaseUrl()).toBe("https://examplestore.vtexcommercestable.com");
48
+ });
49
+
50
+ it("falls back to the account host rather than publish an unparseable publicUrl", () => {
51
+ configureVtex({ ...ACCOUNT, publicUrl: "://" });
52
+
53
+ expect(storefrontBaseUrl()).toBe("https://examplestore.vtexcommercestable.com.br");
54
+ });
55
+
56
+ it("returns an origin with no trailing slash", () => {
57
+ configureVtex({ ...ACCOUNT, publicUrl: "https://secure.example.com/" });
58
+
59
+ expect(storefrontBaseUrl()).not.toMatch(/\/$/);
60
+ });
61
+ });
@@ -24,7 +24,7 @@
24
24
  */
25
25
 
26
26
  import type { Product, ProductLeaf } from "@decocms/apps-commerce/types";
27
- import { getVtexConfig, vtexFetch, vtexIOGraphQL } from "../client";
27
+ import { getVtexConfig, storefrontBaseUrl, vtexFetch, vtexIOGraphQL } from "../client";
28
28
  import { listBrands } from "../loaders/brands";
29
29
  import { batch } from "./batch";
30
30
  import { withIsSimilarTo } from "./similars";
@@ -372,9 +372,7 @@ export function withKitItems(): ProductEnricher {
372
372
  if (!productIDs.size) return products;
373
373
 
374
374
  const config = getVtexConfig();
375
- const baseUrl = config.publicUrl
376
- ? `https://${config.publicUrl}`
377
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
375
+ const baseUrl = storefrontBaseUrl(config);
378
376
 
379
377
  const batches = batch([...productIDs], KIT_ITEMS_BATCH_SIZE);
380
378
  const productsById = new Map<string, ProductLeaf>();
@@ -428,9 +426,7 @@ export function withVariants(): ProductEnricher {
428
426
  if (!productIDs.size) return products;
429
427
 
430
428
  const config = getVtexConfig();
431
- const baseUrl = config.publicUrl
432
- ? `https://${config.publicUrl}`
433
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
429
+ const baseUrl = storefrontBaseUrl(config);
434
430
 
435
431
  const batches = batch([...productIDs], VARIANTS_BATCH_SIZE);
436
432
  const productsById = new Map<string, Product>();
@@ -1,5 +1,5 @@
1
1
  import type { Product } from "@decocms/apps-commerce/types";
2
- import { getVtexConfig, vtexFetch } from "../client";
2
+ import { getVtexConfig, storefrontBaseUrl, vtexFetch } from "../client";
3
3
  import { pickSku, toProduct } from "./transform";
4
4
  import type { LegacyProduct } from "./types";
5
5
 
@@ -18,9 +18,7 @@ export const withIsSimilarTo = async (product: Product): Promise<Product> => {
18
18
  if (!rawSimilars?.length) return product;
19
19
 
20
20
  const config = getVtexConfig();
21
- const baseUrl = config.publicUrl
22
- ? `https://${config.publicUrl}`
23
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
21
+ const baseUrl = storefrontBaseUrl(config);
24
22
 
25
23
  const similars = rawSimilars.map((p) => {
26
24
  const sku = pickSku(p);