Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 75 additions & 36 deletions vtex/loaders/intelligentSearch/productDetailsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { STALE } from "../../../utils/fetch.ts";
import type { RequestURLParam } from "../../../website/functions/requestToParam.ts";
import { AppContext } from "../../mod.ts";
import {
isIntelligentSearchV1,
searchProducts,
toPath,
withDefaultFacets,
withDefaultParams,
Expand All @@ -11,9 +13,10 @@ import { pageTypesToSeo } from "../../utils/legacy.ts";
import {
getSegmentCacheKeyWithoutUTM,
getSegmentFromBag,
withSegmentCookie,
withSegmentParams,
} from "../../utils/segment.ts";
import { withIsSimilarTo } from "../../utils/similars.ts";
import { HttpError } from "../../../utils/http.ts";
import { pickSku, toProductPage } from "../../utils/transform.ts";
import type {
AdvancedLoaderConfig,
Expand Down Expand Up @@ -49,7 +52,7 @@ export interface Props {

/**
* When there's no ?skuId querystring, we need to figure out the product id
* from the pathname. For this, we use the pageType api
* from the pathname. For this, we use the pageType api (legacy IS flow only).
*/
const getProductID = (page: PageType) => {
if (page.pageType !== "Product") {
Expand Down Expand Up @@ -93,36 +96,70 @@ const loader = async (

const url = new URL(baseUrl);
const skuId = url.searchParams.get("skuId");
const productId = !skuId && getProductID(await pageTypePromise);

/**
* Fetch the exact skuId. If no one was provided, try fetching the product
* and return the first sku
*/
const query = skuId
? `sku:${skuId}`
: productId
? `product:${productId}`
: null;

// In case we dont have the skuId or the productId, 404
if (!query) {
return null;
}
let product: VTEXProduct | null = null;

if (isIntelligentSearchV1(ctx)) {
// v1 exposes a dedicated single-product endpoint. Look it up by SKU when a
// skuId is present in the URL, otherwise by slug — no need to resolve the
// productId from the pageType API.
const [field, value] = skuId
? (["sku", skuId] as const)
: (["slug", lowercaseSlug] as const);

// Without a skuId or a slug there is nothing to look up, 404
if (!value) {
return null;
}

product = await vcsDeprecated
["GET /api/intelligent-search/v1/products"]({
field,
value,
locale,
simulationBehavior: props.simulationBehavior ?? "default",
...withSegmentParams(segment),
// sc is required by this endpoint to resolve pricing/availability.
sc: segment?.payload?.channel ?? ctx.salesChannel ?? "1",
}, STALE)
.then((res) => res.json())
.catch((error) => {
// A missing product resolves to a 404 on the v1 products endpoint;
// translate it into a not-found page instead of surfacing the error.
if (error instanceof HttpError && error.status === 404) {
return null;
}
throw error;
});
} else {
// Legacy pipeline: resolve the productId from the pageType, then take the
// first product returned by product_search.
const productId = !skuId && getProductID(await pageTypePromise);
const query = skuId
? `sku:${skuId}`
: productId
? `product:${productId}`
: null;

// In case we dont have the skuId or the productId, 404
if (!query) {
return null;
}

const facets = withDefaultFacets([], ctx);
const params = withDefaultParams({
query,
count: 1,
locale,
simulationBehavior: props.simulationBehavior ?? "default",
});
const { products: [product] } = await vcsDeprecated
["GET /api/io/_v/api/intelligent-search/product_search/*facets"]({
...params,
facets: toPath(facets),
}, { ...STALE, headers: withSegmentCookie(segment) })
.then((res) => res.json());
const params = withDefaultParams({
query,
count: 1,
locale,
simulationBehavior: props.simulationBehavior ?? "default",
});
const { products: [firstProduct] } = await searchProducts(
ctx,
segment,
params,
toPath(withDefaultFacets([], ctx)),
);
product = firstProduct;
}

// Product not found, return the 404 status code
if (!product) {
Expand All @@ -133,18 +170,20 @@ const loader = async (

let kitItems: VTEXProduct[] = [];
if (sku.isKit && sku.kitItems) {
// Kit components are a multi-SKU lookup, which the single-product endpoint
// does not support, so it stays on the product_search pipeline.
const params = withDefaultParams({
query: `sku:${sku.kitItems.join(";")}`,
count: sku.kitItems.length,
simulationBehavior: props.simulationBehavior ?? "default",
});

const result = await vcsDeprecated
["GET /api/io/_v/api/intelligent-search/product_search/*facets"]({
...params,
facets: toPath(facets),
}, { ...STALE, headers: withSegmentCookie(segment) })
.then((res) => res.json());
const result = await searchProducts(
ctx,
segment,
params,
toPath(withDefaultFacets([], ctx)),
);

kitItems = result.products;
}
Expand Down
16 changes: 7 additions & 9 deletions vtex/loaders/intelligentSearch/productList.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { Product } from "../../../commerce/types.ts";
import { STALE } from "../../../utils/fetch.ts";
import { AppContext } from "../../mod.ts";
import {
isFilterParam,
searchProducts,
toPath,
withDefaultFacets,
withDefaultParams,
} from "../../utils/intelligentSearch.ts";
import {
getSegmentCacheKeyWithoutUTM,
getSegmentFromBag,
withSegmentCookie,
} from "../../utils/segment.ts";
import { withIsSimilarTo } from "../../utils/similars.ts";
import { sortProducts, toProduct } from "../../utils/transform.ts";
Expand Down Expand Up @@ -218,7 +217,6 @@ const loader = async (
): Promise<Product[] | null> => {
const props = expandedProps.props ??
(expandedProps as unknown as Props["props"]);
const { vcsDeprecated } = ctx;
const { url } = req;
const segment = getSegmentFromBag(ctx);
const locale = segment?.payload?.cultureInfo ??
Expand All @@ -228,12 +226,12 @@ const loader = async (
const params = withDefaultParams({ ...args, locale });
const facets = withDefaultFacets(selectedFacets, ctx);

const { products: vtexProducts } = await vcsDeprecated
["GET /api/io/_v/api/intelligent-search/product_search/*facets"]({
...params,
facets: toPath(facets),
}, { ...STALE, headers: withSegmentCookie(segment) })
.then((res) => res.json());
const { products: vtexProducts } = await searchProducts(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When intelligentSearchV1 changes, this call switches the backend while cacheKey remains identical. Stale-while-revalidate can serve the previous API's product list after rollout; include the selected API mode in the cache key or invalidate this cache when the flag changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vtex/loaders/intelligentSearch/productList.ts, line 229:

<comment>When `intelligentSearchV1` changes, this call switches the backend while `cacheKey` remains identical. Stale-while-revalidate can serve the previous API's product list after rollout; include the selected API mode in the cache key or invalidate this cache when the flag changes.</comment>

<file context>
@@ -228,13 +226,12 @@ const loader = async (
-      facets: toPath(facets),
-    }, STALE)
-    .then((res) => res.json());
+  const { products: vtexProducts } = await searchProducts(
+    ctx,
+    segment,
</file context>

ctx,
segment,
params,
toPath(facets),
);

const options = {
baseUrl: url,
Expand Down
20 changes: 4 additions & 16 deletions vtex/loaders/intelligentSearch/productListingPage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { ProductListingPage } from "../../../commerce/types.ts";
import { parseRange } from "../../../commerce/utils/filters.ts";
import { STALE } from "../../../utils/fetch.ts";
import sendEvent from "../../actions/analytics/sendEvent.ts";
import { AppContext } from "../../mod.ts";
import {
isFilterParam,
searchFacets,
searchProducts,
toPath,
withDefaultFacets,
withDefaultParams,
Expand All @@ -17,7 +18,6 @@ import {
import {
getSegmentCacheKeyWithoutUTM,
getSegmentFromBag,
withSegmentCookie,
} from "../../utils/segment.ts";
import { pageTypesFromUrl } from "../../utils/intelligentSearch.ts";
import { withIsSimilarTo } from "../../utils/similars.ts";
Expand Down Expand Up @@ -276,7 +276,6 @@ const loader = async (
req: Request,
ctx: AppContext,
): Promise<ProductListingPage | null> => {
const { vcsDeprecated } = ctx;
const { url: baseUrl } = req;
const url = new URL(props.pageHref || baseUrl);
const segment = getSegmentFromBag(ctx);
Expand Down Expand Up @@ -315,19 +314,8 @@ const loader = async (
const params = withDefaultParams({ ...searchArgs, page, locale });
// search products on VTEX. Feel free to change any of these parameters
const [productsResult, facetsResult] = await Promise.all([
vcsDeprecated
["GET /api/io/_v/api/intelligent-search/product_search/*facets"]({
...params,
facets: toPath(selected),
}, {
...STALE,
headers: segment ? withSegmentCookie(segment) : undefined,
}).then((res) => res.json()),
vcsDeprecated["GET /api/io/_v/api/intelligent-search/facets/*facets"]({
...params,
facets: toPath(fselected),
}, { ...STALE, headers: segment ? withSegmentCookie(segment) : undefined })
.then((res) => res.json()),
searchProducts(ctx, segment, params, toPath(selected)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When intelligentSearchV1 changes, this call can switch between legacy and v1 responses, but the loader's cacheKey omits the flag. Include the flag in every affected Intelligent Search cache key or purge those caches when rolling out the flag.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vtex/loaders/intelligentSearch/productListingPage.ts, line 317:

<comment>When `intelligentSearchV1` changes, this call can switch between legacy and v1 responses, but the loader's `cacheKey` omits the flag. Include the flag in every affected Intelligent Search cache key or purge those caches when rolling out the flag.</comment>

<file context>
@@ -313,21 +312,10 @@ const loader = async (
-      facets: toPath(fselected),
-    }, STALE)
-      .then((res) => res.json()),
+    searchProducts(ctx, segment, params, toPath(selected)),
+    searchFacets(ctx, segment, params, toPath(fselected)),
   ]);
</file context>

searchFacets(ctx, segment, params, toPath(fselected)),
]);

const currentPageTypes = !props.useCollectionName
Expand Down
30 changes: 7 additions & 23 deletions vtex/loaders/intelligentSearch/suggestions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Suggestion } from "../../../commerce/types.ts";
import { STALE } from "../../../utils/fetch.ts";
import { AppContext } from "../../mod.ts";
import {
searchProducts,
searchSuggestions,
toPath,
topSearches,
withDefaultFacets,
withDefaultParams,
} from "../../utils/intelligentSearch.ts";
import {
getSegmentCacheKeyWithoutUTM,
getSegmentFromBag,
withSegmentCookie,
} from "../../utils/segment.ts";
import { withIsSimilarTo } from "../../utils/similars.ts";
import { toProduct } from "../../utils/transform.ts";
Expand Down Expand Up @@ -44,43 +45,26 @@ const loaders = async (
req: Request,
ctx: AppContext,
): Promise<Suggestion | null> => {
const { vcsDeprecated } = ctx;
const { url } = req;
const { count, query } = props;
const segment = getSegmentFromBag(ctx);
const locale = segment?.payload?.cultureInfo ??
ctx.defaultSegment?.cultureInfo ?? "pt-BR";

const suggestions = () =>
vcsDeprecated["GET /api/io/_v/api/intelligent-search/search_suggestions"]({
locale,
query: query ?? "",
}, {
// Not adding suggestions to cache since queries are very spread out
// deco: { cache: "stale-while-revalidate" },
headers: withSegmentCookie(segment),
}).then((res) => res.json());
searchSuggestions(ctx, segment, { locale, query: query ?? "" });

const topSearches = () =>
vcsDeprecated["GET /api/io/_v/api/intelligent-search/top_searches"]({
locale,
}, { ...STALE, headers: withSegmentCookie(segment) })
.then((res) => res.json());
const getTopSearches = () => topSearches(ctx, segment, { locale });

const productSearch = () => {
const facets = withDefaultFacets([], ctx);
const params = withDefaultParams({ query, count: count ?? 4, locale });

return vcsDeprecated
["GET /api/io/_v/api/intelligent-search/product_search/*facets"]({
...params,
facets: toPath(facets),
}, { ...STALE, headers: withSegmentCookie(segment) })
.then((res) => res.json());
return searchProducts(ctx, segment, params, toPath(facets));
};

const [{ searches }, { products, recordsFiltered }] = await Promise.all([
query ? suggestions() : topSearches(),
query ? suggestions() : getTopSearches(),
productSearch(),
]);

Expand Down
9 changes: 2 additions & 7 deletions vtex/loaders/intelligentSearch/topsearches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import { AppContext } from "../../mod.ts";
import {
getSegmentCacheKeyWithoutUTM,
getSegmentFromBag,
withSegmentCookie,
} from "../../utils/segment.ts";
import { STALE } from "../../../utils/fetch.ts";
import { topSearches } from "../../utils/intelligentSearch.ts";
import { Suggestion } from "../../../commerce/types.ts";

/**
Expand All @@ -20,11 +19,7 @@ export default async function (
const locale = segment?.payload?.cultureInfo ??
ctx.defaultSegment?.cultureInfo ?? "pt-BR";

return await ctx.vcsDeprecated
["GET /api/io/_v/api/intelligent-search/top_searches"]({
locale,
}, { ...STALE, headers: withSegmentCookie(getSegmentFromBag(ctx)) })
.then((res) => res.json());
return await topSearches(ctx, segment, { locale });
}

export const cache = {
Expand Down
10 changes: 10 additions & 0 deletions vtex/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ export interface Props {
*/
platform: "vtex";

/**
* @title Use Intelligent Search API v1
* @description Opt in to VTEX's new Intelligent Search API v1 for search, PLP,
* PDP, facets and suggestions. v1 no longer reads the segment cookie (context
* is sent as explicit query params) and uses the dedicated `/products`
* endpoint for PDPs. When off, the legacy Intelligent Search API is used.
* @default false
*/
intelligentSearchV1?: boolean;

advancedConfigs?: {
doNotFetchVariantsForRelatedProducts?: boolean;
/**
Expand Down
Loading
Loading