From 4c743b9c905859a5fc9bf92bd05b2527e2445833 Mon Sep 17 00:00:00 2001 From: guitavano Date: Thu, 10 Sep 2026 17:41:58 -0300 Subject: [PATCH 1/2] feat(vtex): bump Intelligent Search to API v1 Migrates the VTEX Intelligent Search integration from the legacy `/api/io/_v/api/intelligent-search/*` endpoints to the new v1 API (`/api/intelligent-search/v1/*`). - Rename all IS route keys to v1 (underscores -> hyphens) in client.ts. - v1 no longer reads the vtex_segment cookie: forward locale, sales channel (sc), region, country and marketing context as explicit query params via new `withSegmentParams` helper, replacing `withSegmentCookie` on the IS calls. - PDP now uses the dedicated `GET /products` single-product endpoint (lookup by sku or slug) instead of `product_search` + `products[0]`. Kit items stay on product-search (multi-SKU lookup); pageType is kept only for SEO. Missing products (404) map to a not-found page. Validated end-to-end against a live VTEX account: PLP, PDP, facets, suggestions and top-searches all return correct data, with the v1 `Cache-Control: public, max-age=600` responses. Co-Authored-By: Claude Opus 4.8 --- .../intelligentSearch/productDetailsPage.ts | 79 ++++++++-------- vtex/loaders/intelligentSearch/productList.ts | 7 +- .../intelligentSearch/productListingPage.ts | 16 ++-- vtex/loaders/intelligentSearch/suggestions.ts | 17 ++-- vtex/loaders/intelligentSearch/topsearches.ts | 5 +- vtex/utils/client.ts | 90 ++++++++++++++----- vtex/utils/segment.ts | 38 ++++++++ 7 files changed, 162 insertions(+), 90 deletions(-) diff --git a/vtex/loaders/intelligentSearch/productDetailsPage.ts b/vtex/loaders/intelligentSearch/productDetailsPage.ts index 62d1eb22f..c2df2944f 100644 --- a/vtex/loaders/intelligentSearch/productDetailsPage.ts +++ b/vtex/loaders/intelligentSearch/productDetailsPage.ts @@ -11,13 +11,13 @@ 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, - PageType, Product as VTEXProduct, SimulationBehavior, } from "../../utils/types.ts"; @@ -47,18 +47,6 @@ export interface Props { simulationBehavior?: SimulationBehavior; } -/** - * When there's no ?skuId querystring, we need to figure out the product id - * from the pathname. For this, we use the pageType api - */ -const getProductID = (page: PageType) => { - if (page.pageType !== "Product") { - return null; - } - - return page.id!; -}; - /** * @title Product Details Page - Intelligent Search * @description List a product details page, with product and SEO data. commonly used for product pages. @@ -93,36 +81,38 @@ 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) { + // The v1 Intelligent Search 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 anymore. + 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; } - 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 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; + }); // Product not found, return the 404 status code if (!product) { @@ -133,6 +123,8 @@ 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, @@ -140,10 +132,11 @@ const loader = async ( }); const result = await vcsDeprecated - ["GET /api/io/_v/api/intelligent-search/product_search/*facets"]({ + ["GET /api/intelligent-search/v1/product-search/*facets"]({ ...params, - facets: toPath(facets), - }, { ...STALE, headers: withSegmentCookie(segment) }) + ...withSegmentParams(segment), + facets: toPath(withDefaultFacets([], ctx)), + }, STALE) .then((res) => res.json()); kitItems = result.products; diff --git a/vtex/loaders/intelligentSearch/productList.ts b/vtex/loaders/intelligentSearch/productList.ts index b55263334..3f35880ec 100644 --- a/vtex/loaders/intelligentSearch/productList.ts +++ b/vtex/loaders/intelligentSearch/productList.ts @@ -10,7 +10,7 @@ import { import { getSegmentCacheKeyWithoutUTM, getSegmentFromBag, - withSegmentCookie, + withSegmentParams, } from "../../utils/segment.ts"; import { withIsSimilarTo } from "../../utils/similars.ts"; import { sortProducts, toProduct } from "../../utils/transform.ts"; @@ -229,10 +229,11 @@ const loader = async ( const facets = withDefaultFacets(selectedFacets, ctx); const { products: vtexProducts } = await vcsDeprecated - ["GET /api/io/_v/api/intelligent-search/product_search/*facets"]({ + ["GET /api/intelligent-search/v1/product-search/*facets"]({ ...params, + ...withSegmentParams(segment), facets: toPath(facets), - }, { ...STALE, headers: withSegmentCookie(segment) }) + }, STALE) .then((res) => res.json()); const options = { diff --git a/vtex/loaders/intelligentSearch/productListingPage.ts b/vtex/loaders/intelligentSearch/productListingPage.ts index f87064bb2..2158b614b 100644 --- a/vtex/loaders/intelligentSearch/productListingPage.ts +++ b/vtex/loaders/intelligentSearch/productListingPage.ts @@ -17,7 +17,7 @@ import { import { getSegmentCacheKeyWithoutUTM, getSegmentFromBag, - withSegmentCookie, + withSegmentParams, } from "../../utils/segment.ts"; import { pageTypesFromUrl } from "../../utils/intelligentSearch.ts"; import { withIsSimilarTo } from "../../utils/similars.ts"; @@ -313,20 +313,20 @@ const loader = async ( ctx.defaultSegment?.cultureInfo ?? "pt-BR"; const params = withDefaultParams({ ...searchArgs, page, locale }); + const segmentParams = withSegmentParams(segment); // 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"]({ + ["GET /api/intelligent-search/v1/product-search/*facets"]({ ...params, + ...segmentParams, facets: toPath(selected), - }, { - ...STALE, - headers: segment ? withSegmentCookie(segment) : undefined, - }).then((res) => res.json()), - vcsDeprecated["GET /api/io/_v/api/intelligent-search/facets/*facets"]({ + }, STALE).then((res) => res.json()), + vcsDeprecated["GET /api/intelligent-search/v1/facets/*facets"]({ ...params, + ...segmentParams, facets: toPath(fselected), - }, { ...STALE, headers: segment ? withSegmentCookie(segment) : undefined }) + }, STALE) .then((res) => res.json()), ]); diff --git a/vtex/loaders/intelligentSearch/suggestions.ts b/vtex/loaders/intelligentSearch/suggestions.ts index 7530f85d6..7518fee0a 100644 --- a/vtex/loaders/intelligentSearch/suggestions.ts +++ b/vtex/loaders/intelligentSearch/suggestions.ts @@ -9,7 +9,7 @@ import { import { getSegmentCacheKeyWithoutUTM, getSegmentFromBag, - withSegmentCookie, + withSegmentParams, } from "../../utils/segment.ts"; import { withIsSimilarTo } from "../../utils/similars.ts"; import { toProduct } from "../../utils/transform.ts"; @@ -52,19 +52,15 @@ const loaders = async ( ctx.defaultSegment?.cultureInfo ?? "pt-BR"; const suggestions = () => - vcsDeprecated["GET /api/io/_v/api/intelligent-search/search_suggestions"]({ + vcsDeprecated["GET /api/intelligent-search/v1/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()); const topSearches = () => - vcsDeprecated["GET /api/io/_v/api/intelligent-search/top_searches"]({ + vcsDeprecated["GET /api/intelligent-search/v1/top-searches"]({ locale, - }, { ...STALE, headers: withSegmentCookie(segment) }) + }, STALE) .then((res) => res.json()); const productSearch = () => { @@ -72,10 +68,11 @@ const loaders = async ( const params = withDefaultParams({ query, count: count ?? 4, locale }); return vcsDeprecated - ["GET /api/io/_v/api/intelligent-search/product_search/*facets"]({ + ["GET /api/intelligent-search/v1/product-search/*facets"]({ ...params, + ...withSegmentParams(segment), facets: toPath(facets), - }, { ...STALE, headers: withSegmentCookie(segment) }) + }, STALE) .then((res) => res.json()); }; diff --git a/vtex/loaders/intelligentSearch/topsearches.ts b/vtex/loaders/intelligentSearch/topsearches.ts index 94bc087fb..cdeb1d504 100644 --- a/vtex/loaders/intelligentSearch/topsearches.ts +++ b/vtex/loaders/intelligentSearch/topsearches.ts @@ -2,7 +2,6 @@ import { AppContext } from "../../mod.ts"; import { getSegmentCacheKeyWithoutUTM, getSegmentFromBag, - withSegmentCookie, } from "../../utils/segment.ts"; import { STALE } from "../../../utils/fetch.ts"; import { Suggestion } from "../../../commerce/types.ts"; @@ -21,9 +20,9 @@ export default async function ( ctx.defaultSegment?.cultureInfo ?? "pt-BR"; return await ctx.vcsDeprecated - ["GET /api/io/_v/api/intelligent-search/top_searches"]({ + ["GET /api/intelligent-search/v1/top-searches"]({ locale, - }, { ...STALE, headers: withSegmentCookie(getSegmentFromBag(ctx)) }) + }, STALE) .then((res) => res.json()); } diff --git a/vtex/utils/client.ts b/vtex/utils/client.ts index b485045ea..f239f447a 100644 --- a/vtex/utils/client.ts +++ b/vtex/utils/client.ts @@ -16,8 +16,10 @@ import { OrderFormOrder, PageType, PortalSuggestion, + Product, ProductSearchResult, SelectableGifts, + SimulationBehavior, SimulationItem, SimulationOrderForm, SPEvent, @@ -25,6 +27,31 @@ import { Suggestion, } from "./types.ts"; +/** + * Context parameters for the Intelligent Search API v1. + * + * Unlike the legacy API, v1 no longer reads the `vtex_segment` cookie: locale, + * sales channel, region and marketing context must be sent explicitly as query + * parameters. + * + * @see https://developers.vtex.com/updates/release-notes/2026-07-08-new-intelligent-search-api-v1 + */ +export interface IntelligentSearchContext { + /** @description BCP 47 language code, e.g. pt-BR */ + locale?: string; + /** @description Sales channel (trade policy) ID */ + sc?: string; + /** @description Region ID for regionalized results */ + regionId?: string; + /** @description Three-letter country code (ISO 3166 ALPHA-3), e.g. BRA */ + country?: string; + utmSource?: string; + utmCampaign?: string; + utmiCampaign?: string; + campaigns?: string; + priceTables?: string; +} + export interface VTEXCommerceStable { "GET /api/vtexid/pub/authentication/start": { searchParams: { @@ -137,37 +164,54 @@ export interface VTEXCommerceStable { }; }; "GET /api/catalog_system/pub/category/tree/:level": { response: Category[] }; - "GET /api/io/_v/api/intelligent-search/search_suggestions": { + "GET /api/intelligent-search/v1/search-suggestions": { response: Suggestion; searchParams: { locale: string; query: string }; }; - "GET /api/io/_v/api/intelligent-search/top_searches": { + "GET /api/intelligent-search/v1/top-searches": { response: Suggestion; searchParams: { locale: string }; }; - "GET /api/io/_v/api/intelligent-search/product_search/*facets": { + "GET /api/intelligent-search/v1/product-search/*facets": { response: ProductSearchResult; - searchParams: { - page: number; - count: number; - query?: string; - sort?: string; - fuzzy?: string; - locale?: string; - hideUnavailableItems: boolean; - }; - }; - "GET /api/io/_v/api/intelligent-search/facets/*facets": { + searchParams: + & { + page: number; + count: number; + query?: string; + sort?: string; + fuzzy?: string; + hideUnavailableItems: boolean; + } + & IntelligentSearchContext; + }; + "GET /api/intelligent-search/v1/facets/*facets": { response: FacetSearchResult; - searchParams: { - page: number; - count: number; - query?: string; - sort?: string; - fuzzy?: string; - locale?: string; - hideUnavailableItems: boolean; - }; + searchParams: + & { + page: number; + count: number; + query?: string; + sort?: string; + fuzzy?: string; + hideUnavailableItems: boolean; + } + & IntelligentSearchContext; + }; + // Dedicated single-product lookup introduced by the Intelligent Search API v1, + // replacing the `product_search` + `product:`/`sku:` pipeline for PDPs. + "GET /api/intelligent-search/v1/products": { + response: Product; + searchParams: + & { + /** @description Identifier value to look up, interpreted per `field`. */ + value: string; + /** @description Which identifier `value` represents. Defaults to `id`. */ + field?: "id" | "slug" | "ean" | "sku" | "reference"; + hideUnavailableItems?: boolean; + simulationBehavior?: SimulationBehavior; + } + & IntelligentSearchContext; }; "GET /api/checkout/changeToAnonymousUser/:orderFormId": { diff --git a/vtex/utils/segment.ts b/vtex/utils/segment.ts index 4afa1ca7a..9416f730f 100644 --- a/vtex/utils/segment.ts +++ b/vtex/utils/segment.ts @@ -213,6 +213,44 @@ export const withSegmentCookie = ( return h; }; +/** + * Builds the Intelligent Search API v1 context query parameters from the segment. + * + * The IS API v1 no longer reads the `vtex_segment` cookie, so sales channel, + * region, locale and marketing context must be forwarded explicitly as query + * parameters. + * + * @see https://developers.vtex.com/updates/release-notes/2026-07-08-new-intelligent-search-api-v1 + */ +export const withSegmentParams = (segment?: WrappedSegment | null) => { + const payload = segment?.payload; + if (!payload) { + return {}; + } + + const { + channel, + regionId, + countryCode, + utm_source, + utm_campaign, + utmi_campaign, + campaigns, + priceTables, + } = payload; + + return { + ...(channel ? { sc: channel } : {}), + ...(regionId ? { regionId } : {}), + ...(countryCode ? { country: countryCode } : {}), + ...(utm_source ? { utmSource: utm_source } : {}), + ...(utm_campaign ? { utmCampaign: utm_campaign } : {}), + ...(utmi_campaign ? { utmiCampaign: utmi_campaign } : {}), + ...(typeof campaigns === "string" ? { campaigns } : {}), + ...(priceTables ? { priceTables } : {}), + }; +}; + export const setSegmentBag = ( cookies: Record, req: Request, From bb8b3e4bd100a9de804fa58fcd82ba29f09540ad Mon Sep 17 00:00:00 2001 From: guitavano Date: Thu, 10 Sep 2026 17:56:40 -0300 Subject: [PATCH 2/2] feat(vtex): gate Intelligent Search v1 behind `intelligentSearchV1` flag Puts the v1 migration behind an opt-in app flag instead of a hard cutover, so stores can adopt it gradually and fall back to the legacy API. - Add `intelligentSearchV1` prop to the VTEX app (default false). - Restore the legacy IS routes in client.ts alongside the v1 ones. - Centralize the legacy/v1 endpoint + context selection in `searchProducts`, `searchFacets`, `searchSuggestions` and `topSearches` helpers; loaders now call these instead of the raw client. v1 sends context as query params, legacy keeps the segment cookie. - PDP branches on the flag: v1 uses the dedicated `/products` endpoint, legacy keeps the pageType -> product_search -> products[0] pipeline. Validated end-to-end against a live account with the flag both OFF and ON: PLP, PDP, facets and suggestions return identical, correct results. Co-Authored-By: Claude Opus 4.8 --- .../intelligentSearch/productDetailsPage.ts | 114 +++++++++++----- vtex/loaders/intelligentSearch/productList.ts | 17 +-- .../intelligentSearch/productListingPage.ts | 20 +-- vtex/loaders/intelligentSearch/suggestions.ts | 27 +--- vtex/loaders/intelligentSearch/topsearches.ts | 8 +- vtex/mod.ts | 10 ++ vtex/utils/client.ts | 34 +++++ vtex/utils/intelligentSearch.ts | 128 ++++++++++++++++++ 8 files changed, 272 insertions(+), 86 deletions(-) diff --git a/vtex/loaders/intelligentSearch/productDetailsPage.ts b/vtex/loaders/intelligentSearch/productDetailsPage.ts index c2df2944f..41bddf923 100644 --- a/vtex/loaders/intelligentSearch/productDetailsPage.ts +++ b/vtex/loaders/intelligentSearch/productDetailsPage.ts @@ -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, @@ -18,6 +20,7 @@ import { HttpError } from "../../../utils/http.ts"; import { pickSku, toProductPage } from "../../utils/transform.ts"; import type { AdvancedLoaderConfig, + PageType, Product as VTEXProduct, SimulationBehavior, } from "../../utils/types.ts"; @@ -47,6 +50,18 @@ export interface Props { simulationBehavior?: SimulationBehavior; } +/** + * When there's no ?skuId querystring, we need to figure out the product id + * from the pathname. For this, we use the pageType api (legacy IS flow only). + */ +const getProductID = (page: PageType) => { + if (page.pageType !== "Product") { + return null; + } + + return page.id!; +}; + /** * @title Product Details Page - Intelligent Search * @description List a product details page, with product and SEO data. commonly used for product pages. @@ -82,37 +97,69 @@ const loader = async ( const url = new URL(baseUrl); const skuId = url.searchParams.get("skuId"); - // The v1 Intelligent Search 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 anymore. - 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; - } + 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 product = await vcsDeprecated - ["GET /api/intelligent-search/v1/products"]({ - field, - value, + const params = withDefaultParams({ + query, + count: 1, 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; }); + const { products: [firstProduct] } = await searchProducts( + ctx, + segment, + params, + toPath(withDefaultFacets([], ctx)), + ); + product = firstProduct; + } // Product not found, return the 404 status code if (!product) { @@ -131,13 +178,12 @@ const loader = async ( simulationBehavior: props.simulationBehavior ?? "default", }); - const result = await vcsDeprecated - ["GET /api/intelligent-search/v1/product-search/*facets"]({ - ...params, - ...withSegmentParams(segment), - facets: toPath(withDefaultFacets([], ctx)), - }, STALE) - .then((res) => res.json()); + const result = await searchProducts( + ctx, + segment, + params, + toPath(withDefaultFacets([], ctx)), + ); kitItems = result.products; } diff --git a/vtex/loaders/intelligentSearch/productList.ts b/vtex/loaders/intelligentSearch/productList.ts index 3f35880ec..211859a90 100644 --- a/vtex/loaders/intelligentSearch/productList.ts +++ b/vtex/loaders/intelligentSearch/productList.ts @@ -1,8 +1,8 @@ import type { Product } from "../../../commerce/types.ts"; -import { STALE } from "../../../utils/fetch.ts"; import { AppContext } from "../../mod.ts"; import { isFilterParam, + searchProducts, toPath, withDefaultFacets, withDefaultParams, @@ -10,7 +10,6 @@ import { import { getSegmentCacheKeyWithoutUTM, getSegmentFromBag, - withSegmentParams, } from "../../utils/segment.ts"; import { withIsSimilarTo } from "../../utils/similars.ts"; import { sortProducts, toProduct } from "../../utils/transform.ts"; @@ -218,7 +217,6 @@ const loader = async ( ): Promise => { 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 ?? @@ -228,13 +226,12 @@ const loader = async ( const params = withDefaultParams({ ...args, locale }); const facets = withDefaultFacets(selectedFacets, ctx); - const { products: vtexProducts } = await vcsDeprecated - ["GET /api/intelligent-search/v1/product-search/*facets"]({ - ...params, - ...withSegmentParams(segment), - facets: toPath(facets), - }, STALE) - .then((res) => res.json()); + const { products: vtexProducts } = await searchProducts( + ctx, + segment, + params, + toPath(facets), + ); const options = { baseUrl: url, diff --git a/vtex/loaders/intelligentSearch/productListingPage.ts b/vtex/loaders/intelligentSearch/productListingPage.ts index 2158b614b..098e25608 100644 --- a/vtex/loaders/intelligentSearch/productListingPage.ts +++ b/vtex/loaders/intelligentSearch/productListingPage.ts @@ -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, @@ -17,7 +18,6 @@ import { import { getSegmentCacheKeyWithoutUTM, getSegmentFromBag, - withSegmentParams, } from "../../utils/segment.ts"; import { pageTypesFromUrl } from "../../utils/intelligentSearch.ts"; import { withIsSimilarTo } from "../../utils/similars.ts"; @@ -276,7 +276,6 @@ const loader = async ( req: Request, ctx: AppContext, ): Promise => { - const { vcsDeprecated } = ctx; const { url: baseUrl } = req; const url = new URL(props.pageHref || baseUrl); const segment = getSegmentFromBag(ctx); @@ -313,21 +312,10 @@ const loader = async ( ctx.defaultSegment?.cultureInfo ?? "pt-BR"; const params = withDefaultParams({ ...searchArgs, page, locale }); - const segmentParams = withSegmentParams(segment); // search products on VTEX. Feel free to change any of these parameters const [productsResult, facetsResult] = await Promise.all([ - vcsDeprecated - ["GET /api/intelligent-search/v1/product-search/*facets"]({ - ...params, - ...segmentParams, - facets: toPath(selected), - }, STALE).then((res) => res.json()), - vcsDeprecated["GET /api/intelligent-search/v1/facets/*facets"]({ - ...params, - ...segmentParams, - facets: toPath(fselected), - }, STALE) - .then((res) => res.json()), + searchProducts(ctx, segment, params, toPath(selected)), + searchFacets(ctx, segment, params, toPath(fselected)), ]); const currentPageTypes = !props.useCollectionName diff --git a/vtex/loaders/intelligentSearch/suggestions.ts b/vtex/loaders/intelligentSearch/suggestions.ts index 7518fee0a..700264ff3 100644 --- a/vtex/loaders/intelligentSearch/suggestions.ts +++ b/vtex/loaders/intelligentSearch/suggestions.ts @@ -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, - withSegmentParams, } from "../../utils/segment.ts"; import { withIsSimilarTo } from "../../utils/similars.ts"; import { toProduct } from "../../utils/transform.ts"; @@ -44,7 +45,6 @@ const loaders = async ( req: Request, ctx: AppContext, ): Promise => { - const { vcsDeprecated } = ctx; const { url } = req; const { count, query } = props; const segment = getSegmentFromBag(ctx); @@ -52,32 +52,19 @@ const loaders = async ( ctx.defaultSegment?.cultureInfo ?? "pt-BR"; const suggestions = () => - vcsDeprecated["GET /api/intelligent-search/v1/search-suggestions"]({ - locale, - query: query ?? "", - }).then((res) => res.json()); + searchSuggestions(ctx, segment, { locale, query: query ?? "" }); - const topSearches = () => - vcsDeprecated["GET /api/intelligent-search/v1/top-searches"]({ - locale, - }, STALE) - .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/intelligent-search/v1/product-search/*facets"]({ - ...params, - ...withSegmentParams(segment), - facets: toPath(facets), - }, STALE) - .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(), ]); diff --git a/vtex/loaders/intelligentSearch/topsearches.ts b/vtex/loaders/intelligentSearch/topsearches.ts index cdeb1d504..eeb010195 100644 --- a/vtex/loaders/intelligentSearch/topsearches.ts +++ b/vtex/loaders/intelligentSearch/topsearches.ts @@ -3,7 +3,7 @@ import { getSegmentCacheKeyWithoutUTM, getSegmentFromBag, } from "../../utils/segment.ts"; -import { STALE } from "../../../utils/fetch.ts"; +import { topSearches } from "../../utils/intelligentSearch.ts"; import { Suggestion } from "../../../commerce/types.ts"; /** @@ -19,11 +19,7 @@ export default async function ( const locale = segment?.payload?.cultureInfo ?? ctx.defaultSegment?.cultureInfo ?? "pt-BR"; - return await ctx.vcsDeprecated - ["GET /api/intelligent-search/v1/top-searches"]({ - locale, - }, STALE) - .then((res) => res.json()); + return await topSearches(ctx, segment, { locale }); } export const cache = { diff --git a/vtex/mod.ts b/vtex/mod.ts index c287f0864..e569d2813 100644 --- a/vtex/mod.ts +++ b/vtex/mod.ts @@ -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; /** diff --git a/vtex/utils/client.ts b/vtex/utils/client.ts index f239f447a..ace7d4b48 100644 --- a/vtex/utils/client.ts +++ b/vtex/utils/client.ts @@ -164,6 +164,40 @@ export interface VTEXCommerceStable { }; }; "GET /api/catalog_system/pub/category/tree/:level": { response: Category[] }; + // Legacy Intelligent Search endpoints. Kept alongside the v1 routes so the + // `intelligentSearchV1` app flag can fall back to them. + "GET /api/io/_v/api/intelligent-search/search_suggestions": { + response: Suggestion; + searchParams: { locale: string; query: string }; + }; + "GET /api/io/_v/api/intelligent-search/top_searches": { + response: Suggestion; + searchParams: { locale: string }; + }; + "GET /api/io/_v/api/intelligent-search/product_search/*facets": { + response: ProductSearchResult; + searchParams: { + page: number; + count: number; + query?: string; + sort?: string; + fuzzy?: string; + locale?: string; + hideUnavailableItems: boolean; + }; + }; + "GET /api/io/_v/api/intelligent-search/facets/*facets": { + response: FacetSearchResult; + searchParams: { + page: number; + count: number; + query?: string; + sort?: string; + fuzzy?: string; + locale?: string; + hideUnavailableItems: boolean; + }; + }; "GET /api/intelligent-search/v1/search-suggestions": { response: Suggestion; searchParams: { locale: string; query: string }; diff --git a/vtex/utils/intelligentSearch.ts b/vtex/utils/intelligentSearch.ts index 482b33f7a..af07a633d 100644 --- a/vtex/utils/intelligentSearch.ts +++ b/vtex/utils/intelligentSearch.ts @@ -1,9 +1,17 @@ import { AppContext } from "../mod.ts"; import { STALE } from "../../utils/fetch.ts"; +import { + withSegmentCookie, + withSegmentParams, + type WrappedSegment, +} from "./segment.ts"; import type { + FacetSearchResult, + ProductSearchResult, SelectedFacet, SimulationBehavior, Sort, + Suggestion, } from "../utils/types.ts"; export const SESSION_COOKIE = "vtex_is_session"; @@ -73,6 +81,126 @@ export const withDefaultParams = ({ simulationBehavior, }); +/** + * Whether the store opted in to the VTEX Intelligent Search API v1 (via the + * `intelligentSearchV1` app flag). When off, the legacy Intelligent Search + * endpoints are used. + */ +export const isIntelligentSearchV1 = (ctx: AppContext): boolean => + ctx.intelligentSearchV1 ?? false; + +type DefaultParams = ReturnType; + +/** + * Runs a product search on the Intelligent Search API, picking the v1 or the + * legacy endpoint based on the `intelligentSearchV1` flag. v1 forwards the + * segment context as explicit query params; the legacy API reads it from the + * segment cookie. + */ +export const searchProducts = ( + ctx: AppContext, + segment: WrappedSegment | null | undefined, + params: DefaultParams, + facets: string, +): Promise => { + const { vcsDeprecated } = ctx; + + if (isIntelligentSearchV1(ctx)) { + return vcsDeprecated + ["GET /api/intelligent-search/v1/product-search/*facets"]({ + ...params, + ...withSegmentParams(segment), + facets, + }, STALE).then((res) => res.json()); + } + + return vcsDeprecated + ["GET /api/io/_v/api/intelligent-search/product_search/*facets"]({ + ...params, + facets, + }, { + ...STALE, + headers: segment ? withSegmentCookie(segment) : undefined, + }).then((res) => res.json()); +}; + +/** + * Runs a facets search on the Intelligent Search API (v1 or legacy). + */ +export const searchFacets = ( + ctx: AppContext, + segment: WrappedSegment | null | undefined, + params: DefaultParams, + facets: string, +): Promise => { + const { vcsDeprecated } = ctx; + + if (isIntelligentSearchV1(ctx)) { + return vcsDeprecated["GET /api/intelligent-search/v1/facets/*facets"]({ + ...params, + ...withSegmentParams(segment), + facets, + }, STALE).then((res) => res.json()); + } + + return vcsDeprecated["GET /api/io/_v/api/intelligent-search/facets/*facets"]({ + ...params, + facets, + }, { + ...STALE, + headers: segment ? withSegmentCookie(segment) : undefined, + }).then((res) => res.json()); +}; + +/** + * Fetches search term suggestions (v1 or legacy). v1 only accepts locale/query. + */ +export const searchSuggestions = ( + ctx: AppContext, + segment: WrappedSegment | null | undefined, + { locale, query }: { locale: string; query: string }, +): Promise => { + const { vcsDeprecated } = ctx; + + if (isIntelligentSearchV1(ctx)) { + return vcsDeprecated["GET /api/intelligent-search/v1/search-suggestions"]({ + locale, + query, + }).then((res) => res.json()); + } + + return vcsDeprecated + ["GET /api/io/_v/api/intelligent-search/search_suggestions"]({ + locale, + query, + }, { headers: segment ? withSegmentCookie(segment) : undefined }) + .then((res) => res.json()); +}; + +/** + * Fetches the store's top searches (v1 or legacy). v1 only accepts locale. + */ +export const topSearches = ( + ctx: AppContext, + segment: WrappedSegment | null | undefined, + { locale }: { locale: string }, +): Promise => { + const { vcsDeprecated } = ctx; + + if (isIntelligentSearchV1(ctx)) { + return vcsDeprecated["GET /api/intelligent-search/v1/top-searches"]({ + locale, + }, STALE).then((res) => res.json()); + } + + return vcsDeprecated["GET /api/io/_v/api/intelligent-search/top_searches"]({ + locale, + }, { + ...STALE, + headers: segment ? withSegmentCookie(segment) : undefined, + }).then((res) => res.json()); +}; + const IS_ANONYMOUS = Symbol("segment"); const IS_SESSION = Symbol("segment");