Unified Search Extension
Overview
The Bloomreach Discovery unified extension normalizes search results into the Unified Data Model, so your storefront components work identically regardless of the search backend. It provides two endpoints:
searchProducts— product search with facets, pagination, and sortinggetProductDetails— single product lookup by ID
The @vsf-enterprise/bloomreach-discovery-api package exports the createUnifiedExtension factory and all related types. The unified extension is not registered automatically — you need to create and register it in your middleware configuration.
Installation
Using the search-bloomreach module (recommended)
If you installed the search-bloomreach module via the Alokai CLI, the unified extension is already configured. The module copies a config.ts file into your sf-modules/search-bloomreach/ directory with sensible defaults and ecommerce-specific settings for your platform.
The module also sets up both SDK modules (bloomreach for the raw API and bloomreachUnified for normalized data). Skip to Usage below.
Manual setup
If you're wiring the integration manually, use createUnifiedExtension to create the extension and register it in your middleware config:
// apps/storefront-middleware/integrations/bloomreach-discovery/config.ts
import type { ApiClientExtension, Integration } from '@alokai/connect/middleware';
import type { MiddlewareConfig, UnifiedConfig } from '@vsf-enterprise/bloomreach-discovery-api';
import { createUnifiedExtension } from '@vsf-enterprise/bloomreach-discovery-api';
const unifiedApiExtension = createUnifiedExtension({
config: {
defaultCurrency: 'EUR',
facetConfig: [
{ names: ['colors', 'Farbe'], type: 'COLOR' },
{ names: ['sizes', 'Größe'], type: 'SIZE' },
{ names: ['category'], type: 'CATEGORY' },
{ names: ['price'], range: true, type: 'PRICE' },
],
facetFields: ['price', 'colors', 'sizes', 'category', 'brand'],
resolveDomainKey: (context) => {
return `store_${context.config.normalizerContext?.locale ?? 'en'}`;
},
resolveViewId: (context) => {
const { currency, locale } = context.config.normalizerContext ?? {};
return currency && locale ? `${locale}_${currency.toLowerCase()}` : undefined;
},
} satisfies UnifiedConfig,
isNamespaced: true,
normalizers: { addCustomFields: [{}] },
});
export const config = {
configuration: {
discoveryApi: {
accountId: Number(process.env.BLOOMREACH_DISCOVERY_ACCOUNT_ID),
authKey: process.env.BLOOMREACH_DISCOVERY_AUTH_KEY,
domainKey: process.env.BLOOMREACH_DISCOVERY_DOMAIN_KEY,
},
},
extensions: (extensions: ApiClientExtension[]) => [...extensions, unifiedApiExtension],
location: '@vsf-enterprise/bloomreach-discovery-api/server',
} satisfies Integration<MiddlewareConfig>;
SDK Module
After configuring the middleware, add the unified SDK module to your storefront:
import type { BloomreachUnifiedEndpoints } from '@sf-modules-middleware/search-bloomreach';
import { defineSdkModule } from '@vue-storefront/next';
export const bloomreachUnified = defineSdkModule(({ buildModule, config, getRequestHeaders, middlewareModule }) =>
buildModule(middlewareModule<BloomreachUnifiedEndpoints>, {
apiUrl: `${config.apiUrl}/bloomreach/unified`,
cdnCacheBustingId: config.cdnCacheBustingId,
defaultRequestConfig: { headers: getRequestHeaders() },
ssrApiUrl: `${config.ssrApiUrl}/bloomreach/unified`,
}),
);
Export from your modules index:
export * from './bloomreach';
For the raw Bloomreach Discovery API (autosuggest, recommendations, etc.), see the Quick Start guide to set up the bloomreach SDK module.
Usage
Search products
const { products, facets, pagination } = await sdk.bloomreachUnified.searchProducts({
search: 'running shoes',
pageSize: 20,
currentPage: 1,
sortBy: 'price asc',
facets: {
colors: ['red', 'blue'],
},
});
Search by category
const { products, facets, pagination } = await sdk.bloomreachUnified.searchProducts({
category: 'electronics/laptops',
pageSize: 12,
});
Get product details
const { product, categoryHierarchy } = await sdk.bloomreachUnified.getProductDetails({
id: 'product-123',
});
Response types
Both endpoints return normalized Unified Data Model types:
searchProductsreturns{ products: SfProductCatalogItem[], facets: SfFacet[], pagination: SfPagination }getProductDetailsreturns{ product: SfProduct, categoryHierarchy: SfCategory[] }
These types are framework-agnostic and match the same shape used by other Alokai integrations (SAP Commerce Cloud, commercetools, etc.), so storefront components work across backends without changes.
Configuration Reference
The UnifiedConfig object controls how Bloomreach results are normalized. Pass it via the config property of createUnifiedExtension.
Core settings
| Property | Type | Default | Description |
|---|---|---|---|
defaultCurrency | string | 'USD' | Fallback currency when the vsf-currency cookie is not set |
currencies | string[] | — | List of indexed currencies. When set, requests currency-specific price fields (price_eur, sale_price_eur, etc.) |
Facets
| Property | Type | Default | Description |
|---|---|---|---|
facetConfig | FacetConfigRule[] | — | Declarative rules mapping facet names to types (COLOR, SIZE, PRICE, etc.) |
facetFields | string[] | — | Facet field names to request from the API. Omit to request all available |
excludeFacets | string[] | — | Facet names to exclude from the normalized response |
facetFilterOperator | 'AND' | 'OR' | 'OR' | How multiple values within one facet are combined |
filterFacets | (facet) => boolean | — | Dynamic filter callback for facets |
getFacetConfig | (facet) => FacetConfigRule | — | Fallback for facets not matched by facetConfig |
FacetConfigRule
interface FacetConfigRule {
names: string[]; // Facet field names this rule applies to
type?: string; // 'COLOR', 'SIZE', 'CATEGORY', 'PRICE', 'MULTI_SELECT', etc.
range?: boolean; // true for range facets (e.g. price)
multiSelect?: boolean; // Allow multiple selections (default: true)
}
Category
| Property | Type | Default | Description |
|---|---|---|---|
categoryRoot | string | — | Root category ID. When set, the category facet shows children of this category instead of top-level categories |
Dynamic resolvers
| Property | Signature | Description |
|---|---|---|
resolveDomainKey | (context) => string | undefined | Resolve domain_key per request. Overrides the static domainKey from env |
resolveViewId | (context) => string | undefined | Resolve view_id per request. Maps locale + currency to a Bloomreach view for currency-specific pricing |
resolveTrackingParams | (context) => { url?, refUrl?, brUid2? } | Resolve Bloomreach tracking parameters per request. When omitted, values are extracted from request headers and cookies |
transformImageUrl | (url: string) => string | Transform product image URLs (e.g. prepend a media host) |
Normalizer customization
Use addCustomFields to extend the normalized output with custom fields:
import { defineAddCustomFields } from '@vsf-enterprise/bloomreach-discovery-api';
const addCustomFields = defineAddCustomFields({
normalizeProductCatalogItem: (input, context) => ({
brand: input.brand,
isNew: input.is_new === 'true',
}),
});
const unifiedApiExtension = createUnifiedExtension({
config: { /* ... */ },
isNamespaced: true,
normalizers: { addCustomFields: [addCustomFields] },
});
Custom fields are available under the $custom property:
const { products } = await sdk.bloomreachUnified.searchProducts({ search: 'shoes' });
products[0].$custom.brand; // typed