Vue Storefront is now Alokai! Learn More
Unified Search Extension

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 sorting
  • getProductDetails — 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

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:

storefront-unified-nextjs/sdk/modules/bloomreach.ts
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:

storefront-unified-nextjs/sdk/modules/index.ts
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:

  • searchProducts returns { products: SfProductCatalogItem[], facets: SfFacet[], pagination: SfPagination }
  • getProductDetails returns { 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

PropertyTypeDefaultDescription
defaultCurrencystring'USD'Fallback currency when the vsf-currency cookie is not set
currenciesstring[]List of indexed currencies. When set, requests currency-specific price fields (price_eur, sale_price_eur, etc.)

Facets

PropertyTypeDefaultDescription
facetConfigFacetConfigRule[]Declarative rules mapping facet names to types (COLOR, SIZE, PRICE, etc.)
facetFieldsstring[]Facet field names to request from the API. Omit to request all available
excludeFacetsstring[]Facet names to exclude from the normalized response
facetFilterOperator'AND' | 'OR''OR'How multiple values within one facet are combined
filterFacets(facet) => booleanDynamic filter callback for facets
getFacetConfig(facet) => FacetConfigRuleFallback 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

PropertyTypeDefaultDescription
categoryRootstringRoot category ID. When set, the category facet shows children of this category instead of top-level categories

Dynamic resolvers

PropertySignatureDescription
resolveDomainKey(context) => string | undefinedResolve domain_key per request. Overrides the static domainKey from env
resolveViewId(context) => string | undefinedResolve 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) => stringTransform 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