Customizations

1. Custom Response Processing

Override the fetcher method to customize response processing:

// In a code.js block
if (searchClient && searchClient.fetcher) {
  const originalFetcher = searchClient.fetcher.bind(searchClient)

  searchClient.fetcher = async function (params) {
    const result = await originalFetcher(params)

    // Custom processing
    result.products = result.products.map((product) => ({
      ...product,
      customField: "customValue",
    }))

    return result
  }
}

2. Custom Filter Processing

Override filter methods to add custom logic:

// Custom filter application
if (searchClient) {
  const originalSetFilter = searchClient.setFilter.bind(searchClient)

  searchClient.setFilter = async function (filter) {
    // Add custom pre-processing
    if (filter.key === "price") {
      filter.value = formatPrice(filter.value)
    }

    return originalSetFilter(filter)
  }
}

3. Custom Sort Options

Modify sort option resolution:

if (searchClient) {
  const originalResolveSortOptions =
    searchClient.resolveSortOptions?.bind(searchClient)

  searchClient.resolveSortOptions = function (label) {
    // Custom sort mapping
    const customSortMap = {
      popularity: "custom_popularity_score",
      newest: "created_at_desc",
    }

    return customSortMap[label] || originalResolveSortOptions?.(label) || label
  }
}

4. Using a Specific Search Integration in a code.js Block

When building custom blocks, you receive useSearchContext as a prop. Pass a provider name to activate that integration for your block and the entire layout.

Selecting a Provider

// In templates/BlockTemplates/MyCustomBlock/code.js
export default function MyCustomBlock({
  useSearchContext = () => ({ searchClient: null, isSearchClient: false }),
}) {
  // Select Algolia as the active provider
  const { searchClient, isSearchClient } = useSearchContext("algolia")

  ...
}

Supported provider names:

  • "algolia"
  • "search-spring"
  • "nosto-search"
  • "instant-search-plus"

Important: Calling useSearchContext("algolia") sets Algolia as the active provider for the entire layout, not just this block. If multiple blocks call useSearchContext with different provider names, the last one to render will win. Best practice is to select the provider once in your main product grid or search block, and let other blocks inherit it by calling useSearchContext() without arguments.

Example: ProductGrid with Conditional Provider Selection

export default function ProductGrid({
  useSearchContext = () => ({ searchClient: null, isSearchClient: false }),
  useSearchParams,
  pageState,
}) {
  const searchParams = useSearchParams()
  const searchQuery = searchParams?.get("searchQuery") || pageState?.searchParams?.searchQuery

  // Use Algolia when searching, otherwise don't set a provider (pass undefined)
  const providerType = searchQuery ? "algolia" : undefined
  const { searchClient, isSearchClient } = useSearchContext(providerType)

  ...
}

Example: Carousel Using a Different Provider Instance

export default function RecommendedProducts({ blockConfig }) {
  // Create a separate Search Spring instance for this carousel
  const carouselClient = useSearchInstance({
    searchParams: { collectionId: blockConfig.collectionId },
    productsPerPage: 8,
    type: "search-spring",
  })

  ...
}

Key Points

  • Pass the provider name to useSearchContext() once in your main block to activate it
  • Pass undefined to useSearchContext() to not set a provider and inherit the current active provider or fall back to default behavior.
  • Use useSearchInstance() when you need a separate client instance with different parameters. This will not affect the entire layout and is typically used in carousels.
  • Remember that provider selection with useSearchContext() affects the entire layout, not just the current block.

Integration-Specific Customizations

Algolia

Search Parameters Customization

// Override search parameters to add custom filters
searchClient.setSearchParametersOnHelper = function (searchParameters) {
  const existingFilters = searchParameters.filters
  const customFilter = "(inStock:true) AND isOnline:true AND (NOT isNfs:true)"

  searchParameters.filters = existingFilters
    ? `${existingFilters} AND ${customFilter}`
    : customFilter

  return searchParameters
}

Query and Index Management

// Set search query
searchClient.setQueryOnHelper("search term")

// Set search index for sorting
searchClient.setIndexOnHelper("products_price_asc")

// Override rule contexts for custom merchandising behavior
searchClient.setRuleContextsOnHelper = function () {
  // Access the helper via this.helper
  if (this.collectionHandle) {
    const customRuleContext = `custom-${this.collectionHandle}`
    this.helper.setState(
      this.helper.state.setQueryParameter("ruleContexts", [customRuleContext])
    )
  }
}

Note: setRuleContextsOnHelper is a method to override, not just invoke. It will replace the default rule context behavior.

Access Algolia Helper Directly

// Access Algolia-specific helper for advanced customizations
// Modify search parameters directly
searchClient.helper.setState(
  searchClient.helper.state.setQueryParameter("analyticsTags", ["custom-tag"])
)

// Add custom facet configuration
searchClient.helper.setState(
  searchClient.helper.state.addFacet("custom_attribute")
)

Important: Direct helper access is best used within overridden search client methods (like setRuleContextsOnHelper, or applyBaseParamsToIntegration). Accessing the helper outside of these methods can lead to unintended results, as the search client may reset or override your changes during its internal operations. Always prefer method overrides when possible for more predictable behavior.

Response Formatting

// Custom response formatting for Algolia results
searchClient.formatResponse = function ({ results, isFacetsOnly }) {
  // Custom logic here
  return {
    products: results.hits,
    page: results.page,
    totalPages: results.nbPages,
    totalProducts: results.nbHits,
    hasMore: results.page + 1 < results.nbPages,
    nextPage: results.page + 1 < results.nbPages ? results.page + 1 : null,
    // Add custom fields
    customField: "custom value",
    facets: results.facets,
  }
}

SearchSpring Client

Request Parameters

// Customize request parameters
if (searchClient.buildRequestParams) {
  const original = searchClient.buildRequestParams.bind(searchClient)

  searchClient.buildRequestParams = function (params) {
    const result = original(params)
    // Add custom parameters
    result.append("customParam", "customValue")
    result.append("boost", "featured:1.5")
    return result
  }
}

Response Formatting

// Custom response formatting for SearchSpring
if (searchClient.formatSearchSpringResponse) {
  searchClient.formatSearchSpringResponse = function (response) {
    // Return null to use default formatting, or return custom response
    return {
      products: response.results,
      page: response.pagination.currentPage - 1,
      totalPages: response.pagination.totalPages,
      totalProducts: response.pagination.totalResults,
      hasMore: response.pagination.currentPage < response.pagination.totalPages,
      nextPage:
        response.pagination.currentPage < response.pagination.totalPages
          ? response.pagination.currentPage
          : null,
      // Add custom metadata
      searchMetadata: response.merchandising || {},
    }
  }
}

URL Building

// Custom URL building for SearchSpring requests
if (searchClient.buildSearchSpringUrl) {
  searchClient.buildSearchSpringUrl = function (baseUrl, params) {
    // Add custom query parameters or modify URL structure
    return `${baseUrl}?${params.toString()}&customParam=value&tracking=enabled`
  }
}

Merchandising Rules

// Apply custom merchandising rules
if (searchClient.applyMerchandisingRules) {
  searchClient.applyMerchandisingRules = function (params) {
    params.append("boost", "featured:true")
    params.append("merchandising", "seasonal_promotion")
    params.append("pin", "productId:12345:1") // Pin product to position 1
  }
}

Custom Headers

// Set custom headers for SearchSpring requests
if (searchClient.setCustomHeaders) {
  searchClient.setCustomHeaders({
    "X-Custom-Header": "value",
    "X-Store-Context": "premium",
    Authorization: "Bearer token",
  })
}

Nosto Search Client

Custom Rules and Segments

// Set custom rules for Nosto search
if (searchClient.setCustomRules) {
  searchClient.setCustomRules([
    {
      field: "category",
      operation: "equals",
      value: "featured",
    },
    {
      field: "price",
      operation: "range",
      value: { min: 10, max: 100 },
    },
  ])
}

// Set user segments for personalization
if (searchClient.setSegments) {
  searchClient.setSegments([
    "premium_customers",
    "mobile_users",
    "returning_visitors",
  ])
}

// Set session parameters for tracking
if (searchClient.setSessionParams) {
  searchClient.setSessionParams({
    userId: "user123",
    sessionId: "session456",
    visitorId: "visitor789",
  })
}

Request Headers

// Set custom headers for Nosto requests
if (searchClient.setRequestHeaders) {
  searchClient.setRequestHeaders({
    "X-Custom-Header": "value",
    "X-User-Segment": "premium",
    "X-Device-Type": "mobile",
  })
}

GraphQL Operation Name

// Set GraphQL operation name for Nosto
if (searchClient.setOperationName) {
  searchClient.setOperationName("CustomProductSearch")
}

Request Parameters

// Customize request parameters for Nosto
if (searchClient.buildRequestParams) {
  const original = searchClient.buildRequestParams.bind(searchClient)

  searchClient.buildRequestParams = function (baseParams) {
    const params = original(baseParams)
    // Add custom logic
    params.customField = "customValue"
    params.personalization = true
    return params
  }
}

Response Formatting

// Custom response formatting for Nosto
if (searchClient.formatNostoResponse) {
  searchClient.formatNostoResponse = function (result) {
    // Return null to use default formatting, or return custom response
    const hits = result.search.products.hits || []
    const totalProducts = result.search.products.total || 0

    return {
      products: hits, // Will be processed through fetchProductsByNostoHits
      page: 0,
      totalPages: Math.ceil(totalProducts / this.productsPerPage),
      totalProducts,
      hasMore: hits.length === this.productsPerPage,
      nextPage: hits.length === this.productsPerPage ? 1 : null,
      // Add Nosto-specific metadata
      recommendations: result.recommendations || [],
      segments: result.segments || [],
    }
  }
}

Search Client Hooks: Selecting Providers and Usage

This provider exposes hooks that let blocks/components select which search provider to use at runtime. Supported providers:

  • algolia
  • nosto-search
  • search-spring
  • instant-search-plus

You can set a global provider for the entire layout or use a specific provider for an instance as needed.

useSearchContext: Select a Provider for the Layout

The useSearchContext hook optionally accepts a SearchClientType. Passing a type sets the active client for the whole layout. Calling it without a type returns the current active client.

import { useSearchContext } from "@/lib/context-providers/search-client-provider"

export function MyBlock() {
  // Select Algolia for the whole layout
  const { searchClient, isSearchClient, activeClientType } =
    useSearchContext("algolia")

  if (!isSearchClient || !searchClient) return null

  // Use the client as needed...
  return <div>Active client: {activeClientType}</div>
}

Example: ProductGrid selecting a provider

If your ProductGrid block uses the search client, you can target a specific provider:

import { useSearchContext } from "@/lib/context-providers/search-client-provider"

export function ProductGrid() {
  // Force Search Spring for the entire layout
  const { searchClient, isSearchClient } = useSearchContext("search-spring")
  if (!isSearchClient || !searchClient) return null

  // Render grid using the active search client
  return <div>{/* grid implementation */}</div>
}

Multiple Blocks Using the Search Client on the Same Layout

When multiple blocks call useSearchContext, prefer setting the provider once so all blocks align. There are two common patterns:

  • Controller block pattern (recommended): Have a single early-rendered block set the provider. Other blocks call useSearchContext() without a type and inherit the active client.
  • Direct selection (acceptable): Each block can call useSearchContext("<provider>"). Ensure they all pass the same provider to avoid thrashing.

Setting it once example:

import { useSearchContext } from "@/lib/context-providers/search-client-provider"

export function ProductGrid() {
  const { searchClient } = useSearchContext("nosto-search")
  if (!searchClient) return null
  return <div>{/* grid implementation */}</div>
}

You can also manually change the active provider later using the setActiveClientType function from the context if needed.

import { useSearchContext } from "@/lib/context-providers/search-client-provider"

export function Switcher() {
  const { setActiveClientType, activeClientType } = useSearchContext()
  return (
    <button onClick={() => setActiveClientType("algolia")}>
      Switch to Algolia (current: {activeClientType})
    </button>
  )
}

useSearchInstance: Create a Client Instance for Carousels or Scoped Queries

Use useSearchInstance for components that need their own client instance (e.g., carousels). By default it uses the active provider, but you can target a specific provider for that instance with the type option.

import { useSearchInstance } from "@/lib/context-providers/search-client-provider"

export function ProductCarousel({ collectionId }: { collectionId: string }) {
  // Use the active layout provider
  const carouselClient = useSearchInstance({
    searchParams: { collectionId },
    productsPerPage: 8,
  })

  if (!carouselClient) return null
  // Use carouselClient.get(...) etc.
  return <div>{/* carousel implementation */}</div>
}

Target a specific provider for the instance:

const carouselClient = useSearchInstance({
  searchParams: { collectionId },
  productsPerPage: 8,
  type: "search-spring", // Only this instance uses Search Spring
})

Tips

  • Prefer setting the provider once to keep the layout consistent.
  • Passing a stable literal to useSearchContext("<provider>") is safe; avoid toggling providers dynamically across blocks.