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.

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

import serpComponent from './serp'

init({
    ...window.nostoTemplatesConfig,
    serpComponent,
    inputCssSelector: '#search',
    contentCssSelector: '#content',
    serpPath: '/search',
    serpPathRedirect: false,
    formCssSelector: '#search-form',
    formUnbindDelay: 1000, // 1 second
    serpUrlMapping: {
        query: 'q',
    },
    serpQuery: {
        name: 'serp',
        products: {
            size: 20,
            from: 0,
        },
    },
})

The full list of Configuration options is documented here

Search page path

When serpPath parameter is specified, the application will redirect to the specified search page after a search is conducted. Otherwise, the search will be rendered on the same page without changing the URL path.

Search page redirect

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

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:

serp/index.js
import { useAppState, SerpElement } from '@nosto/preact'

export default () => {
    const { response: { products }, loading } = useAppState()

    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>
    )
}

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.

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: {
        name: 'serp',
        products: {
            size: 20,
            from: 0,
        },
    },
})

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

DescriptionExample

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

Features

Faceted navigation

Stats facet

The stats facet returns the minimum and maximum values of numerical fields from search results. This functionality is especially useful when creating interactive elements like sliders. For instance, a price slider can leverage these min-max values to define its range, providing users a simple way to filter products within their desired price range.

serp/facets/stats.js
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>
}

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

Terms facet

The terms facet 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.

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>
}

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.

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.

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>
    )
}

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'.

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>
    )
}

Analytics

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

export default ({ product }) => {
    return (
        <SerpElement as="a" hit={product}>
            {product.name}
        </SerpElement>
    )
}

Component parameters:

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).

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

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:

import { init } from '@nosto/preact'

init({
    ...window.nostoTemplatesConfig,
    fallback: true,
})

Once fallback is enabled, if the search request fails to retrieve data, the search functionality will be temporarily disabled for a short period, and the user will be redirected to the same page they are currently on.

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.

import { init } from '@nosto/preact'

init({
    ...window.nostoTemplatesConfig,
    fallback: true,
    serpFallback: (searchQuery) => {
        location.replace(`/search?q=${searchQuery.query}`);
    },
})

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.

import { init } from '@nosto/preact';

init({
    ...window.nostoTemplatesConfig,
    fallback: true,
    categoryFallback: (query) => {
        location.replace(`/categories/${query.products.categoryId}`);
    },
});

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:

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

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

  • Ranking and Personalization Ranking and Personalization - manage how results are ranked,

  • Synonyms, Redirects, and other search features are also managed through Nosto Dashboard (my.nosto.com).

Last updated