All pages
Powered by GitBook
1 of 6

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Using Search Templates

Using Search Templates

Why use Search Templates?

Search Templates allow you to add a search function to your website quickly and easily without the need to use an API. You can customize the design of your search pages and autocomplete boxes to match your brand's look and feel. This saves you a lot of time compared to implementing search functionality through an API.

Get Started

To get started with Search Templates on your website, navigate to the Search tab under 'On-site' within the Nosto UI. This is where everything for Search can be configured and controlled, including designing the templates for search pages and autocomplete dropdowns, as well as search analytics, merchandising rules and other settings. Synonyms for search queries can also be configured here.

If you prefer to develop the template in your local IDE of choice, we recommend you also take a look at . The CLI tool set allows you to develop the template on your machine with your own tools, and upload the build artifacts directly to Nosto.

To begin implementing Search, navigate to the Templates tab under Search, and Click on “Open Code Editor”.

You will then be redirected to the Code Editor window, where you can see and edit all project files.

Search Templates ship with a library called @nosto/preact that contains functionality to interact with the Nosto Search product. API documentation for the library is available

Project structure

Project structure has the following requirements:

  • index.js - this is application entry point that is used when building project. When building project it will recursively scan this file for imports.

  • build/ - this directory stores build output that will be used when deploying project.

Saving, testing & deploying

After saving changes (CTRL + S) build should be triggered and bundled code should be uploaded to CDN. You can preview final result on your website and deploy it when ready.

Nosto-CLI
here
https://github.com/Nosto/wiki-docs/blob/Techdocs/implementing-nosto/implement-search/implement-search-using-code-editor/broken-reference/README.md
Nosto Admin UI > Search
Code Editor

Implementing Category pages

Category pages can be rendered using search templates over existing category pages.

Configuration

To render the category page, provide additional configuration parameters to the init function in the index.js entry point file. Default configurations for categoryQuery and isCategoryPage are already provided. Custom configuration is necessary only if the default settings are not suitable for your application.

The default isCategoryPage function checks for the presence of an element in the DOM and determines if the page should be considered a category page based on its content.

Category query parameter as function

In the example above, we supply autocomplete query parameters as an object. Additionally, the categoryQuery parameter can also be supplied as a function. The function flavor can be used for building complex query parameters and provides access to other pre-defined configuration parameters. Section below shows an example of categoryQuery supplied as a function which provides the product variationId by accessing the pre-defined categoryId and categoryPath methods from the default configuration.

If you want to integrate only Categories without Search or Autocomplete, ensure that the following entries are removed or commented out:

serpComponent

historyComponent

autocompleteComponent

Handling native results

Nosto will attempt to display the original category page products in case Nosto service is unavailable or can't be reached. In addition, the original products are made available for the SEO crawlers, improving the page's ranking in the search engines. To make it possible, it's recommended to hide the original category page products instead of removing or blocking them.

The best approach is to add ns-content-hidden class name to the same element you are targeting with contentCssSelector or categoryCssSelector. This class name will be stripped away by Nosto automatically as soon as the script is initialized.

In addition, you should define CSS to hide the target element:

Detect search page

The isCategoryPage function should detect whether the current page is a category page. Its result should not be cached, as it needs to dynamically detect the page type since it can change during the app's execution. For example, if the user navigates from a category page to a search page, the function should reflect that change in page type.

Category query

The categoryQuery should generate a category page query based on the current page. It must return either categoryPath (identical to the categories field filter) or categoryId to select the current category. The default implementation extracts the categoryId and categoryPath fields from the DOM.

Category component

The category component should also be implemented. In most cases, it is the same component as the search page component, except that the search query should not be displayed.

Other features & implementation

The category page shares a lot of similarities with the search page, so please refer to the search page documentation:

Analytics

Search automatically tracks to Google Analytics & Nosto Analytics when using the SerpElement component.

Implementing Search page
index.js
import { init } from '@nosto/preact'
import categoryComponent from './category'

init({
    ...window.nostoTemplatesConfig,
    inputCssSelector: '#search',
    contentCssSelector: '#content', // or categoryCssSelector
    categoryComponent: categoryComponent,
    categoryCssSelector: '#MainContent',

    categoryQuery: {
      products: {
        categoryId: "1234567",
        categoryPath: "dresses",
        size: defaultConfig.serpSize,
        from: 0,
      }
    }
})
import { init } from '@nosto/preact'
import categoryComponent from './category'

init({
    ...window.nostoTemplatesConfig,
    inputCssSelector: '#search',
    contentCssSelector: '#content', // or categoryCssSelector
    categoryComponent: categoryComponent,
    categoryCssSelector: '#MainContent',

    categoryQuery: () => {
        return {
          products: {
            categoryId: this.categoryId(),
            categoryPath: this.categoryPath(),
            size: defaultConfig.serpSize,
            from: 0,
          }
        };
    }
})
css
.ns-content-hidden {
    display: none;
    /* Or other styles as needed */
}
category/index.jsx
import { useAppStateSelector } from '@nosto/preact'

export default () => {
    const { products, loading } = useAppStateSelector((state) => ({
        products: state.response.products,
        loading: state.loading,
    }))

    return (
        <div>
            {loading && <div>Loading...</div>}
            {products.total ? <div>
                {products.hits.map(hit => <div>
                    {hit.name}
                    {hit.price} 
                </div>)}
            </div> : <div>
                Category is empty
            </div>}
        </div>
    )
}
export default ({ product }) => {
    return (
        <SerpElement as="a" hit={product}>
            {product.name}
        </SerpElement>
    )
}

Implementing Search page

Configuration

To create a search application, call the init function with your configuration. This will create a new Preact application that renders on the specified contentCssSelector. It will also bind to the input element identified by the provided inputCssSelector and execute a search upon form submission.

Serp query parameter flavors

In the example above, we supply serp query parameters as an object. Additionally, the serpQuery parameter can also be supplied as a function. The function flavor can be used for building complex query parameters and provides access to other pre-defined configuration parameters.

Using variationId for price variations

When you have in use, provide the product variation ID by accessing the pre-defined variationId method from the default configuration:

Using currency for exchange rates

When you use exchange rates for multi-currency support, use the currency parameter instead:

The full list of Configuration options is documented

Search page redirect

When serpPathRedirect parameter is set to true, the application after search submission will redirect the browser to the search page specified in serpPath. Default behavior will only rewrite browser history to the specified path, without reloading the page.

In many cases, the search/autocomplete input is located on a different page from the search results. For example, on the landing or home page; or it may be always visible in the store's header. For those cases, it may be desired to redirect the user to the search results when a search request is submitted. If search page redirect is not configured, Nosto integration assumes that the search results should be rendered on the current page.

The redirect is controlled by two configuration variables:

  • serpPath (string) - specifies the path to the search page (follows the browser's location.pathname).

  • serpPathRedirect - (boolean or function) - combined variable that controls whether or not the redirect is enabled, and also provides a custom navigation mechanism if necessary.

When serpPathRedirect is omitted or set to false, the default behaviour is to update the browser's history (i.e. rewrite the current URL) to add the search query parameter.

When serpPathRedirect is set to true, the browser will redirect to the search page indicated by serpPath upon search submission. The default mechanism is location.href = {targetUrl} . If the current page already matches the search path, the search query parameter will be added instead.

When serpPathRedirect is set to a function, it will be called instead of setting location.href . This is useful to, for example, interact with your frontend framework, inject custom logic before redirect or handle special cases for redirect. For example:

In function form, serpPathRedirect exposes the main query object that holds the data which would instead be send to Nosto. The second object simply holds information about the click.

Checkout our API documentation on

Unbinding existing search input

To prevent events from firing on an existing input, you need to provide the CSS selector of the form that the input is in to the initialization configuration. When optional fromCssSelector is passed, it will unbind the form and the elements inside from existing events. Additionally, formUnbindDelay in milliseconds as value can be passed to delay the unbinding functionality.

Serp component

The search results page component should render a full search page using the provided app state. A minimal example might look like this:

Automatic URL Parameter Compression

When the compressUrlParameters flag is set to true, it automatically applies the URL parameter compression functions for filters, sort and pagination.

Checkout our API documentation on

@nosto/preact library has pre-built functions for changing search url format:

Description
Example

Product thumbnails

Product thumbnails are supported via decorators that augment the product data returned by the Nosto Search service.

The following example shows modifications to the init call to make product thumbnails available in the result data:

The thumbnailDecorator takes a size argument and requires the following additional fields to be made available in the result set for accurate thumbnails:

  • imageHash for imageUrl thumbnails

  • thumbHash for thumbUrl thumbnails

  • alternateImageHashes for alternateImageUrls

The supported sizes are

Code
Description

The same mapping will also be attempted for SKU level data

Checkout our API documentation on

Currency formatting

Currency formatting is implemented via the priceDecorator decorator function.

The priceDecorator utilizes the currency formatting definitions of the Nosto account to format prices into priceText and listPriceText fields, covering both product and SKU level data.

  • Include Required Fields

    • The fields required for this mapping are:

    • price will be formatted to priceText

A complete example of the Search-templates configuration for price variations:

For exchange rates, use currency instead:

Checkout our API documentation on

Multi-Currency

To enable multi-currency functionality in search templates, follow these steps:

  • Enable Multi-Currency in Nosto Admin -

  • Choose the appropriate parameter based on your setup:

    • Use variationId: this.variationId() when you have price variations in use

Price variations example

When using price variations, include the variationId parameter in your search query:

Exchange rates example

When using exchange rates, include the currency parameter in your search query:

Query parameter mapping

In addition to the compressUrlParameters flag the serpUrlMapping should be used to control the mapping from URL parameter keys to paths in the internal query object. The default looks like this:

The key is the internal path in the query model and the value is the query parameter name that should be used in the URL.

Features

Faceted navigation

Stats facet

The returns the minimum and maximum values of numerical fields from search results. This functionality is especially useful when creating interactive elements such as sliders and range selectors. For instance, a price slider can use these min-max values to define its adjustable range, providing a simple way for users to filter products within a specific price range. Similarly, these values are utilized in the RangeSelector to define the overall scope of range selections, allowing for the configuration of selection precision through the range size parameter.

Range Slider

Utilize the useRange ( or previously useRangeSlider ) hook to generate useful context for rendering range inputs. Additionally, employ the component to generate the interactive slider itself. These tools together facilitate the creation of dynamic and interactive range sliders for your application.

Example #1:

with useRange

Example #2

with useRangeSlider (legacy)

Checkout our API documentation for hook

useRangeSlider has been renamed to useRange but the older useRangeSlider name is still supported for backward compatibility.

Range Selector

If you require an alternative method where values are selected through radio buttons rather than a slider, consider using useRangeSelector hook. This tool allows users to choose from predefined range intervals with radio buttons, offering a different interaction style.

The range size parameter in the useRangeSelector hook specifies the size of each interval in the range and determines the total number of range items displayed. Additionally, it automatically rounds the minimum value down to ensure intervals are aligned with the specified range size.

For example, if the minimum product price in the current catalog is 230, and the maximum product price is 1000, the range size of 200 will adjust the starting point to 200 and create intervals displayed under the "Price" filter as follows:

  • 200 - 400

  • 400 - 600

  • 600 - 800

  • 800 - 1000

Checkout our API documentation on

Terms facet

The returns field terms for all products found in the search. This feature analyzes the content of each product and extracts meaningful terms. These terms can then be used to filter or refine search results, providing users with a more accurate and targeted product search.

You can use the toggleProductFilter function to toggle any filter value. This function will either add the filter value if it's not already applied or remove it if it's currently active, thus providing an efficient way to manipulate product filters in your application.

Checkout our API documentation on hook

Pagination

Use the usePagination hook to generate useful context for rendering any desired pagination. Utilize the width parameter to adjust how many page options should be visible. Also, remember to scroll to the top on each page change to ensure a seamless navigation experience for users.

Checkout our API documentation on hook

Infinite Scroll

Nosto search-templates library provides a simple out-of-the-box solution to implement infinite scroll functionality. Simply wrapping your product rendering with the <InfiniteScroll> component is generally enough.

As the user scrolls the page down, the wrapper will detect it using the IntersectionObserver. If it is not supported by the user's browser, a 'load more' button will be shown instead.

Infinite scroll works best when the product images have a pre-defined aspect ratio.

Checkout our API documentation on

Observer options

To achieve a smoother scrolling experience, the InfiniteScroll component accepts an optional prop called observerOptions. This prop allows you to customize the behavior of the , which is used to detect when the scroll trigger comes into view.

The observerOptions prop accepts the same parameters as the IntersectionObserver .

Persistent Search Cache

When using infinite scroll, consider enabling persistent search cache as well. When this feature is enabled, the latest search API response will be automatically cached and stored in the browser's session storage.

This improves the user experience significantly when the user navigates from a product details page back into the search results using the browser's 'back' function. The data necessary to display the products is already available, and the user will see the products immediately, without waiting for them to load again.

This feature is useful for both paginated and infinite scroll, but the benefits are significantly more visible with the latter.

Checkout our API documentation on

Product actions

Since the code editor utilizes the Preact framework, it offers significant flexibility in customizing behavior or integrating the search page with existing elements on your site. For instance, you can implement actions such as 'Add to Cart', 'Wishlist', or 'Quick View'.

Handling native results

Nosto will attempt to display the original search results in case Nosto service is unavailable or can't be reached. In addition, the original products are made available for the SEO crawlers, improving the page's ranking in the search engines. To make it possible, it's recommended to hide the original search results instead of removing or blocking them.

The best approach is to add ns-content-hidden class name to the same element you are targeting with contentCssSelector or categoryCssSelector. This class name will be stripped away by Nosto automatically as soon as the script is initialized.

In addition, you should define CSS to hide the target element:

Analytics

Search automatically tracks to Google Analytics & Nosto Analytics when using SerpElement component.

Component parameters:

The SerpElement component supports any other HTML attribute, e.g. class.

Checkout our API documentation on

Fallback Functionality

The search page incorporates built-in fallback functionality, allowing users to customize the behavior in case the search service encounters issues. To activate this feature, modify the initialization configuration to include the fallback: true key-value pair.

Enabling Fallback

To enable fallback functionality, include the following code in the initialization configuration:

Once fallback is enabled, if the search request fails to retrieve data, the search functionality will be temporarily disabled for 10 minutes, and the original content Nosto has overridden will be restored.

The fallback: true setting only works out of the box if the path is the same for both the dedicated Nosto search page and the native search page, as well as for category pages.

If the paths differ, you must configure the serpFallback or categoryFallback function to ensure proper redirection. See:

Alternative Fallback Behavior

If the behavior described above is undesirable, the configuration supports an alternative option. Fallback mode can be set to fallback: 'legacy', in which case the user will see a page reload if the search request fails. After that, Nosto will not attempt to override the original search results or category pages for 10 minutes.

This behavior has been the default fallback behavior before August 20, 2024.

Customizing Fallback Location

Additionally, it's possible to customize the location to which users are redirected when the search functionality is unavailable. This customization involves specifying functions for both the search engine results page (SERP) and category pages.

SERP Fallback

To redirect users to a specific location when the search engine is down, define a function for serpFallback. This function accepts one parameter containing information about the current search query, including the query itself.

Category Fallback

Similarly, for category pages, define a function for categoryFallback. This function also accepts one parameter containing information about the current query, including the category ID or Path.

By customizing these fallback locations, you can enhance the user experience by providing them with alternative navigation options if the search functionality is temporarily unavailable.

Search engine configuration

Nosto Search engine is relevant out of the box and search API can be used without any initial setup. Nosto Dashboard can be used to further tune search engine configuration:

  • - manage which fields are used for search and their priorities,

  • - create facets (filtering options) for search results page,

  • Ranking and Personalization - manage how results are ranked,

index.js
import { init } from '@nosto/preact'

import serpComponent from './serp'

init({
    ...window.nostoTemplatesConfig,
    serpComponent,
    inputCssSelector: '#search',
    contentCssSelector: '#content',
    serpPath: '/search',
    serpPathRedirect: true,
    formCssSelector: '#search-form',
    formUnbindDelay: 1000, // 1 second
    serpUrlMapping: {
        query: 'q',
    },
    serpQuery: {
        products: {
            size: 20,
            from: 0,
        },
    },
})
thumbnails
  • sku.imageHash for sku.imageUrl thumbnails

  • 7

    200x200 px

    8

    400x400 px

    9

    750x750 px

    10

    Original (Square)

    11

    200x200 px (Square)

    12

    400x400 px (Square)

    13

    750x750 px (Square)

    listPrice will be formatted to listPriceText
  • priceCurrencyCode will be used as the currency code

  • Use the priceDecorator The priceDecorator is responsible for formatting prices into text fields using above mentioned fields.

  • Use currency: this.variationId() when you use exchange rates for multi-currency support
    Synonyms, Redirects, and other search features are also managed through Nosto Dashboard (my.nosto.com).

    Pagination

    Replaces from parameter with page number.

    Before: /search?products.from=20&q=shorts After: /search?page=2&q=shorts

    Sorting

    Returns shorter sort parameters.

    Before: /search?q=shorts&products.sort.0.field=price&products.sort.0.order=desc After: /search?q=shorts&products.sort=price~desc

    Filtering

    Compresses filter parameters. Multiple filter values are separated by a comma, which is encoded. This is because filter values can contain non-alphanumeric letters themselves.

    Before: /search?q=shorts&products.filter.0.field=customFields.producttype&products.filter.0.value.0=Shorts&products.filter.0.value.1=Swim&products.filter.1.field=price&products.filter.1.range.0.gte=10&products.filter.1.range.0.lte=30 After: /search?q=shorts&filter.customFields.producttype=Shorts%7C%7CSwim&filter.price=10~30

    1

    170x170 px

    2

    100x100 px

    3

    90x70 px

    4

    50x50 px

    5

    30x30 px

    6

    100x140 px

    hit

    Product object.

    as

    Element to render <SerpElement /> as. Recommended to use as="a". If a element is used, href attribute is added automatically.

    onClick (optional)

    Additional onClick callback (tracking callback is already implemented in the component).

    price variations
    here
    serpPathRedirect
    compressUrlParameters
    thumbnailDecorator
    priceDecorator
    Enabling multi-currency from the admin
    stats facet
    useRangeSlider
    useRangeSelector
    terms facet
    useActions
    usePagination
    InfiniteScroll
    Intersection Observer API
    options
    persistentSearchCache
    SerpElement
    Customizing Fallback Location
    Searchable Fields
    Facets
    Ranking and Personalization
    index.js
    import { init } from '@nosto/preact'
    
    import serpComponent from './serp'
    
    init({
        ...window.nostoTemplatesConfig,
        serpComponent,
        inputCssSelector: '#search',
        contentCssSelector: '#content',
        serpPath: '/search',
        serpPathRedirect: true,
        formCssSelector: '#search-form',
        formUnbindDelay: 1000, // 1 second
        serpUrlMapping: {
            query: 'q',
        },
        serpQuery() {
            return {
                products: {
                    size: 20,
                    from: 0,
                    variationId: this.variationId()
                },
            }
        }
    })
    index.js
    import { init } from '@nosto/preact'
    
    import serpComponent from './serp'
    
    init({
        ...window.nostoTemplatesConfig,
        serpComponent,
        inputCssSelector: '#search',
        contentCssSelector: '#content',
        serpPath: '/search',
        serpPathRedirect: true,
        formCssSelector: '#search-form',
        formUnbindDelay: 1000, // 1 second
        serpUrlMapping: {
            query: 'q',
        },
        serpQuery() {
            return {
                products: {
                    size: 20,
                    from: 0,
                    currency: this.variationId()
                },
            }
        }
    })
    init({
        serpPath: '/search',
        serpPathRedirect: (query: SearchQuery, options: AutocompleteOptions | undefined) => {
            location.href = `https://store.com/search/${query?.query}` // Query as a path param
        },
    })
    export interface AutocompleteOptions {
        isKeyword?: boolean // true if the user clicked on a suggested keyword
    }
    serp/index.js
    import { useAppStateSelector, SerpElement } from '@nosto/preact'
    
    export default () => {
        const { products, loading } = useAppStateSelector((state) => ({
            products: state.response.products,
            loading: state.loading,
        }))
    
        return (
            <div>
                {loading && <div>Loading...</div>}
                {products.total ? <div>
                    {products.hits.map(hit => <SerpElement as="a" hit={hit}>
                        {hit.name}
                        {hit.price} 
                    </SerpElement>)}
                </div> : <div>
                    No results were found
                </div>}
            </div>
        )
    }
    import { init } from '@nosto/preact'
    
    import serpComponent from './serp'
    
    init({
        ...window.nostoTemplatesConfig,
        serpComponent,
        inputCssSelector: '#search',
        contentCssSelector: '#content',
        serpPath: '/search',
        serpPathRedirect: false,
        serpUrlMapping: {
            query: 'q',
            'products.page': 'page'
        },
        compressUrlParameters: true,
        serpQuery: {
            products: {
                size: 20,
                from: 0,
            },
        },
    })
    import { init, thumbnailDecorator, priceDecorator } from "@nosto/preact"
    
    init({
        ...window.nostoTemplatesConfig,
        ...
        serpQuery: {
            products: {
                fields: [
                    ...
                    // needed for thumbnailDecorator
                    "imageHash"
                ],
                facets: ["*"],
                size: defaultConfig.serpSize,
                from: 0
            }
        },    
        hitDecorators: [
            thumbnailDecorator({ size: "9" })
        ]
    })
    import { init, priceDecorator } from "@nosto/preact";
    
    init({
        ...window.nostoTemplatesConfig,
        ...
        serpQuery() {
            return {
                products: {
                    variationId: this.variationId(),
                    fields: [
                        // needed for priceDecorator
                        "price", 
                        "listPrice",
                        "priceCurrencyCode",
                    ],
                    size: 20,
                    from: 0
                }
            }    
        },
        hitDecorators: [
            priceDecorator()
        ]
    });
    import { init, priceDecorator } from "@nosto/preact";
    
    init({
        ...window.nostoTemplatesConfig,
        ...
        serpQuery() {
            return {
                products: {
                    currency: this.variationId(),
                    fields: [
                        // needed for priceDecorator
                        "price", 
                        "listPrice",
                        "priceCurrencyCode",
                    ],
                    size: 20,
                    from: 0
                }
            }    
        },
        hitDecorators: [
            priceDecorator()
        ]
    });
    import { init } from "@nosto/preact";
    
    init({
        ...window.nostoTemplatesConfig,
        ...
        serpQuery() {
            return {
                products: {
                    variationId: this.variationId()
                    ...
                }
            }
        }
    });
    import { init } from "@nosto/preact";
    
    init({
        ...window.nostoTemplatesConfig,
        ...
        serpQuery() {
            return {
                products: {
                    currency: this.variationId()
                    ...
                }
            }
        }
    });
    serpUrlMapping: {
      query: "q",
      "products.filter": "filter",
      "products.page": "page",
      "products.sort": "sort"
    }
    import { useRange } from "@nosto/search-js/preact/hooks";
    import { useState } from "react";
    
    const Component = ({ facetId }) => {
      const { min, max, range, active, toggleActive, updateRange } = useRange(facetId);
      
      return (
        <div>
          <button onClick={() => toggleActive()}>
            {active ? "Hide" : "Show"} Range Filter
          </button>
          {active && (
            <div>
              Current Range: {range[0]} to {range[1]}
              <button onClick={() => updateRange([min, max])}>Reset Range</button>
            </div>
          )}
        </div>
      );
    };
    
    import { RangeSlider, useRangeSlider } from '@nosto/preact'
    
    export default ({ facet }) => {
        const {
            min,
            max,
            range,
            updateRange
        } = useRangeSlider(facet.id)
    
        return  <div>
            <h2>{facet.name}</h2>
            <label>
                Min.
                <input type="number" value={range[0]} min={min} max={max} onChange={(e) => {
                    const value = parseFloat(e.currentTarget.value) || undefined
                    updateRange([value, range[1]])
                }}/>
            </label>
            <label>
                Max.
                <input type="number" value={range[1]} min={min} max={max} onChange={(e) => {
                    const value = parseFloat(e.currentTarget.value) || undefined
                    updateRange([range[0], value])
                }} />
            </label>
            <RangeSlider id={facet.id} />
        </div>
    }
    import { useRangeSelector } from "@nosto/preact"
    import { useState } from "preact/hooks"
    import RangeInput from "./elements/RangeInput"
    import Icon from "./elements/Icon"
    import RadioButton from "./elements/RadioButton"
    
    export default function RangeSelector({ facet }) {
        const {
            min,
            max,
            range,
            ranges,
            updateRange,
            handleMinChange,
            handleMaxChange,
            isSelected
        } = useRangeSelector(facet.id, 100)
        const [active, setActive] = useState(false)
    
        return (
            <li>
                <div>
                    <ul>
                        {ranges.map(({ min, max, selected }, index) => {
                            return (
                                <li
                                >
                                    <RadioButton
                                        key={index}
                                        value={`${min} - ${max}`}
                                        selected={selected}
                                        onChange={() => updateRange([min, max])}
                                    />
                                </li>
                            )
                        })}
                        <div>
                            <div>
                                <label for={`ns-${facet.id}-min`}>
                                    Min.
                                </label>
                                <RangeInput
                                    id={`ns-${facet.id}-min`}
                                    min={min}
                                    max={max}
                                    range={range}
                                    value={range[0] ?? min}
                                    onChange={e => handleMinChange(parseFloat(e.currentTarget.value) || min)}
                                />
                            </div>
                            <div>
                                <label for={`ns-${facet.id}-max`}>
                                    Max.
                                </label>
                                <RangeInput
                                    id={`ns-${facet.id}-max`}
                                    min={min}
                                    max={max}
                                    range={range}
                                    value={range[1] ?? max}
                                    onChange={e => handleMaxChange(parseFloat(e.currentTarget.value) || max)}
                                />
                            </div>
                        </div>
                    </ul>
                </div>
            </li>
        )
    }
    import { useActions } from '@nosto/preact'
    
    export default ({ facet }) => {
        const { toggleProductFilter } = useActions()
    
        return <div>
            <h2>{facet.name}</h2>
            <ul>
                {facet.data?.map((value) => <li>
                    <label>
                        {value.value}
                        <input
                            type="checkbox"
                            checked={value.selected}
                            onChange={(e) => {
                                e.preventDefault()
                                toggleProductFilter(
                                    facet.field,
                                    value.value,
                                    !value.selected
                                )
                            }}
                        />
                    </label>
                    ({value.count})
                </li>)}
            </ul>
        </div>
    }
    serp/pagination.jsx
    import { usePagination, useActions } from '@nosto/preact'
    
    export default () => {
        const pagination = usePagination({
            width: 5
        })
        const { updateSearch } = useActions()
        
        const createCallback = (from) => () => {
            updateSearch({
                products: {
                    from,
                },
            })
            scrollTo(0, 0)
        }
    
        return (
            <ul>
                {pagination.prev && <li>
                    <a
                        href="javascript:void(0)"
                        onClick={createCallback(pagination.prev.size)}
                    >
                        prev
                    </a>
                </li>}
                {pagination.first && <li>
                    <a
                        href="javascript:void(0)"
                        onClick={createCallback(pagination.first.from)}
                    >
                        {pagination.first.page}
                    </a>
                </li>}
                {pagination.first && <li>...</li>}
                {pagination.pages.map((page) => <li class={page.current ? "active" : ""}>
                    <a
                        href="javascript:void(0)"
                        onClick={createCallback(page.from)}
                    >
                        {page.page}
                    </a>
                </li>)}
                {pagination.last && <li>...</li>}
                {pagination.last && <li>
                    <a
                        href="javascript:void(0)"
                        onClick={createCallback(pagination.last.from)}
                    >
                        {pagination.last.page}
                    </a>
                </li>}
                {pagination.next && <li>
                    <a
                        href="javascript:void(0)"
                        onClick={createCallback(pagination.next.offset)}
                    >
                        <span aria-hidden="true">
                            <i class="ns-icon ns-icon-arrow"></i>
                        </span>
                    </a>
                </li>}
            </ul>
        )
    }
    serp.jsx
    function Products() {
        const products = useAppStateSelector(state => state.response.products)
    
        return (
            <>
                {products.hits.map((hit, index) => {
                    return <Product product={hit} key={hit.productId ?? index} />
                })}
            </>
        )
    }
    
    function SerpInfiniteScroll() {
        return (
            <InfiniteScroll>
                <Products />
            </InfiniteScroll>
        )
    }
    serp.jsx
        <InfiniteScroll observerOptions={{
            rootMargin: "100px"
        }}>
            <Products />
        </InfiniteScroll>
    import { init } from '@nosto/preact'
    
    init({
        ...otherFields,
        persistentSearchCache: true,
    })
    serp/Product.jsx
    import { SerpElement } from '@nosto/preact'
    import { useState } from 'preact/hooks'
    
    export default ({ product }) => {
        const [addedToCart, setAddedToCart] = useState(false)
        
        return (
            <SerpElement
                as="a"
                hit={product}
            >
                <img src={product.imageUrl} />
                <div>
                    {product.name}
                </div>
                <button
                    // Allow the button to be clicked only once
                    disabled={addedToCart}
                    // Add the product to the cart when the button is clicked
                    onClick={(event) => {
                        // Don't navigate to the product page
                        event.preventDefault()
    
                        // Update the button text and disable it
                        setAddedToCart(true)
    
                        // Add the product to the cart, this depends on the cart implementation
                        jQuery.post('/cart/add.js', {
                            quantity: 1,
                            id: product.productId,
                        })
                    }}
                >
                    // Show different text if product was added to the cart
                    {addedToCart ? 'Added to cart' : 'Add to cart'}
                </button>
            </SerpElement>
        )
    }
    css
    .ns-content-hidden {
        display: none;
        /* Or other styles as needed */
    }
    export default ({ product }) => {
        return (
            <SerpElement as="a" hit={product}>
                {product.name}
            </SerpElement>
        )
    }
    import { init } from '@nosto/preact'
    
    init({
        ...window.nostoTemplatesConfig,
        fallback: true,
    })
    import { init } from '@nosto/preact'
    
    init({
        ...window.nostoTemplatesConfig,
        fallback: 'legacy',
    })
    import { init } from '@nosto/preact'
    
    init({
        ...window.nostoTemplatesConfig,
        fallback: true,
        serpFallback: (searchQuery) => {
            location.replace(`/search?q=${searchQuery.query}`);
        },
    })
    import { init } from '@nosto/preact';
    
    init({
        ...window.nostoTemplatesConfig,
        fallback: true,
        categoryFallback: (query) => {
            location.replace(`/categories/${query.products.categoryId}`);
        },
    });

    Implementing Autocomplete

    Autocomplete is an element shown under search input used to display keywords and products for a partial query.

    Example Autocomplete

    Check out autocomplete's look & feel guidelines.

    Configuration

    To enable autocomplete, additional configuration should be passed to init function.

    Autocomplete query parameter as function

    In the example above, we supply autocomplete query parameters as an object. Additionally, the autocompleteQuery parameter can also be supplied as a function. The function flavor can be used for building complex query parameters and provides access to other pre-defined configuration parameters.

    Using variationId for price variations

    When you have price variations in use, provide the product variation ID by accessing the pre-defined variationId method from the default configuration:

    Using currency for exchange rates

    When you use exchange rates for multi-currency support, use the currency parameter instead:

    Customizing dropdown position

    When the autocomplete component is injected, by default it will become the next sibling of the input field. It is possible to override that behavior by specifying the dropdownCssSelector value. If this selector is specified, the dropdown will be injected as the last child of the specified element.

    It can also be set to be the first child of the element by using the object selector syntax.

    The full list of Configuration options is documented

    Autocomplete component

    Voice to text search

    To implement voice to text search in search templates, additional configuration params need to be provided:

    Configuration parameters:

    • speechToTextComponent – The component that renders the voice search button.

    • speechToTextEnabled – A flag to enable the voice search feature, disabled by default

    The voice search button will be injected adjacent to the search input field, positioned as an overlay on the right end of the input.

    Within the button component, the useSpeechToText hook is used to toggle voice input on and off.

    The @nosto/preact package exports two useful utilities:

    • useSpeechToText – A hook to control the voice-to-text functionality.

    • speechToTextSupported – A variable indicating whether the current environment supports the feature.

    Element selection

    Wrap each keywords and product to AutocompleteElement element - it will allow clicking or selecting the element directly with keyboard.

    Search submit

    To submit a search directly from the autocomplete, use the <button type="submit"> element. This will submit the search form.

    History component

    History component renders user search history. It is displayed when user clicks on empty search box.

    HistoryElement renders clickable element that triggers search event with provided query.

    Analytics

    Autocomplete automatically tracks to Google Analytics & Nosto Analytics when using <AutocompleteElement /> component.

    index.js
    import { init } from '@nosto/preact'
    
    import autocompleteComponent from './autocomplete'
    import historyComponent from './history'
    
    init({
        ...window.nostoTemplatesConfig,
        historyComponent,
        autocompleteComponent,
        inputCssSelector: '#search',
        autocompleteQuery: {
            name: 'autocomplete',
            products: {
                size: 5,
            },
            keywords: {
                size: 5,
                fields: [
                    'keyword', '_highlight.keyword'
                ],
            },
        }
    })
    here
    index.js
    import { init } from '@nosto/preact'
    
    import autocompleteComponent from './autocomplete'
    import historyComponent from './history'
    
    init({
        ...window.nostoTemplatesConfig,
        historyComponent,
        autocompleteComponent,
        inputCssSelector: '#search',
        autocompleteQuery() {
            return {
                name: 'autocomplete',
                products: {
                    size: 5,
                    variationId: this.variationId()
                },
                keywords: {
                    size: 5,
                    fields: [
                        'keyword', '_highlight.keyword'
                    ],
                },
            }
        }
    })
    index.js
    import { init } from '@nosto/preact'
    
    import autocompleteComponent from './autocomplete'
    import historyComponent from './history'
    
    init({
        ...window.nostoTemplatesConfig,
        historyComponent,
        autocompleteComponent,
        inputCssSelector: '#search',
        autocompleteQuery() {
            return {
                name: 'autocomplete',
                products: {
                    size: 5,
                    currency: this.variationId()
                },
                keywords: {
                    size: 5,
                    fields: [
                        'keyword', '_highlight.keyword'
                    ],
                },
            }
        }
    })
    index.js
    import { init } from '@nosto/preact'
    
    init({
        // ...
        inputCssSelector: '#search',
        dropdownCssSelector: 'body',
    })
    index.js
    import { init } from '@nosto/preact'
    
    init({
        // ...
        inputCssSelector: '#search',
        dropdownCssSelector: {
            selector: 'body',
            position: 'first', // 'first' or 'last'
        },
    })
    autocomplete/index.jsx
    import { useAppStateSelector, AutocompleteElement } from '@nosto/preact'
    
    export default () => {
        const { products, keywords } = useAppStateSelector((state) => ({
            products: state.response.products,
            keywords: state.response.keywords
        }))
    
        if (!products?.hits?.length && !keywords?.hits?.length) {
            return
        }
    
        return (
            <div>
                {keywords?.hits?.length > 0 && <div>
                    <div>
                        Keywords
                    </div>
                    <div>
                        {keywords.hits.map((hit) => (
                            <AutocompleteElement hit={hit} key={hit.keyword}>
                                {
                                    hit?._highlight?.keyword
                                        ? <span dangerouslySetInnerHTML={{ __html: hit._highlight.keyword }}></span>
                                        : <span>{hit.keyword}</span>
                                }
                            </AutocompleteElement>
                        ))}
                    </div>
                </div>}
                {products?.hits?.length > 0 && <div>
                    <div>
                        Products
                    </div>
                    <div>
                        {products.hits.map((hit) => (
                            <AutocompleteElement hit={hit} key={hit.productId} as="a">
                                <img src={hit.imageUrl}/>
                                <div>
                                    {hit.name}
                                </div>
                                <button
                                    // Allow the button to be clicked only once
                                    disabled={addedToCart}
                                    // Add the product to the cart when the button is clicked
                                    onClick={(event) => {
                                        // Don't navigate to the product page
                                        event.preventDefault()
    
                                        // Update the button text and disable it
                                        setAddedToCart(true)
    
                                        // Add the product to the cart, this depends on the cart implementation
                                        jQuery.post("/cart/add.js", {
                                            quantity: 1,
                                            id: product.productId
                                        })
                                    }}
                                >
                                    // Show different text if product was added to the cart
                                    {addedToCart ? "Added to cart" : "Add to cart"}
                                </button>
                            </AutocompleteElement>
                        ))}
                    </div>
                </div>}
                <div>
                    <button type="submit">
                        See all search results
                    </button>
                </div>
            </div>
        )
    }
    index.js
    import { init } from '@nosto/preact'
    
    import speechToTextComponent from "./SpeechToTextComponent"
    
    init({
        ...window.nostoTemplatesConfig,
        speechToTextComponent,
        speechToTextEnabled: true,
        ...
    })
    history/index.jsx
    import { useAppStateSelector, HistoryElement } from '@nosto/preact'
    
    export default () => {
        const historyItems = useAppStateSelector((state) => state.historyItems)
    
        if (!historyItems || !historyItems.length) {
            return
        }
    
        return (
            <div>
                <div>Recently Searched</div>`
                <div>
                    {historyItems.map((item) => (
                        <HistoryElement query={{ query: item }}>
                            {item}
                        </HistoryElement>
                    ))}
                </div>
            </div>
        )
    }

    FAQ

    Since Search Templates utilize the Search API under the hood, please also check the Search API FAQ

    Share search templates code between multiple accounts

    The functionality enables the sharing of a single instance of search templates across multiple dashboard accounts through linking. This feature proves particularly advantageous in scenarios where there are multiple websites with identical or similar characteristics that necessitate the creation of multiple dashboard accounts (such as supporting multiple languages or environments).

    To link multiple accounts contact support!

    How to implement some differences between websites?

    Even if multiple websites share the same search templates code, we can easily have different logic based on website host or language (or other criteria).

    Different CSS between websites

    Since it's not possible to target CSS rules by domain, it's recommended to add a special class to the page body and use it to target CSS rules.

    Example JS

    Example CSS:

    Show specific SKU when searching by SKU color or ID

    By default, the search indexes all SKUs as a single product. Therefore, even when searching by a specific SKU attribute (like SKU color or ID), the search will return the main product along with all SKUs.

    However, because the search returns all SKUs, SKU selection logic can be implemented on the frontend side. This is achieved by checking each SKU to see if it contains text from the search query.

    Implementation

    These is an example on how to implement SKU selection:

    Use more than 100 facet values

    By default, the search API returns 100 values for each facet, and it's not possible to control that limit in the dashboard. However, it is possible to override directly from the code.

    Get facet ID from the dashboard

    The first step is to know the facet ID of the facet you want to overwrite. The facet ID is stored in the dashboard URL. For example, if you open the facet in the facet manager, the URL should be https://my.nosto.com/admin/shopify-123/search/settings/facetManager/6406df867f8beb629fc0dfb9. This means the facet ID is 6406df867f8beb629fc0dfb9.

    Override facet settings

    Once the ID is known, it's possible to override any facet setting by specifying overwrite properties in the customFacets parameter:

    SEO

    Although search engines can understand some JavaScript-rendered code, they often miss search templates. The reason for this is that the rendering of search templates is delayed in order not to hinder the page loading speed.

    However, it's still possible to achieve great SEO results while using the code editor:

    • Category pages - Ensure that the category page already returns the correct meta tags and page title according to , they bring the biggest impact to the SEO. In category page search templates render only products, so it doesn't significantly impact SEO. Most search engines support (the category page backend should generate these tags using original products). However it does not have any big impact to the SEO. Furthermore, search engines may not favor the discrepancy between structured data and the actual rendered products (cause search engine will see either empty page with loader or nosto search results that are different compared to original).

    • Search pages - Most search engines don't index search pages, so no optimizations are needed.

    If you still have concerns regarding SEO, please consider using .

    the facet configuration
    SEO recommendations
    structured data
    API integration
    function detectWebsite() {
        if (location.hostname === 'de.website.com') {
            return 'de'
        } else if (document.documentElement.lang === 'de') {
            return 'de'
        }
        return 'en'
    }
    
    const helloText = {
        de: 'Hallo',
        en: 'Hello'
    }[detectWebsite()]
    init({
        ...
    }).then(() => {
        if (location.hostname === 'de.website.com') {
            document.body.classList.add('nosto-search-de')
        }
    })
    .nosto-search-de .my-title {
        color: black;
    }
    function getSku(query, product) {
        const words = query.toLowerCase().split(/\s+/)
    
        return product?.skus?.find((sku) => {
            // If the query is a SKU ID, return the SKU
            if (sku.id == query) {
                return true
            }
            const color = sku?.customFields?.color?.toLowerCase()?.split(/\s+/)
    
            return words.some((word) => color.includes(word))
        })
    }
    
    export default ({ product }) => {
        const query = useAppStateSelector((state) => state.query.query)
        const sku = getSku(query, product)
    
        return <a href={sku?.url || product.url}>
            <img src={sku?.imageUrl || product.imageUrl}/>
    
            {sku?.name || product.name}
        </a>
    }
    index.js
    init({
        serpQuery: {
            products: {
                customFacets: [
                    {
                        "id": "6406df867f8beb629fc0dfb9",
                        "size": 10
                    }
                ]
            },
        }
    })