Only this pageAll pages
Powered by GitBook
Couldn't generate the PDF for 167 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

Techdocs

Quick Start Guide

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Implementing Nosto

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

APIs

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Welcome and support

We are glad to have you as a partner and want to help you get running with Nosto as soon as possible while making it as easy as possible for you.

Our material consists of a general introduction to how Nosto works, additional in-depth information and examples for every product with its possible implementation methods as well as comprehensive API references.

We appreciate your professionalism and understanding that we can't document every single use case. We are confident you'll find everything needed to deliver on your requirements. Custom cases need custom solutions and we'll guide you in the right direction.

If you ever feel stuck, something is confusing or misleading, please so we can a) support you and b) update our information.

contact us

Customizations

How to get the most out of our guides and Nosto

It is tempting to skim the headings of a documentation (we feel you and know you want to get back to coding asap). We also understand that you want to make sure you can cover all the complexity for your client and site.

From our experience, there is always added frustration and costs due to several added feedback loops when we take shortcuts in the beginning. You and your client will get the most out of your time and the Nosto platform when you:

  • Set aside one hour to read this guide and write down all questions or concerns you might have.

  • Use our onboarding team to consult and assist you with best practices. We are there to help you with troubleshooting.

  • Focus on the basics first before addressing the complex use cases (although we know they are more fun).

  • Use the product as it is intended. This is the most common pitfall: After hours and hours of complex programming it often turns out there already is a functionality inside the platform which is configured with only a few clicks.

  • Don't just copy & paste the code examples. Yes, it happens. To improve readability, our examples give you an overview with only the most relevant data/parameters. We don't want to clutter your mind, so please refer to the API references for an extensive overview.

We are happy to with any question regarding the details and are confident you will be able to solve 90% of your questions with this guide.

Pro Tip: Let us review your project plan/milestones before you get started.

We can't stress this enough: When the basics are missing, the more advanced features won't work as intended and you will become very frustrated.

Therefore, please make sure to fill up your brand-new car with the right fuel before taking it for a spin around the neighborhood.

What Nosto needs

Fundamentally, Nosto needs a replica of the client's product catalog with price and currency information, parent/child relations, categories, custom fields, tags, stock/inventory information, etc..

To achieve the onsite functionalities, Nosto must be able to do these things:

  1. Know on which page the users are.

  2. Know what data is on a page (what product, what category, …).

How you ensure a clean and quick implementation

Nosto offers different APIs to use and they all mostly offer the same functionality (in some cases you’ll face a classical trade-off battle). That’s why it’s important to not just look at what you want to achieve, but what setup you currently have (and what your client needs).

We want to make the implementation as easy and clean for you as possible. We don’t want you to bloat up your code or you to do unnecessary things.

Therefore, please take a few minutes and write down the answers to the following questions so we can prevent future headaches (and save a lot of time and money):

  1. Is your client’s site running on a major eCommerce platform like Magento 2, Shopify, Shopware, BigCommerce, Prestashop or Salesforce?

Know specific things about the current user (on all pages):

  • Shopping cart content

  • Name and email (if logged in)

  • Know about certain events like impressions and clicks.

  • Write content to the page, either from the client- or server side.

  • Access your frontend: Our crawler must be whitelisted, you must provide valid product URLs and product images.

  • Have you decoupled the frontend from the backend/are you running a headless frontend? (incl. Magento 2 Hyvä, React, Next.js or Shopify Hydrogen)

  • Is the frontend a single-page-application (SPA) or a progressive-web-app (PWA) where the URL never changes when a user browses the site?

  • Does your client have an international/localized setup with multiple currencies and/or languages?

  • Does your client have multiple customer groups?

  • We will ask you for these answers in our kickoff meeting and will craft the mutual project plan accordingly.

    support you

    Using the API

    Implement Autocomplete using the Nosto Autocomplete library

    Nosto Autocomplete library is designed to simplify the implementation of Search Autocomplete functionality by providing:

    • Autocomplete products, keywords and history visualization.

    • Automatic bindings to Nosto Search API.

    • Autocomplete component state management.

    • Nosto Analytics out of the box, Google Analytics support.

    • Default Autocomplete components and templates.

    • Keyboard navigation.

    The Nosto Autocomplete library is independent from the Search Templates offering which covers Search, Category Merchandising and Autocomplete.

    Search Templates offers a hosted development environment based on Visual Studio Web using Preact components for the development of Search and Category Merchandising result pages, as well as Autocomplete experiences. Nosto Autocomplete covers only the Autocomplete part as an independent NPM package and provides integration into various rendering technologies such as React/Preact, Mustache and Liquid.

    Further reading

    See library TypeDoc page to see more detailed documentation of the library.

    Features

    This section offers implementation advice specific to certain optional Nosto features.

    Deployment & Testing

    This section provides guides on how to test and deploy your Search Templates and Search Templates Starter implementations.

    Implement on native mobile

    Implementing Nosto on a native mobile application allows a retailer to collect behavioral events similarly as within a traditional web page. This information will then be merged with Nosto data across other sources, impacting product relationships and real-time statistics across the board.

    Read more about the commercial benefits of Mobile Application support here: https://www.nosto.com/products/mobile-app-personalization/

    Due to the nature of the technical environment related to Native Application development, both behavioral and transactional data needs to be sent manually, and consequently all requests for personalization features needs to be tailored within the application as well.

    Read more about Nosto's GraphQL API: https://docs.nosto.com/techdocs/apis/graphql-an-introduction

    Read more about implementing Nosto with GraphQL on IOS and Android: https://docs.nosto.com/techdocs/apis/graphql-an-introduction/graphql-for-ios-and-android

    Architecture and integration overview

    LIM's current architecture combines Nosto Semantic Search with the Core Recommendation Pipeline — Predictive, Semantic, and Visual AI, plus LLMs — to generate responses in real time. The system is horizontally scalable, built to serve high-volume traffic without degradation. LIM also respects all consent and privacy settings configured at the merchant level in Nosto.

    Integration options

    LIM can be integrated in two ways, depending on your system's architecture:

    • REST API — suited to direct backend or service-based integrations

    • MCP (Model Context Protocol) — recommended for agent-native and LLM-driven systems

    Both expose the same core capabilities and return equivalent outputs.

    Implement on a physical store

    For multi-channel retailers tying the brick-and-mortar stores into Nosto's personalization solution allows retailers to collect all data into one. This information will then be merged with Nosto data across other sources, impacting product relationships and real-time statistics across the board. Retailers who have unified their data, can also start matching customers across different channels, and personalize every aspect of their shopping journey.

    Read more about the commercial benefits of Mobile Application support here: https://www.nosto.com/products/in-store-personalization/

    Working with Orders

    Rate Limits

    The Nosto LIM Skill requests are rate-limited per merchant, and 429 responses include Retry-After and X-RateLimit-* headers.

    FAQ

    The nosto parameter allows Nosto to attribute clicks on content and recommendations. In the event, you'd like to omit the parameter, you'll need to manually send the product-view event. You can do so by executing the following snippet.

    nostojs(api => {
       var request = api.createRecommendationRequest()
         .addEvent('vp', "product-id", "placement-id")
         .loadRecommendations();
    });

    How the attribution is tracked will be entirely dependent upon your implementation.

    GraphQL: Updating Order Statuses

    If you would like to update the order-status for a given order, you can do so using the following request.

    mutation {
      updateStatus(number: "ORD102-33", params: {
        orderStatus: "fraud"
        paymentProvider: "klarna"
        statusDate: "2011-12-03T10:17:30"
      }) {
        number
        statuses {
          date
          orderStatus
          paymentProvider
        }
      }
    }

    What Nosto does and how it works

    Nosto replicates an eCommerce site’s product catalog (one account per domain/language) as the foundation for all Nosto modules. You might need to adjust the product data structure, so please let us know about your parent/child relationships, customer groups (pricing and visibility) and how you handle translations and multiple currencies (fixed prices or exchange rates).

    Nosto then does two basic things onsite:

    1. Profile creation: Track what a user is doing on an eCommerce site (which pages (landing page, product page, category page, …) shoppers look at and what they buy).

      • This data is sent to the Nosto backend so we can understand and present insights to the client in our dashboard.

    2. Content personalization: Change the content on an eCommerce site per user depending on the data we have collected and which campaigns have been set up by the client.

      • "Content" is a broad term and ranges:

        • from a hero/banner image on a home, landing, category or product page

    Nosto has several apps/plugins for common platforms like Shopify, Shopware, Magento, ... that give you a head start. Please review the platform-specific documentation at the bottom of this page. The feature set (product and order sync, adding the script and page tagging, ...) can vary and might need to be extended for custom requirements.

    Setting up your account

    You must use a valid domain for your website. If you are creating a test account and running your store locally, you must use valid TLD as using localhost is not supported.

    You cannot use the following domains as they are reserved by the IANA.

    • .test

    We recommend using a valid official TLD that is aliased to localhost for testing purposes. You will need to edit your operating-system dependent hosts file to add an alias for the domain you are using.

    Nosto periodically crawls your website to keep the catalog data in sync and therefore if you run your webshop locally, Nosto will be unable to crawl your website. In order to overcome this, you will either need to:

    1. make it publicly available using a service such as or

    2. Use the to keep your catalog in sync

    Adding the Nosto Script

    To start tracking visits and content the Nosto script needs to be active on all pages within the store where the user might navigate. Replace $accountID from the code below with your own account ID and place the code within the <head> section of your sites HTML content. You can find your stores account IDs from the account list within the Nosto admin.

    <script type="text/javascript">
        (function(){var name="nostojs";window[name]=window[name]||function(cb){(window[name].q=window[name].q||[]).push(cb);};})();
    </script>
    <script src="https://connect.nosto.com/include/$accountID" async></script>

    Note: The script and the snippet should be added as high up in the <head> portion of the page so the connection is initialized as soon as possible. As the script is flagged async, the page load isn’t delayed.

    Note: This needs to exist on every page.

    Alternatively to the script injection Nosto can also be used as a library dependency in your Javascript application via the following utility library

    Once included on all pages, you can review if the site is transmitting data using the Nosto Debug Toolbar. If the debug toolbar executes and shows up on the page Nosto can track visits on the page. You can further verify your session in the Nosto admin by using the live feed under https://my.nosto.com/admin/$accountID/liveFeed

    Adding the Search Tagging

    Nosto utilizes tracks what a customer is searching for by reading the search query from the URL's query parameter or from the page source.

    • When the search term exists as a part of the URL's query parameters e.g. https://www.example.com?q=searchterm, Nosto can be configured to read the search term and you can skip the search term tagging.

    • When the search term exists as a part of the URL e.g. https://www.example.com/searchterm, Nosto is unable to read it from the URL and you will need to tag as a part of the page source. When you implement the tagging for the search term, remember that it is untrusted user input added as part of your page html and it should be html-encoded to prevent XSS vulnerabilities on the site.\

    or via DOM tagging

    Tagging your page types

    The page-type tagging enables Nosto to trigger actions, such as showing popups, depending upon a page type. Tagging the page types is optional but without the page-type tagging, you will not be able to avail the use of page type based triggers.

    Here is a list of all the valid page types:

    • The home page of your store should be tagged as front.

    • All category pages should be tagged as category.

    • All product pages should be tagged as product.

    • The shopping cart page should be tagged as cart .

    • The checkout page, where order information is filled, should be tagged as checkout.

    • The order confirmation page should be tagged as order.

    • The search results page should be tagged as search.

    • All no-found pages should be tagged as notfound.

    • Other pages should be tagged as other.

    or via DOM tagging

    Advanced Usage

    Nosto also supports some advanced use-cases depending on how your store is currently built. You should first read through the following topics to have an understanding of how Nosto works before moving to the advanced use-cases:

    • Manual Implementation - Essentials

      • Adding the Nosto Script

    The topics listed below extend the essential tagging with support for SKUs, Multi-currency and Customer group pricing.

    • Manual Implementation - Advanced

    Product Image Configuration

    Customizing Product Image Aspect Ratio

    You can use CSS custom properties (variables) to define the aspect ratio for specific components. This is useful for components like the autocomplete dropdown where you might want a different aspect ratio than the main product grid.

    The project defines a CSS custom property --ns-aspect-ratio for this purpose.

    Example: Autocomplete Product Image

    The product images within the autocomplete results use this CSS variable.

    File to inspect: src/components/Autocomplete/Item/Product.module.css

    /* src/components/Autocomplete/Item/Product.module.css */
    .image {
      height: auto;
      aspect-ratio: var(--ns-aspect-ratio);
      object-fit: contain;
      width: 100%;
    }

    You can override the value of --ns-aspect-ratio in your theme's CSS file or directly in the component's stylesheet to change the aspect ratio. The value is defined in src/variable.css.

    File to edit: src/variable.css

    By changing --ns-aspect-ratio to 4 / 3, for instance, any component using this variable will render images with a 4:3 aspect ratio.

    Autocomplete features

    Enabling Autocomplete Features

    This document explains how to enable and configure various features for the autocomplete component, including category suggestions and trending searches.

    To enable these features, you only need to modify the withAutocompleteDefaults function in src/config.ts to request the necessary data. The UI components in src/components/Autocomplete/Results/ are already set up to render this data once it's fetched.


    Enabling Category Suggestions

    You can enhance the autocomplete experience by including category suggestions alongside product and keyword results.

    Configuration

    To enable this feature, add a categories object to the query returned by the withAutocompleteDefaults function.

    File to edit: src/config.ts

    By adding the categories object to the query, you are instructing @nosto/search-js to fetch category suggestions. The useResponse hook within the Results component (src/components/Autocomplete/Results/Results.tsx) will then receive this data.

    The Results component, in turn, passes the category data to the Categories component (src/components/Autocomplete/Results/Categories.tsx), which is responsible for rendering the suggestions. No additional rendering configuration is needed.


    Displaying popular searches can help guide users and improve product discovery. This feature shows suggestions based on what other shoppers are searching for.

    To enable this feature, add a popularSearches object to the query returned by the withAutocompleteDefaults function.

    File to edit: src/config.ts

    Similar to category suggestions, adding the popularSearches object to the query will cause the data to be fetched. The useResponse hook in the Results component will receive the data and pass it to the PopularSearches component (src/components/Autocomplete/Results/PopularSearches.tsx), which handles the rendering automatically.

    Using Search Templates

    Search Templates is no longer actively developed. We recommend using instead, which provides modern development tooling, full source code control, and a local development environment. Onboarding assistance is only available for Search Templates Starter.

    Implement on your website

    Most customers implement Nosto by installing a Nosto extension to their e-commerce platform. The extension provides a working out of the box implementation for majority of websites handling installing tagging and product updates. However, for customized PWA/SPA environments, you should follow the additional SPA/PWA guides.

    Adding the Customer information

    On every page, the customer information should be tagged if the customer is logged in. If the customer isn't logged in, this but can be omitted.

    The customer information is primarily used for sending personalized triggered emails and for building multi-channel experiences.

    The full schema for customer tagging is defined

    or via DOM tagging

    The new marketing-permission flag denotes whether the customer has consented to email marketing. If the marketing-permission field is omitted, we assume that the current customer has not given their consent and Nosto will refrain from sending out any personalized triggered emails.

    The marketing permission is false by default but if a user has explicitly agreed to receive marketing then you can set it to true manually. In practice, this means reading and mapping the value from opt-in for marketing in your platform e.g. a consumer explicitly subscribed for marketing emails when checking out.

    Basic Tagging

    In the event that you are unable to expose the entire subset of the product tagging, you can simply tag the product-id.

    The full schema for product tagging is defined

    or via DOM tagging

    When the entirety of the product metadata is tagged, Nosto is able to crawl your site and build a 1:1 replica of your product catalog but in this basic example, you will need to use an alternative mechanism for synchronising your catalog with Nosto.

    Note: If you do use this approach, your account-manager must disable crawling for your account. Failure to do so will result in a broken catalog replica.

    In order to keep your product catalog in Nosto up to date, you must leverage the .

    Adding the Category/Brand Tagging

    Nosto utilizes meta tags to track what category or brand a certain visitor is viewing or what page type the currently viewed page is. These values are then used for dynamic filtering for categories and brands applied through the Nosto admin UI or exposure of certain pop-up campaigns for page types.

    The category tagging should be exposed whenever a user is viewing a certain category.

    or via DOM tagging

    The brand tagging should be exposed whenever a user is viewing a certain brand or vendor.

    or via DOM tagging

    Categories must always be delimited by a slash. For example, /Home/Accessories is a valid category while

    Repository Ownership

    Each Search Templates Starter project lives in its own Git repository. To keep development clean and accountable, that repository should be owned by whoever is actively developing or maintaining the template.

    The principle is simple: The primary developer of the template owns the template and its repository.

    • If external (non-Nosto) developers or their agency are building and maintaining the template, they should own the repository.

    • If Nosto worked on the template previously and development is being handed over to the merchant or their agency, ownership should be

    Adding the Product Tagging

    All product pages should contain the product tagging. The product tagging can be the entire metadata or only a small subset of it.

    The product tagging is used to pass the context of the current product being viewed which in turn is used to personalize the recommendations e.g. cross-sellers, and commonly also periodically crawled by Nosto to build an index.

    Note: The product tagging must be server-side rendered as the Nosto crawler does not execute Javascript.

    You can tag your product pages in two different ways:

    URL Mapping

    When a search is performed (either by submitting the search form or clicking a non-redirect keyword), the user is taken to the search results page. The state of the search (query, filters, pagination, etc.) is reflected in the URL's query parameters.

    The search results page URL is managed by a set of utility functions in src/mapping/url/. The URL is constructed with the following parameters:

    • q: The search query.

    FAQ

    Due to performance optimization, the search function will calculate the total count up to 10,000. In this case the search page should display a count of 10,000+ to indicate that more than 10,000 products were found.

    Filters and sorting operations are executed on all found products, even if there are more than this limit. Therefore, it's still possible to find other products if you filter or sort them. This should not affect the user experience in any way because it's unlikely that someone would actually view more than 10,000 products with a single search.

    By default, up to 250 products can be retrieved at once (in a single request). Paginate to access results past 250 products.

    As soon as you use the results delivered by Nosto API, you will see that the listings in categories have a different order than listings delivered by your native shop-system, even if you didn't set up any merchandising rules. The products are mainly delivered as indexed during the data sync, but there is no defined behavior for this. We recommend to always set up at least one global rule before going live with Nosto category merchandising to create product listings matching your business strategies.

    Testing

    With the , you can see all the changes made to your website right away. To enable this feature, simply turn on the preview mode. After saving any changes in the code editor, you will be able to see them directly on your website.

    How to use preview:

    1. Navigate to your website

    2. In the URL, append ?nostodebug=true to enable the debug toolbar

    Template customization

    This section describes template customization tools and best practices for the following Nosto products:

    • Product Recommendations

    • Onsite Content Personalization

    • Pop-Ups

    Installation

    You can install the Nosto Autocomplete library via npm:

    The Nosto Autocomplete library can be imported and used in various ways, depending on your preferred framework or template language. Some of the supported import methods include:

    Framework
    Import Statement

    Deploying

    When you finished working on search implementation & carefully tested everything using , it's time to deploy everything.

    The Nosto CLI only handles preview deployments. To promote your changes to production:

    1. Test thoroughly in preview mode

    2. Navigate to Nosto Admin UI > Search > Templates

    Starting points

    Each new Nosto account comes with three base recommendation templates to customize.

    The Default template has the following features:

    • Recommended products in a grid

    • Alternate image on hover

    Implement LIM Skill (Early Access)

    Nosto LIM Skill enables third-party agentic systems to deliver real-time, high-quality product discovery and recommendations grounded in live merchant data, behavioral signals, and merchandising rules — without requiring the agent to reason over raw catalogs or ranking logic itself.

    LIM acts as a recommendation and intent-resolution layer that third-party agent platforms call on demand via API or MCP. It returns structured, merchant-safe product recommendations suitable for conversational, guided, or autonomous shopping experiences.

    Modern LLM-based agentic experiences are strong at understanding intent, but struggle to:

    • Rank products accurately in real time

    GraphQL

    GraphQL is a query language for APIs. What does this mean for you? Unlike regular SOAP or REST APIs, GraphQL gives you the ultimate flexibility in being able to specify in your API requests specifically what data you need and get back exactly that.

    As a query language, it provides you with a lot of flexibility that most normal APIs will not. Without needing to recreate endpoints, you can provide developers with the same functionality as a bulk endpoint. Your queries will be cleaner and easier to understand by combining multiple queries into one request.

    The regular API is very well structured and specifically defined. The endpoints have their set requests and responses and that’s what you get whether or not that matches your usage pattern. GraphQL lets you control all of this so that the way you consume the data matches exactly what you need.

    This is both a pro and a con. If your use case does not require all of the data, GraphQL can speed up your requests as we do less work on the server-side to fulfill those requests. Conversely, if you need all of the data in one request, your requests could slow down as we do more work to fulfill these requests.

    Product cards

    Nosto recommends using shop-provided resources for rendering product cards in Nosto templates. Doing so offers:

    • Faster onboarding

    • Easier maintenance

    • Consistent styling and behavior

    Updating Identities

    Mutations can be used to update the email identities in Nosto. An "identity" is the personal information associated with an email address.

    The upsertIdentity mutation allows you to upsert the details of an identity. The given example updates the customer attributes for the email john.doe@nosto.com and requests the details of all the attributes of the identity.

    If the identity for john.doe@nosto.com does not exist, a new identity will be created.

    Note: If a specified attribute already exists on that identity, it will be overwritten.

    The attributes associated with an identity can be used to segment users. This works similarly to how the attributes can be leveraged .

    Testing and Debugging

    The GraphQL endpoints provide functionality to make it easier to test against a real account.

    Every operation made against the GraphQL endpoints cause your website's data to be mutated. When doing performance testing or an equivalent, it is often necessary to exclude test traffic so as to not pollute your live account.

    Using the header X-Nosto-Ignore: True will cause any traffic from being recorded. Queries and Mutations will work normally but any API calls containing this header will not be archived or accrue towards the statistics.

    An example of how this header can be leveraged can be found on our app.

    Every operation made against our GraphQL endpoint returns a unique request identifier contained in an X-Request-Id

    The Playground

    Start exploring Nosto's GraphQL API on your account. GraphiQL is an in-browser IDE for exploring GraphQL. You don't need any access for using the GraphiQL Explorer.

    Toy around, use the API and when you'd like API access to the endpoint for use through a library such as Apollo, please contact support.

    Use the embedded GraphiQL explorer below to run queries. The GraphiQL Explorer is accessible at https://my.nosto.com/\[account-id]/graphql (please add your account ID).

    Documentation is a first-class citizen of GraphQL, and GraphiQL leverages it. The right-hand pane exists for you to explore the possible queries, mutations, fields, their types (if they’re required), the works. Even if your server doesn’t implement human-composed descriptions, you will always be able to explore the graph of possibilities.

    Tagging all the metadata (Recommended): This approach is the recommended way to tag your product pages. It contains the entirety of the product metadata and leverages the crawler to build a 1:1 replica of your catalog.
  • Tagging the bare minimum: This approach entails tagging just the product-id and requires you to leverage an API to build a 1:1 replica of your catalog. This is an advanced use-case and requires that your account-manager disables crawling for your website.

  • Additionally, please refer to our help pages for further useful information!

    Search returns up to 10000 documents

    Products in category listings are in a different order as soon as I deliver results from Nosto API

    .example
  • .localhost

  • .invalid

  • .local

  • .localdomain

  • .domain

  • .lan

  • .home

  • .corp

  • .wip

  • Pagekite
    Ngrok
    Products API

    Adding support for customer group pricing

    Adding the Product Tagging
    Adding the Category/Brand Tagging
    Adding the Cart Tagging
    Adding the Order Tagging
    Adding the Customer information
    Extending tagging with SKUs
    Adding support for multi-currency
    Search Templates Starter
    response header.

    While we ensure that the APIs are as robust as possible if you do encounter an HTTP 5XX response from the endpoint, simply log the request identifier along with the error as the unique request identifier allows our engineers to troubleshoot the issue swiftly.

    Ignoring Test Requests

    Debugging Requests

    Nosto's GraphQL Android Example

    to product recommendations ("You might also like") on any page type (or even in the mini-cart or search overlay)

  • to conversion rate optimized, personalized category pages and SERPs which replace the native platform functionality.

  • Watch video: Overview of Nosto by example of a custom implementation
    Watch video: How Nosto injects personalized content
    Big Picture of a Nosto implementation
    p: The current page number (omitted for the first page).
  • size: The number of results per page.

  • filter.*: Applied filters (e.g., filter.brand=Nike).

  • sort: The selected sorting option.

    1. State Management: The SearchQueryHandler component (src/components/SearchQueryHandler/SearchQueryHandler.tsx) is responsible for synchronizing the application's search state with the URL.

    2. URL Updates: It uses the updateUrl function (src/mapping/url/updateUrl.ts) to serialize the current search state into URL parameters and update the browser's history using window.history.replaceState.

    3. Initial State: On page load, the getCurrentUrlState function (src/mapping/url/getCurrentUrlState.ts) deserializes the parameters from the URL to initialize the search state. This ensures that a user can share a URL, and it will load the same search results.

    A search for "shoes" on the second page with a filter for the brand "Nike" would result in a URL like this:

    /search?q=shoes&p=2&filter.brand=Nike

    Search Results Page URL Management

    URL Structure

    How It Works

    Example

    The Nosto debug toolbar should open up, where you will be asked to log in

  • Once you have logged in, enable the Preview toggle button at the bottom

  • You should now be able to view your changes live, via the Search box

  • Before each deployment search should be manually tested to ensure that everything works correctly.

    What to test?

    • Autocomplete returns results

    • Search displays results, facets with counts

    • You can select multiple facets (on the same field and different fields)

    • Sorting is working

    • Pagination is working

    Test both mobile & desktop view using Chrome device simulation.

    How to use debug toolbar to preview search

    Nosto debug toolbar

    Manual testing

    These three products share the same templating technology and use Velocity based templates which are injected into the page via Nosto's client script.

    In addition to Velocity-based server-side templating, we also have templating usage in other products:

    • Search Templates

    • Nosto Autocomplete

    For template customization we recommend the usage of both our own web component offering and third party tooling such as:

    • Swiper A modern touch slider used for creating responsive, mobile-friendly carousels. It offers smooth transitions, extensive configuration options, and high performance. Find more details at Swiper Homepage.

    • unpic A lightweight, on-demand image optimization library that delivers optimized images with lazy loading. It helps improve performance and user experience by automatically adjusting image sizes. Visit the unpic Homepage for additional information.

    • shoelace A collection of professionally designed, accessible, and customizable web components. It makes it easy to build modern web interfaces with consistent styling. Learn more on the shoelace Homepage.

    Web Components

    Click "Deploy latest and launch live"

    Important: Always test your changes thoroughly in preview mode before promoting to production, as this affects all your store visitors. It can take up to 15 minutes for deployment to be visible.

    • Preview First: All CLI uploads go to preview mode initially

    • Admin Control: Production deployments require manual approval in the Admin UI

    • Rollback Available: Previous versions can be restored from the Admin UI if needed

    If the most recent update doesn't work properly, you have the option to revert to any previous update. Reverting won't alter the source, which means you can deploy the latest changes again simply by clicking on the main deployment button.

    1. Navigate to the Nosto Admin UI > Search > Templates

    2. Click on the desired deployment, click on ... and then on Redeploy

    If your latest update doesn't work and you don't have a previous working version to go back to, or if you want to completely remove the search function, you can turn off the search templates for a temporary period.

    Reverting won't alter the source, which means you can deploy the latest changes again simply by clicking on the main deployment button.

    How to disable templates?

    1. Navigate to the Nosto Admin UI > Search > Templates

    2. Click on the Disable Templates button.

    Production Deployment

    Nosto debug toolbar

    Deployment Safety

    Rollback previous deployment

    How to revert deployment

    Disable Templates

    Ribbons for new, most viewed, and top-selling products

  • Highlighting of discounts

  • Add to cart functionality

  • The Carousel template extends the base template with a Swiper-based carousel to cycle between the recommended products. The library dependency is loaded via a script module, but a locally available version of the library can be used as well.

    • Carousel implementation via Swiper

    • Swiper is loaded via CDN URL into script module scope

    • Swiper default styles are injected into the DOM

    • Navigation module is loaded, and navigation buttons are provided in the DOM

    • Basic mobile breakpoints are provided

    The Swatches template extends the base template with SKU selection-aware product cards with swatches for color and size selection.

    • Color and size swatch rendering

    • Product and SkuOptions web components to maintain swatch selection state and abstract the add-to-cart logic away

    • Web components library is loaded via CDN URL into script module scope

    • Product image is updated based on color swatch selection

    • Add to cart button becomes visible when color and size values have been chosen

    Default

    Carousel

    Swatches

    Understand catalog context, inventory status, and SKU performance
  • Respect merchant-defined merchandising strategies

  • Adapt to live behavioral signals (session-level and user-level)

  • Scale reliably across large catalogs and traffic spikes

  • LIM closes this gap by combining:

    • Semantic understanding of user intent

    • Real-time behavioral learning

    • Merchant-controlled merchandising logic

    • Production-grade recommendation infrastructure

    This lets partners focus on conversation and orchestration, while LIM handles what to recommend and in what order.

    LIM is not a conversational agent, and it does not replace a third-party LLM or orchestration layer.

    Instead, LIM is a specialized product discovery and recommendation system, invoked by an agent whenever product discovery, comparison, or suggestion is required.

    Typical flow:

    1. User expresses shopping intent in conversation

    2. Partner agent interprets intent and context

    3. Agent calls Nosto LIM with structured input

    4. LIM returns ranked product recommendations or agentic suggestions

    5. Agent presents, explains, or acts on the results

    LIM always returns structured output and never takes ownership of the conversation.

    Introduction

    What problem LIM solves

    Where LIM fits in an agentic architecture

    If you would like to access our intelligence engine, you must use GraphQL. The legacy REST APIs are primarily used for sending orders, products and exchange rates.

    Geo Segments:

    Geo segments rely on the ability to determine the geographical location of the end customer. This typically requires a client-side script that can access the user's IP address. In case of a GraphQL only implementation, this should still be possible if the GraphQL calls are made directly from the end user to Nosto. If the calls are being proxied to a backend server and subsequently forwarded to Nosto, this will essentially hide the end-user's IP from the request and instead, send the backend server IP to Nosto, making it impossible to determine the geolocation of the end user.

    What’s the difference between GraphQL API and the regular API?

    Should I use GraphQL over the regular API?

    Limitations

    Below are the recommended approaches.

    If your shop themes use web components, we suggest leveraging them in your Nosto templates as well. This avoids duplicating markup and logic between your shop and Nosto templates. For building web components efficiently, consider using Lit or similar high-level frameworks.

    Nosto offers several web components designed to simplify product card integration:

    • DynamicCard Renders product cards entirely on the Shopify side. &#xNAN;Requires alternate product card templates to be available within Shopify themes. Choose this approach if the shop already uses product card markup in Liquid templates and you want to reuse that markup in Nosto campaign rendering. Detailed instructions on how to set this up in your Shopify store are provided here

    • Product Enhances static product card markup with interactive features such as:

      • Swatch selection

      • Add-to-cart interactions

      • Dynamic product image updates based on swatch and SKU selections

    Nosto's web component offering is documented here

    If web components aren’t an option, we advise duplicating only the markup for product cards within Nosto templates while applying shop-side CSS rules to maintain consistent styling.

    Custom Web Components

    Nosto Web Components

    Style Reuse

    nostojs(api => {
      api.setTaggingProvider("pageType", "search")
      api.setTaggingProvider("searchTerms", ["green shoes"])    
    })
    <div class="nosto_page_type" style="display:none" translate="no">search</div>
    <div class="nosto_search_term" style="display:none" translate="no">green shoes</div>

    To learn more about the api.setTaggingProvider usage, please refer to the official API documentation.

    To learn more about the api.setTaggingProvider usage, please refer to the official API documentation.

    /* src/variable.css */
    :root {
      /* ... other variables ... */
    
      --ns-aspect-ratio: 1; /* Default to square */
    }

    How It Works

    Enabling Popular Searches

    Configuration

    How It Works

    The marketing-permission should be included as a part of the customer tagging and should be rendered on all pages.

    The customer-reference can be leveraged to unify sessions across channels such as between online and offline. It is a unique identifier provided by you that is used in conjunction with the Nosto cookie. The customer-reference can also be used to uniquely identify users in lieu of an email address.

    The customer-reference should be a long, secure and a non-guessable identifier. For example, use your internal customer-id or the customer's loyalty program identifier and use a secure hash function like an HMAC-SHA256 to hash it.

    To learn more about the api.setTaggingProvider usage, please refer to the official API documentation.

    Tagging marketing permission

    here

    Tagging customer reference

    Mustache

    import { autocomplete, fromMustacheTemplate, defaultMustacheTemplate } from "@nosto/autocomplete/mustache"

    Liquid

    import { autocomplete, fromLiquidTemplate, defaultLiquidTemplate } from "@nosto/autocomplete/liquid"

    Preact

    import { autocomplete, Autocomplete } from "@nosto/autocomplete/preact"

    React

    import { autocomplete, Autocomplete } from "@nosto/autocomplete/react"

    Choose the import method that aligns with your project's requirements and technology stack.

    ❗Do not combine multiple imports as it will fetch multiple bundles.❗

    Base

    import { autocomplete } from "@nosto/autocomplete"

    There is currently no way to delete an identity.

    Upserting Identities

    What can identity attributes be used for?

    Deleting Identities

    when importing them via a CSV

    Using Queries

    The query methods allow you to fetch basic recommender data and does not require any sessions. Query operations are light and do not give you the benefit of personalization but can be used to fulfill simple use cases e.g. an in-store display that always shows the top 10 viewed products.

    _The given example simply fetches products related to a given search term aliased as q_related and also fetches the most viewed products within the last week.

    curl -0 -v -X POST https://api.nosto.com/v1/graphql \
    -u ":<token>" \
    -H 'Content-Type: application/graphql' \
    -d @- << EOF
    query {
      recos (preview: false, image: VERSION_7_200_200) {
        hot_now: toplist(hours: 168, sort: VIEWS, params: {
          minProducts: 1
          maxProducts: 10
        }) {
          primary {
            name
            productId
          }
        }
        q_related: search(term: "black", params: {
          minProducts: 1
          maxProducts: 10
        }) {
          primary {
            name
            productId
          }
        }
      }
    }
    EOF
    transferred
    to the receiving party.

    Keeping ownership aligned with active development means the people responsible for the code have full control over it.

    Note: To receive Nosto support when something goes wrong and you can't solve it, you should provide us access to the codebase one way or another.

    When ownership changes hands, the entire repository state needs to be transferred, including the commit history. There are multiple ways to do it - for example, GitHub ownership transfer tool, or a Git bundle - a single file that packages the whole repository and can be transferred over easily.

    GitHub offers a repository transfer tool that can be used to transfer ownership of a repository. If you have full access to the repo, this is arguably the simplest way to transfer ownership.

    If the ownership tool is not applicable for one reason or another, you can go the manual route.

    Even if you already have the repository locally, re-clone it elsewhere as a mirror. This is different from a regular clone, as it captures the entire repository state.

    This produces what is known as a bare clone in a folder named repository-name.git. It is essentially the raw Git structure rather than the code as you usually see it.

    This creates a file named repository-name.bundle inside the directory. That single file is what you transfer over.

    Note: The bundle can be large for older codebases, but for Starter-based repositories it should typically be in the tens of megabytes.

    Before sending, verify the bundle was packed correctly by trying to clone from it (using the same command the receiving side will run - see Importing the repo below). Once you've confirmed it clones cleanly, send the bundle file to the new owner through a secure channel.

    Create a new repository on GitHub, your preferred Git cloud provider or other hosting option in the usual way.

    After receiving the bundle, clone the repo from the file as you would from a regular remote repository.

    Update the cloned repository's remote to point at your new GitHub repository, then push.

    Now the repository has a new owner!

    Who should own the repository?

    Transferring repository ownership

    GitHub ownership transfer tool

    Exporting the repo manually

    Step 1 - Clone the repository

    Step 2 - Bundle it into a single file

    Step 3 - Sanity check and send it

    Importing the repo

    Step 1 - Create your own Git repository

    Step 2 - Clone the repository from the bundle

    Step 3 - Point the remote at your repository and push

    nostojs(api => {
      api.setTaggingProvider("pageType", "product")
    })
     <div class="nosto_page_type" style="display:none" translate="no">product</div>
    // src/config.ts
    
    function withAutocompleteDefaults(query: SearchQuery) {
      return {
        ...query,
        // ... other properties
        categories: {
          fields: ["name", "url"],
          size: 3 // The maximum number of category suggestions to fetch
        }
      } satisfies SearchQuery
    }
    // src/config.ts
    
    function withAutocompleteDefaults(query: SearchQuery) {
      return {
        ...query,
        // ... other properties
        popularSearches: {
          fields: ["query"],
          size: 3 // The maximum number of trending searches to fetch
        }
      } satisfies SearchQuery
    }
    nostojs(api => {
      api.setTaggingProvider("customer", {
        email: "john.doe@example.com",
        first_name: "John",
        last_name: "Doe",
        customer_reference: "e18daf14-d715-4d77-82f2-93eceb4ae1ef",
        type: "loggedin",
        newsletter: false
      })
    })
    <div class="nosto_customer" style="display:none" translate="no">
      <span class="email">john.doe@example.com</span>
      <span class="first_name">John</span>
      <span class="last_name">Doe</span>
      <span class="customer_reference">e18daf14-d715-4d77-82f2-93eceb4ae1ef</span>
      <span class="marketing_permission">false</span>
    </div>
    npm install @nosto/autocomplete
    curl -0 -v -X POST https://api.nosto.com/v1/graphql \
    -u ":<token>" \
    -H 'Content-Type: application/graphql' \
    -d @- << EOF
    mutation {
      upsertIdentity(identity: {
        email: "john.doe@nosto.com",
        attributes: [
          {name: "loyalty-tier", value: "Gold"},
          {name: "shoe-size", value: "42"},
        ]
      }) {
        email,
        attributes {
          name,
          value
        }
      }
    }
    EOF
    git clone --mirror {repository-link}
    git -C repository-name.git bundle create repository-name.bundle --all
    git clone repository-name.bundle {folder-to-clone-into}
    git remote set-url origin {your-repo-link}
    git push --all
    git push --tags

    Implement on your PWA

    Implement on your Headless

    Implement on a standard e-commerce store

    When implementing in SPA and PWA environments, product updates must be done via REST API. In case you are using some of the platforms that Nosto has extension for the extension takes care of the product updates.

    If you have implemented SPA / PWA on top a platform that Nosto has extension for you will still need to implement the frontend part using Session API. The extension will take care of the product updates, order confirmations, exchange rates and other background processes but displaying the recommendations, popups, etc. must be done using Session API.

    In case your website implement some dynamic functionality, you can use the JS API. Note that you cannot mix Session API and Page Tagging.

    Implementing by a Nosto Extension

    Magento

    Magento 2

    Shopware

    Shopify

    Prestashop

    BigCommerce

    Salesforce

    Choosing the implementation method

    Implement on your SPA

    SPA / PWA on top of a platform that Nosto has extension for

    When dynamic functionality is needed / no page reload

    Nosto does not support a product feed and you must leverage the API in order to synchronise your product catalog.

    Once included on all pages, you can review if the site is transmitting data using the Nosto Debug Toolbar. If you can see product attributes being picked up under "Tagging" then the product details are correctly set up. You can further verify that products are being indexed to the catalog under the Nosto admin by navigating to Tools → Products: https://my.nosto.com/admin/$accountID/campaigns/products/list

    Nosto debug toolbar / products
    Nosto product catalog
    live-feed-product-view

    The translate attribute is a HTML5 standard attribute which specifies whether the value of the element and it's Text node children should be translated. If your tagging elements are being translated by e.g. Google Translator then this is the way to opt out elements being translated by Google and possibly other vendors.

    nostojs(api => {
      api.setTaggingProvider("pageType", "product")
      api.setTaggingProvider("products", [{ product_id: "Canoe123" }])
    })
    <div class="nosto_page_type" style="display:none" translate="no">product</div>
    <div class="nosto_product nosto_basic" style="display:none" translate="no"> 
      <span class="product_id">Canoe123</span>
    </div>

    To learn more about the api.setTaggingProvider usage, please refer to the official API documentation.

    Via an API

    Via a Feed

    here
    Products API

    Troubleshooting

    Translate attribute

    Home > Accessories
    is not.

    With Nosto you can also expose other attributes that should be used for category/brand page filtering. For example when a user clicks on a certain color, only products with that certain color attribute should be exposed by both the category list, and Nosto Onsite Recommendations. Available values correspond to custom fields tagged as part of the Product Tagging.

    or via DOM tagging

    Page type tagging should be exposed whenever a user is interacting with a page so Nosto understands what kind of page this is.

    or via DOM tagging

    Page type is optional and used mainly for triggering popups and also to understand what kind of page the user is currently interacting with. The page type must always be lowercase and the accepted values for page type are: front, category, produc, cart, order, search and notfound.

    Once included on all pages, you can review if the site is transmitting data using the Nosto Debug Toolbar. If you can see order contents being picked up under "Tagging" → "Category" then the category and page type tagging are correctly set up in the source code.

    Nosto debug category

    The translate attribute is a HTML5 standard attribute which specifies whether the value of the element and it's Text node children should be translated. If your tagging elements are being translated by e.g. Google Translator then this is the way to opt out elements being translated by Google and possibly other vendors.

    nostojs(api => {
      api.setTaggingProvider("pageType", "category")
      api.setTaggingProvider("categories", ["/Mens/Jackets/Ski Jackets"]) 
    })
    <div class="nosto_page_type" style="display:none" translate="no">category</div>
    <div class="nosto_category" style="display:none" translate="no">/Mens/Jackets/Ski Jackets</div>
    nostojs(api => {
      api.setTaggingProvider("pageType", "category")
      api.setTaggingProvider("brands", ["Acme"]) 
    })
    <div class="nosto_page_type" style="display:none" translate="no">category</div>
    <div class="nosto_brand" style="display:none" translate="no">Acme</div>

    To learn more about the api.setTaggingProvider usage, please refer to the official API documentation.

    Tagging the categories

    nostojs(api => {
      api.setTaggingProvider("tags", ["color: Red", "gender: Men"])
    })
    <span class="nosto_tag" style="display:none" translate="no">color: Red</span>
    <span class="nosto_tag" style="display:none" translate="no">gender: Men</span>
    nostojs(api => {
      api.setTaggingProvider("pageType", "category")    
    })
    <div class="nosto_page_type" style="display:none" translate="no">category</div>

    Faceting by other attributes

    Tagging the current page type

    Troubleshooting

    Translate attribute

    You don’t even have to read the documentation to discover our API. GraphiQL supports debugging as-you-type, giving hints and pointing out errors.

    GraphiQL comes with a JSON viewer with all the niceties you’d expect: code folding, automatic indentation, copy support, and read-only so you won’t accidentally delete or edit.

    Documentation

    Debugging

    screen shot 2018-08-26 at 12 06 07

    JSON Viewer

    Troubleshooting Nosto script:

    Nosto Js
    Nosto debug toolbar

    Defining Nosto placements

    You can define placements for Nosto to use in two ways:

    1. Traditional approach: Using native HTML elements with nosto_element class

    2. Web component approach: Using the <nosto-campaign> web component

    Both approaches mark locations in your site where Nosto can hook into and expose onsite content.

    You can define placements via native HTML elements with the nosto_element class. Each element marks a location in your site where Nosto can hook into and expose onsite content.

    Here is an example of a <div> tag on the site:

    The class needs to always reference nosto_element so Nosto understands that this element is available for onsite content placement. However the id frontpage-nosto-1 is flexible but requires that each unique element-id has a matching placement defined in Nosto's admin dashboard in order to expose campaigns.

    Here is an example of a page with multiple <div> elements:

    As an alternative to nosto_element divs, you can use the <nosto-campaign> web component. This approach provides cleaner markup integration.

    or alternatively

    for better compatibility with the scoped styling conventions of Velocity templates

    The web component supports additional features like lazy loading, product-specific recommendations, cart synchronization, and embedded Vue templates:

    Use nosto_element divs when:

    • Working with existing legacy implementations

    • You prefer traditional HTML markup patterns

    Use <nosto-campaign> web component when:

    • You need advanced features like lazy loading or cart synchronization

    • You want better integration with modern web development practices

    • You need embedded Vue templates for store-side templating

    To use the <nosto-campaign> web component, you need to include the Nosto Web Components library. For detailed setup instructions, see the .

    Search page redirects

    Autocomplete Search Submission and Redirects

    This document explains how submitting a search from the autocomplete component navigates the user to the Search Engine Results Page (SERP).


    Overview

    When a user selects a suggestion or submits a query from the autocomplete dropdown, the application's behavior depends on the user's current location. The goal is to ensure the user always ends up on the main SERP to see the full results.

    This logic is primarily handled within the onSubmit function in the Search component.

    File to inspect: src/components/Search/Search.tsx


    How It Works

    The onSubmit function checks the current page's URL (window.location.pathname) to decide whether to perform an in-place search update or a full-page redirect.

    If the user is already on a page that includes /search in its path, submitting a new search will not cause a full-page redirect.

    • The newSearch({ query }) action is dispatched.

    • @nosto/search-js fetches the new results.

    • The components on the SERP update in place to display the new results.

    This provides a fast and smooth experience when refining a search.

    If the user performs a search from any page that is not the SERP (e.g., the homepage, a content page), the application will perform a full-page redirect to the SERP.

    • The browser is redirected to /search?q=<your-query>.

    • The SERP then loads, reads the q parameter from the URL, and automatically fetches and displays the results for that query.

    This ensures a consistent experience, where a search always leads the user to the dedicated, fully-featured search results page.

    Here is a simplified look at the logic inside src/components/Search/Search.tsx:

    Hybrid Vector Search

    Merchants with Hybrid Vector Search enabled gain the ability to search products based on conceptual similarity in addition to precise retrieval based on textual relevance.

    Depending on merchant preferences, Hybrid Vector Search can take over from keyword search when:

    • The search query has a poor click-through rate.

    • The search query has a poor conversion rate.

    • The search query has no results.

    • Hybrid Vector Search was requested specifically for a particular search query.

    In principle, Hybrid Vector Search works out of the box and doesn't require changes to the integration. The suggestions below are optional.

    To understand results, it's valuable to know which type of logic generated them. The search API exposes this information in the of the search response.

    searchType contains the type of search logic being used:

    • keyword indicates normal keyword search.

    • vector indicates Hybrid Vector Search.

    This information can be optionally used in the search result page to convey whether these results are precise (keyword search) or conceptually related (Hybrid Vector Search). Hybrid Vector Search results can be more general than keyword search results — communicating with the user helps with setting expectations.

    In order to benefit from analytics for Hybrid Vector Search, the search type must be included in impression- and click tracking events sent to Nosto.

    The searchType value queried from the search API can be included in tracking events verbatim within the tracking metadata property searchType, on the same level as query. Here is an example for a well-formed tracking metadata that includes the search type:

    When using NostoJS' automatic tracking tracking (using the track parameter), searchType tracking is included automatically, and no adjustments to the integration are required.

    API integrations and integrations managing tracking metadata manually need to take care of passing through searchType explicitly.

    When Hybrid Vector Search engages, features of Nosto search work as normal with one major caveat: Only the 1000 most relevant results are accessible via pagination and covered by facets and sorting. Depending on the strictness of the relevance threshold defined in the Hybrid Vector Search settings, fewer results could be available.

    This cutoff is based purely on Hybrid Vector Search relevance and does not take pinning or promote/demote rules into account.

    Submit search

    When submitting Search results through Autocomplete, submit callback is called on these events:

    • Enter key press.

    • Submit button click.

    • Keyword click.

    By default submit checks if query/keyword length satisfies minQueryLength, sends Search Submit event to Nosto Analytics, and sends Search request to the Nosto Search API.

    In the usual scenario, you want to render Search Results on submit, so you should override submit function:

    To disable submit pass undefined value. \

    Setting nostoAnalytics: true will enable Nosto Analytics tracking. Tracking results can be seen in the Nosto Dashboard under Search & Categories -> Analytics page.

    By default, we send pageview events to existing GA tag, found in shop site. To send pageview events with correct search information, a minimal configuration is needed in googleAnalytics property.

    • serpPath - Search query url parameter name

    • queryParamName - Search query url parameter name

    For example, if search results URL is https://examplenostoshop.com/search-results?query=shoes, then configuration should be:

    To disable Google Analytics, set googleAnalytics: false.

    Styling

    The content of this page only applies to templates used within:

    • Product Recommendations

    • Onsite Content Personalization

    Styling the recommendations is generally quite straightforward. Just add a style block to the template and use CSS to style the recommendation elements as you would style any HTML content.

    Encapsulating styles

    While the basic styling of the recommendations works great, it can be problematic if there are multiple recommendation elements on the same page. If styles from different recommendations share the same class names, the last element on the page will override the styles of the previous recommendations.

    You can use the $divId variable to print out the current placement ID in the template. Using the variable, you can form CSS selectors that start with the ID of the placement, which limits the scope of the CSS to only the current campaign. You can also target styles to the campaign directly by using the ID selector.

    Nested CSS

    We recommend the use of Nested CSS for scoped styling of campaign templates due to its more compact syntax and wide browser support.

    As most of the recommendation template styles should be scoped to a specific slot, it is common to see scoping structures like this:

    #$divId .nosto-block {
      ...
    }
    #$divId .nosto-header {
     ...
    }
    #$divId .nosto-list {
     ...
    }

    which can be expressed like this using nested CSS:

    To make CSS Nesting in placement and popup templates also available for older browsers that don’t support this feature, the client script provides a polyfill for this. To use the nesting polyfill, you will need to provide the attribute nested on a style element.

    Example conversion:

    becomes the following with divId as nosto-product1:

    The transpilation will be applied in debug mode for all browsers and in normal mode for browsers that don’t support CSS Nesting.

    Using Search Templates

    Search Templates is no longer actively developed. We recommend using instead, which provides modern development tooling, full source code control, and a local development environment. Onboarding assistance is only available for Search Templates Starter.

    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 Nosto-CLI. 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”.

    Nosto Admin UI > Search

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

    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.

    Scripting

    The content of this page only applies to templates used within:

    • Product Recommendations

    • Onsite Content Personalization

    Nosto campaign templates support two ways to define JavaScript script elements as part of the templates.

    In the legacy mode, the script contents are evaluated in the scope of the nosto iframe and can refer to the main window via the global _targetWindow variable.

    To support ES module loading in placement and popup script elements, the client script supports the usage of script[type='module'] elements in both of these contexts. This newer module mode is evaluated in the main window but uses module scope for sandboxing. To write variables to the global scope, you will need to do so explicitly by declaring fields in the window object.

    For new accounts, we recommend the use of ES module scripts. For older accounts with existing templates, the legacy script mode works as well, but interaction with the main window is a bit more verbose.

    The differences between the two modes are summarized here:

    Legacy scripts

    • Syntax: <script>...</script>

    • Loaded into the nosto iframe sandbox

    • Access to the site window happens via the _targetWindow variable

    Module scripts

    • Syntax: <script type="module">...</script>

    • Loaded as sandboxed modules into the site window

    • Site window contents are directly available, e.g., jQuery

    becomes

    Additionally, module scripts support:

    Import syntax

    Top-level await

    Lightweight sandboxing

    Updating Categories

    Mutations can be used to update the category listing in Nosto. The upsertCategories mutation allows you to update one or more category at one go. If the category doesn't already exist, a new one is created.

    A category can have the following fields:

    • id The category identifier. If a category with this id doesn't already exist, a new one is created.

    • name The displayed name for the category.

    • urlPath The path that can be used to generate the URL of the category listing.

    • available If the category is visible on the store. This can be set to false to soft delete the category in an upsert.

    Some stores support hierarchical categories i.e. a category may have parent and child categories. The following fields can be used for hierarchical categories:

    • parentId The identifier of the parent category.

    • fullName The name of every category in the hierarchy.

    Custom logic

    If you want to attach stateful logic as event handlers to your template elements, petite-vue is a useful tool. petite-vue is an alternative distribution of Vue optimized for progressive enhancement. It provides the same template syntax and reactivity mental model as standard Vue. However, it is specifically optimized for "sprinkling" a small amount of interactions on an existing HTML page rendered by a server framework.

    Some rules/constraints to consider:

    • Leave the HTML rendering primarily to Velocity

    • Maintain minimal state in the Vue context, enough to satisfy your use case

    • Use a single Vue app context for the whole template

    • Make sure that the recommendation template renders correctly without the Vue layer

    These rules will guide you to use petite-vue within it's intended use cases, for Progressive Enhancement, and not like Vue, a SPA framework.

    Some use cases where petite-vue is useful are listed below

    This example is stateless and show cases how functions can be exposed to template

    Usage from template

    In this case the selection state is kept in petite-vue and hooked into the template

    template usage

    Headless and SPA (Single Page Application) implementation methods

    The following gives a quick overview of page tagging/event tracking (coupled with handling Nosto product recommendations and banners) for headless and SPA (Single Page Application) builds. You can find more details in the , but this page will already give you a general understanding of the concept.

    Search and Category Merchandising is separate from the personalization guide and covered at the end of this page.

    On every page visit, you need to send a request to Nosto using our about the page type the user is browsing and what exactly they're looking at (e.g. type = product, ID = 123).

    You can , the concept is the same every time.

    Nosto then returns a response with two types of content:

    Adding the Cart Tagging

    On every page load, the cart content must be tagged. The cart contents are the 1:1 representation of the user's mini-cart.

    The cart information is used by the Nosto to tailor the recommendations, dispatch abandoned cart emails and fire Facebook pixel events for retargeting purposes.

    The full schema for cart tagging is defined

    or via DOM tagging

    Note: The product ID of the product tagging, cart tagging and order tagging must match. Failure to do so will lead to a mismatch in both attribution and statistics across the Nosto product.

    Cart content changes should be reflected to Nosto by either calling

    Parameterless Attribution

    By default Nosto tracks campaign attribution without additional url parameters. The tracking happens by registering click listeners to the campaign elements that detect product url clicks and associate them with the attribution metadata of the rendered campaign. The pair of url and campaign attribution is stored in the local storage of the Browser.

    In most cases this will work out of the box, but in certain scenarios adjustments need to be made. For a comprehensive overview, please read our .

    The following tracking modes are supported:

    • a[data-nosto-product]

    Using Search Templates Starter

    The Search Templates Starter is a Preact-based starter template that provides a complete development environment for building custom search experiences with Nosto.

    Search Templates Starter is aimed at developers comfortable with:

    • TypeScript — the codebase is TypeScript-first throughout

    • Git — version control is central to the workflow

    Dynamic product cards

    For Shopify stores, dynamic cards offer a powerful way to display products. Instead of building product cards from individual pieces of data (like a product's name and price), you can load the complete, ready-made product card directly from your theme.

    This is the recommended approach when:

    • Your product cards have complex logic, such as variant selectors, color swatches, or add-to-cart forms.

    • You want to maintain a single source of truth for your product card templates within your Shopify theme.

    Manual Implementation - Essentials

    To implement Nosto manually you will need to go through the following steps to ensure that the store data is captured by Nosto. The following steps will allow Nosto to gather product, cart and order data, and analyze how individual customers are interacting with this data. The implementation steps listed here are necessary for both functionalities based on crowd logic and 1-1 behavioral personalization.

    If you are running a headless frontend or SPA (Single Page Application), you will follow the same approach using the Nosto Session API. Please read more on the , and (instead of page tagging).

    Using Mutations

    The mutation methods allow you to change the session on Nosto's end and request personalized recommendations. Each mutation operation allows you to update the cart and customer information, all while giving you access to recommendations for the sessions.

    Any mobile experience built atop Nosto's GraphQL API should use the mutation operation as it feeds data into to the recommender systems while providing personalization data.

    _The given example updates the customer's information and his current shopping cart contents sends an event that the customer is currently viewing product number 400 and requests the personalized recommendations associated with a given product for him aliased as front_page_1.

    Querying Products

    Querying products gives access to Nosto's product catalogs current state. It can be useful for backend integration and verification purposes. It is not meant for online use purposes as it doesn't include any concepts related to user sessions or attribution, for those refer to

    Session API
    Session API
    GraphQL
    Manual Implementation

    Project structure

    Saving, testing & deploying

    here
    https://github.com/Nosto/wiki-docs/blob/Techdocs/implementing-nosto/implement-search/implement-search-using-code-editor/broken-reference/README.md
    Search Templates Starter
    Code Editor
    Working with Shopify themes (additional integration patterns available)

    Traditional div-element approach

    Web component approach with <nosto-campaign>

    Basic usage

    Advanced features

    When to choose each approach

    Setup and integration

    Web Components documentation

    Scenario 1: User is Already on the SERP

    Scenario 2: User is on Another Page (e.g., Homepage)

    Implementation Example

    Recognizing the type of search logic being used

    Tracking requirements

    Limitations

    SearchProducts object
    enabled
    - Enable Google Analytics

    📊 Nosto Analytics (enabled by default)

    Note: You should additionally add click events on your search results page according to Nosto Tech Docs with type: serp || category according to the results page type.

    📈 Google Analytics (enabled by default)

    Pop-Ups

    Further reading

    CSS Nesting
    Browser support

    Helper modules in the nosto window namespace are available without a prefix

    Helper modules in the nosto window namespace are available via the nosto. prefix

    Pop-Ups

    Examples

    Further reading

    JavaScript modules

    Event handlers to call external APIs and libraries

    Product selection for bundle creation and related total

    curl -0 -v -X POST https://api.nosto.com/v1/graphql \
    -u ":<token>" \
    -H 'Content-Type: application/graphql' \
    -d @- << EOF
    mutation MySession {
      updateSession(id: "ad8f0d0e-1156-4df2-b385-10e03f8f8a44",
        params: {
          customer: {
            firstName: "John"
            lastName: "Doe"
            marketingPermission: true
            customerReference: "319330"
          }
          event: {
            type: VIEWED_PRODUCT
            target: "400"
          }
          cart: {
            items: [
              {
                productId: "100",
                skuId: "100-1",
                name: "#100",
                unitPrice: 199,
                priceCurrencyCode: "EUR",
                quantity: 1
              },
              {
                productId: "200",
                skuId: "200-1",
                name: "#200",
                unitPrice: 299,
                priceCurrencyCode: "EUR",
                quantity: 2
              },
              {
                productId: "300",
                skuId: "300-1",
                name: "#300",
                unitPrice: 399,
                priceCurrencyCode: "EUR",
                quantity: 3
              },
            ]
          }
        }) {
          id,
          recos (preview: false, image: VERSION_8_400_400) {
            front_page_1: related(productIds: ["525834092559"],
              relationship: VIEWED_TOGETHER
              params: {
                minProducts: 3,
                maxProducts: 5
            }) {
            primary {
              productId
            }
          }
        }
      }
    }
    EOF
    curl -0 -v -X POST https://api.nosto.com/v1/graphql \
    -u ":<token>" \
    -H 'Content-Type: application/graphql' \
    -d @- << EOF
    query {
      products(
        limit: 5
        offset: 0
        sort: {field: PRICE, reverse:true},
        filter: {categories: "shoes"}
      ) {
        products {
          productId
          url
          price
          categories
        }
      }
    }
    EOF

    List Products

    The maximum number of products that can be paged over is capped at 10000. If you need to get around this limitation, we recommend adding more restrictive filters to narrow down the result set.

    Query by Product ID

    GraphQL For Headless

    Updating Products

    Mutations can be used to update the product catalog in Nosto. The updateProducts mutation allows you to update one or more products at a go.

    Any validation errors in the product data are accessible in the response. The entire product object is accessible in the response too. In the event that a product validation error led to the product to not be updated, the response would contain the errors as well as the invalid product data.

    The given example updates the product #101 and requests the details of the updated products and any associated errors.

    curl -0 -v -X POST https://api.nosto.com/v1/graphql \
    -u ":<token>" \
    -H 'Content-Type: application/graphql' \
    -d @- << EOF
    mutation {
      updateProducts(products: [
        {
          productId: "101"
          url: "http://mridang.dev.nos.to:8890/product.htm"
          imageUrl: "https://example.com/product/sku-1.jpg"
          priceCurrencyCode: "EUR"
          price: 10
          skus: [
            {
              id: "sku-1"
              name: "One"
              availability: "InStock"
              price: 100
              listPrice: 111
              imageUrl: "https://example.com/product/sku-1.jpg"
            }
          ]
        }
      ]) {
        result {
          errors {
            field
            message
          }
          data {
            productId
          }
        }
      }
    }
    EOF

    The given example updates the price of #101 and requests the details of the updated products and any associated errors.

    Attributions are tracked based on the id of the clicked product supplied in the data-nosto-product attribute

  • Recommended for most reliable attribution

  • Example: <a href="..." data-nosto-product="1234">...</a>

  • a[href]

    • Attributions are tracked based on the URL of the clicked product supplied in the href attribute

    • Fallback logic that requires that the source and target URL of the product are aligned

    • Example: <a href="https://example.com/products/1234">...</a>

  • :not(a)[data-nosto-product]

    • Attributions are tracked based on the id of the clicked product supplied in the data-nosto-product attribute, triggering an immediate network call to Nosto

    • Useful for any interaction that doesn't trigger a navigation to a PDP page, e.g. ATC dialogs, modals etc.

    • Example: <span atc-dialog data-nosto-product="1234">...</span>

  • In case the product urls used in Nosto campaigns have HTTP level redirects applied the HTML should link back to the canonical url used in Nosto campaign via link[refl="canonical"] elements in the head element. Nosto uses both the current location and the canonical page url as lookup keys for the attribution metadata.

    When combined with Session API based requests and HTML based campaign results it is advisable to let the Nosto API handle the campaign injection by enabling campaign injection on the session level:

    Check out the API documentation for defaultSession

    Parameterless attribution became the default attribution mechanism on May 26th 2025. If your setup relies on the legacy nosto parameters being present you can enable the legacy behavior in your main account settings page.

    Below is an example of a custom element that fetches JSON results based on the placement attribute, renders them and register parameterless attribution for product link clicks:

    In case the campaign markup is rendered into a non-placement element the element will need to be registered with parameterless attribution handling via api.attributeProductClicksInCampaign:

    Check out the API documentation for attributeProductClicksInCampaign

    Capturing clicks

    personalization attribution guide

    Product url redirects

    Session API based usage

    Reliance on the legacy nosto parameters

    JS API based usage: JSON Rendering Attribution

    Attribution in custom element based Nosto campaign rendeirng

    Rendering of campaign markup in non-managed placement elements

    You need to display rich product information that may not be available as individual fields in the search API response.

    The starter template uses a decorator to extract a product's handle from its URL. This handle is then passed as a prop to the DynamicCard Preact component, which in turn sets the handle attribute on the nosto-dynamic-card web component.

    The key props you will work with are:

    • handle: This is derived automatically by the handleDecorator from the product.url. You typically do not need to manage this manually.

    • section: The ID of the section to render from your product template.

    • template: The name of the alternate template to use for rendering.

    Note: To render a dynamic card, you must provide the handle prop along with either the section or template prop.

    The nosto-dynamic-card web component leverages Shopify's built-in support for these features. When you provide a section or template prop, the component constructs the appropriate URL to fetch the pre-rendered HTML from your Shopify store.

    Note: The template prop usage is currently being deprecated and we recommend the section prop usage as it is recommended by Shopify and proven to be more robust.

    Read more about the Dynamic Product Cards web component.

    The template provides DynamicCardProduct components for both the search results page (SERP) and the autocomplete dropdown. You need to ensure the template prop matches a template available in your backend.

    • SERP Location: src/components/Product/DynamicCardProduct.tsx

    • Autocomplete Location: src/components/Autocomplete/DynamicCardProduct/DynamicCardProduct.tsx

    For example, to change the template name for the search results page:

    The search results page (Products.tsx) can be configured to use either the static Product.tsx component (client-side rendering) or the DynamicCardProduct.tsx component (server-side rendering).

    To use dynamic cards, ensure the component is imported from DynamicCardProduct.tsx.

    When using dynamic product cards, the HTML for the cards is fetched directly from the merchant's website. This means that the styling for these cards is not included in the starter template's CSS.

    To visually test the dynamic product cards in your local development environment, you need to include your store's CSS. You can do this by adding a <link> tag to the <head> of the index.html file in the root of the project.

    For example:

    Replace https://your-store.com/store.css with the actual URL to your store's main stylesheet. This will allow you to see the correctly styled product cards when running the development server.

    How It Works

    Configuration

    1. Configure the DynamicCardProduct Component

    2. Enable Dynamic Rendering in the Product Grid

    Visually Testing Dynamic Product Cards

    <div class="nosto_element" id="frontpage-nosto-1" translate="no"></div>
    // Three separate <divs> after another on a page
    
    <div class="nosto_element" id="frontpage-nosto-1" translate="no"></div>
    <div class="nosto_element" id="frontpage-nosto-2" translate="no"></div>
    <div class="nosto_element" id="frontpage-nosto-3" translate="no"></div>
    
    // You can also add the class and id to an element you are already using for other purposes
    
    <div class="sidebar nosto_element" id="nosto-sidebar" translate="no">
    
       <h1>Hello World!</h1>
       <h2>This is the sidebar</h2>
    
       // You can also nest nosto elements within other wrapper elements
    
       <div class="nosto_element" id="nosto-sidebar-nested-1"></div>
    
    </div>
    <nosto-campaign placement="frontpage-nosto-1"></nosto-campaign>
    <nosto-campaign placement="frontpage-nosto-2"></nosto-campaign>
    <nosto-campaign placement="frontpage-nosto-3"></nosto-campaign>
    <nosto-campaign id="frontpage-nosto-1"></nosto-campaign>
    <nosto-campaign id="frontpage-nosto-2"></nosto-campaign>
    <nosto-campaign id="frontpage-nosto-3"></nosto-campaign>
    <!-- Lazy-loaded campaign -->
    <nosto-campaign placement="below-fold-recommendations" lazy></nosto-campaign>
    
    <!-- Product-specific recommendations -->
    <nosto-campaign placement="related-products" product-id="123456"></nosto-campaign>
    
    <!-- Cart-synchronized campaign -->
    <nosto-campaign placement="cart-recommendations" cart-synced></nosto-campaign>
    
    <!-- Campaign with embedded Vue template -->
    <nosto-campaign placement="best-sellers">
      <template>
        <div class="product-card" v-for="product in products">
          <span class="product-name">{{ product.name }}</span>
          <span class="product-price">{{ product.price }}</span>
        </div>  
      </template>
    </nosto-campaign>
    // src/components/Search/Search.tsx
    
    export default function Search() {
      const { newSearch } = useActions();
    
      const onSubmit = (query: string) => {
        // If we are already on the search page, just update the results
        if (window.location.pathname.includes("/search")) {
          newSearch({ query });
        } else {
          // Otherwise, redirect to the main search page
          window.location.href = `/search?q=${encodeURIComponent(query)}`;
        }
      };
    
      // ... component JSX that uses onSubmit
    }
    {
      "hasResults": true,
      "autoComplete": false,
      "autoCorrect": false,
      "keyword": false,
      "organic": true,
      "refined": false,
      "refinedQuery": null,
      "sorted": false,
      "query":  "t-shirt",
      "resultId": "d65b040c-56ae-4c6d-a038-fe908e140855",
      "searchType": "vector" // or "keyword"
    }
    submit: async (query, config, options) => {
        if (
            query.length >= config.minQueryLength
        ) {
            const response = await search(
                {
                    query,
                },
                {
                    redirect: true,
                    track: config.nostoAnalytics ? "serp" : undefined,
                    ...options
                }
            )
            // Do something with response. For example, update Search Engine Results Page products state.
        }
    },
    googleAnalytics: {
        serpPath: "search-results",
        queryParamName: "query",
        enabled: true
    }
    #$divId {
      .nosto-block {
        ...
      }
      .nosto-header {
        ...
      }
      .nosto-list {
        ...
      }
    }
    <style nested>
    #$divId {
      .wrapper {
        .blue {
          color: blue;
        }
        .red {
          color: red;
        }
      }
    }
    </style>
    <style nested data-transpiled="true">
    #nosto-product1 .wrapper .blue { color: blue; }
    #nosto-product1 .wrapper .red { color: red; }
    </style>
    <script>
      _targetWindow.jQuery(
    </script>
    <script type="module">
      jQuery(
    </script>
    <script type="module">
      import { createApp } from 'https://unpkg.com/petite-vue?module'
      createApp().mount()
    </script>
    <script type="module">
      const response = await fetch('http://www.acme.com/myapi/myresource')
      // Do something with the response
    </script>
    <script type="module">
      // Config is not written to the window namespace but scoped to the script tag
      const config = {
        key: "738209438"
      }
    </script>
    curl -0 -v -X POST https://api.nosto.com/v1/graphql \
    -u ":<token>" \
    -H 'Content-Type: application/graphql' \
    -d @- << EOF
    mutation {
      upsertCategories(categories: [{
        id: "123",
        name: "Shoes",
        parentId: "456",
        urlPath: "/categories/mens/shoes",
        fullName: "/Mens/Shoes",
        available: true
      }]) {
        categoryResult {
          errors {
            field
            message
          }
          category {
            id
            name
            parentId
            urlPath
            fullName
            available
          }
        }
      }
    }
    EOF
    <script type="module">
    import { createApp } from "https://unpkg.com/petite-vue?module"
    
    createApp({ 
      addToCart(productId) {
        // call platform specific add to cart API  
      }    
     }).mount("#$divId")
    </script>
    <span @click="addToCart('$product.productId')">Add to cart</span>
    <script type="module">
    import { createApp } from "https://unpkg.com/petite-vue?module"
    
    const prices = {
    #foreach($product in $!products)
      "$product.productId": $product.price.asNumber(),
    #end
    }
    
    createApp({  
      selected: [],
      toggle(id) {
        const idx = this.selected.indexOf(id)
        if (idx > -1) {
          this.selected.splice(idx, 1)
        } else {
          this.selected.push(id)
        }
      },
      get total() {
        return this.selected
          .map(id => prices[id] ?? 0)
          .reduce((acc, curr) => acc + curr, 0)
      }
    }).mount("#$divId")
    </script>
    <div class="product-grid">
    #foreach($product in $!products)
      <div class="product" @click="toggle("$product.productId")">
        ...
      </div>
    #end
    </div>
    
    <div>Total: {{ total }}
    curl -0 -v -X POST https://api.nosto.com/v1/graphql \
    -u ":<token>" \
    -H 'Content-Type: application/graphql' \
    -d @- << EOF
    query {
      product(id: "5358") {
        productId
        name
        url
        price
        listPrice
        imageUrl
        attributes {
          key
          value
        }
        skus {
          name
          price
          listPrice
          availability
          imageUrl
          url
        }
      }
    }
    EOF
    curl -0 -v -X POST https://api.nosto.com/v1/graphql \
    -u ":<token>" \
    -H 'Content-Type: application/graphql' \
    -d @- << EOF
    mutation {
      updateProducts(products: [
        {
          productId: "101"
          url: "http://mridang.dev.nos.to:8890/product.htm"
          price: 10
        }
      ]) {
        result {
          errors {
            field
            message
          }
          data {
            productId
          }
        }
      }
    }
    EOF
    api.defaultSession()
      .setResponseMode("HTML")
      .enableCampaignInjection()
      .viewProduct(...)
      .setPlacements(...)
      .load() 
    export class NostoRenderer extends HTMLElement {
      async connectedCallback() {
        const api = await new Promise(nostojs)
        const placement = this.getAttribute("placement")
        if (placement) {
          const results = await api
            .createRecommendationRequest({ includeTagging: true })
            .setElements([placement])
            .setResponseMode("JSON_ORIGINAL")
            .load()
          if (results.recommendations[placement]) {
            const rec = results.recommendations[placement]
            const container = document.getElementById(placement)
            // TODO: Define your own method to render products
            renderProductsToContainer(container, recommendation)
            api.attributeProductClicksInCampaign(this, rec)
          }
        }
      }
    }
    
    if (!customElements.get("nosto-renderer")) {
      customElements.define("nosto-renderer", NostoRenderer)
    }
    const placementId = "frontpage-nosto-1"
    
    const response = await api
     .createRecommendationRequest({ includeTagging: true })
     .setResponseMode("JSON_ORIGINAL")
     .setElements([placementId])
     .load()
    
    const recommendation = response.recommendations[placementId]
    const container = document.getElementById(placementId)
    if (recommendation && container) {
      // TODO: Define your own method to render products
      renderProductsToContainer(container, recommendation)
      api.attributeProductClicksInCampaign(container, recommendation)
    }
    // src/components/Product/DynamicCardProduct.tsx
    import { DynamicCard, SerpElement } from "@/elements";
    import type { Product } from "@nosto/search-js-sdk";
    
    export default function DynamicCardProduct({ product }: { product: Product }) {
      return (
        <SerpElement product={product}>
          {/* Change this template name to match your backend template */}
          <DynamicCard handle={product.handle!} template="your-product-card-template" />
        </SerpElement>
      );
    }
    // src/components/Products/Products.tsx
    
    // For dynamic, server-rendered cards:
    import Product from "@/components/Product/DynamicCardProduct";
    
    // For static, client-rendered cards, you would use:
    // import Product from "@/components/Product/Product";
    
    // ... rest of the component
    <!-- index.html -->
    <!doctype html>
    <html lang="en">
      <head>
        <!-- Add a link to your store's CSS file -->
        <link rel="stylesheet" href="https://your-store.com/store.css" />
        ...
      </head>
    </html>

    Product Recommendations -> JSONResult with an array of JSONProduct

  • Onsite Content Personalization (OCP, e.g. banners or text) -> HTML

  • You take the response and pass it to your rendering function, building the HTML template and injecting it into your theme.

    This is done via "placements" (empty divs on every page that can be populated from the backend, e.g. pdp-top, pdp-mid, home-1, home-2, ...) and you pass all the placement-IDs that are on the current page to Nosto. Nosto then returns the data of the campaigns that are inside of those placements.

    Using placements gives the eCom-team a high degree of flexibility since they can control what to show where and they can run A/B tests within Nosto.

    Nosto offers you several helper functions to simplify injecting your campaigns and setting up click attribution. If you want to read more on DOM injection and click attribution read this.

    The event tracking can also be done via GraphQL.

    The concept is the same: specify data about the session (cart and customer) and request product recommendations for a given page type.

    Please beware of the following drawbacks:

    1. You request the campaigns for a specific product ID or category (without placements) and will receive the Recommendation campaign IDs directly and therefore can't use Nosto built-in A/B testing. You need an alternative, full page A/B testing like Omniconvert in this case.

    2. Dynamic filtering is not possible via GraphQL. We highly recommend to go with the Session API and use viewCustomField.

    3. Nosto OCP (like personalized banners or other HTML content) can not be retrieved via GraphQL.

    4. Adding explicit affinity signals manually like with the is not supported. as well as are supported.

    The Nosto team is happy to support you finding the method that matches your tech stack, requirements and preferences. We highly recommend reading our personalization implementation guide, but if you're in a hurry, take a look at our comparison table.

    Here you can find an overview of all implementation methods.

    The differences between the GraphQL API and JS Library (wrapping the GraphQL API) are:

    • Queries done with the JS Library are automatically tracked, only clicks need to be tracked (when a user clicks on a product that was returned by a Nosto-powered search overlay, SERP or PLP)

    • Nosto A/B testing is automatically included with the JS Library and needs to be handled explicitly when using GraphQL

    • Personalized and segmented results with GraphQL need an addition of the JS Library to get the current session params from the browser and pass it to the GraphQL request

    The endpoints and requests are very similar, you either pass a search query or a category and Nosto returns all products and associated facets. Here are several examples for pagination, sorting, faceting etc..

    If you are using Shopify Hydrogen or Magento Hyvä, you can use Nosto's dedicated React Component Library for Shopify Hydrogen and the built-in support for Hyvä in the Nosto Magento plugin.

    Page Tagging and Event Tracking + Requesting Nosto Content for rendering with your Templates

    personalization implementation guide
    Session API
    find the different page types here

    Nosto Content via Session API

    Nosto Content via GraphQL

    Choosing the right Implementation Method

    Implementation Methods for Nosto Search/Category Merchandising (CM)

    api.setTaggingProvider("cart", data)
    with updated cart contents or updating the cart tagging in the DOM.

    Many e-commerce stores utilize SKU:s or "child" products that are sorted under the same "parent" product. To extend the above example with SKU support refer to this article

    In cases where a product might have multiple prices in differing currencies, you can also add support for multi-currency. Refer to this article

    If the platform itself has support for persistent shopping cart or other technologies that remember the users cart contents you do not need to worry about filling out the cart when a user returns to the site. If your platform generates a restore cart link you can also send that to Nosto by adding it as a new attribute within the parent container "nosto_cart".

    The following piece of code is just a rough example on how a restore cart could look like. The idea of the example is to document how this is tagged to Nosto.

    or via DOM tagging

    Once included on all pages, you can review if the site is transmitting data using the Nosto Debug Toolbar. If you can see cart contents being picked up under "Tagging" → "Cart" then the cart details are correctly set up in the source code. You can further verify your session in the Nosto admin by using the live feed under https://my.nosto.com/admin/$accountID/liveFeed to see if Nosto correctly picks up product view → product carted events.

    Nosto debug toolbar cart
    live-feed-product-cart

    The translate attribute is a HTML5 standard attribute which specifies whether the value of the element and it's Text node children should be translated. If your tagging elements are being translated by e.g. Google Translator then this is the way to opt out elements being translated by Google and possibly other vendors.

    nostojs(api => {
      api.setTaggingProvider("cart", {
        items: [
          {
            product_id: "Canoe123",
            quantity: 1,
            name: "Acme Canoe",
            unit_price: 999.0,
            price_currency_code: "EUR"
          },
          {
            product_id: "Canoe245",
            quantity: 3,
            name: "Acme Large Canoe",
            unit_price: 19.0,
            price_currency_code: "EUR"
          }
        ]
      })
    })
    <div class="nosto_cart" style="display:none" translate="no">
    
        <div class="line_item">
            <span class="product_id">Canoe123</span>
            <span class="quantity">1</span>
            <span class="name">Acme Canoe</span>
            <span class="unit_price">999.00</span>
            <span class="price_currency_code">EUR</span>
        </div>
    
        <div class="line_item">
            <span class="product_id">Canoe245</span>
            <span class="quantity">3</span>
            <span class="name">Acme Large Canoe</span>
            <span class="unit_price">19.00</span>
            <span class="price_currency_code">EUR</span>
        </div>
    
    </div>

    To learn more about the api.setTaggingProvider usage, please refer to the official API documentation.

    Dynamic cart changes

    here
    nostojs(api => {
      api.setTaggingProvider("restoreLink", "https://example.com/cart/restore?cart=4D5C3060-1334-4C63-B6FA-D9D342D88B08")
    })
    <div class="restore_link">https://example.com/cart/restore?cart=4D5C3060-1334-4C63-B6FA-D9D342D88B08</div>

    Adding support for advanced use cases

    Tagging the cart restore link

    Troubleshooting

    Translate attribute

    NPM-based tooling — dependency management, scripts, local dev servers

  • React or Preact — components follow standard React patterns

  • Agentic coding (using LLM tools such as Copilot, Codex or Claude) is optional but particularly useful here. Unlike the legacy VSCode web-based Search Templates, the Search Templates Starter is built with an assumption that AI tools are a staple of modern development. We recommend relying on LLMs to give you a better starting point before finalizing the feature development manually. For example, if you want to convert the starter to a different styling approach such as Tailwind, or swap out individual components for ones that better fit your stack, an LLM is useful. See LLM Examples for practical guidance.

    Search Templates Starter allows you to build fully custom search implementations with modern development practices. You get:

    • Full source code control with Git version management

    • Modern development tooling including TypeScript, Vitest, and Storybook

    • Local development environment with hot reloading and debugging

    • Component library with pre-built, customizable search components

    • Complete flexibility to modify any aspect of the search experience

    For any serious development work, Search Templates Starter is the recommended choice. It gives you the tools and workflow you would expect from a modern frontend project, and it is the approach Nosto actively develops and supports.

    If you have not worked with Nosto Search before, here is what you need to know about how Search Templates Starter fits into your stack.

    You do not need to host the built bundle yourself. When you build and push your templates using nosto-cli, the artifacts are uploaded to Nosto's infrastructure and served to your store visitors automatically — no CDN setup or server configuration required. Self-hosting is supported if you need it, but for most use cases Nosto's hosting is all you need.

    Your store already includes the Nosto script tag. That script is responsible for loading and injecting the search templates into your page. As long as:

    1. Search Templates are enabled for your account

    2. You have built and pushed your templates with nosto-cli

    3. Your src/config.ts includes the correct CSS selectors to target your page's DOM elements

    …the templates will appear on your site out of the box. There is nothing extra to wire up on the frontend.

    The development workflow separates preview from production deployment. During development, nosto-cli provides a watch mode (nosto st dev) that automatically builds and pushes your artifacts to Nosto as you save files — only the built artifacts are uploaded, not your entire repository. Your changes go to preview mode first and are not visible to real visitors yet. To see them yourself:

    1. Navigate to your store and append ?nostodebug=true to the URL

    2. Log in to the Nosto debug toolbar that appears

    3. Toggle Preview mode on

    4. Your latest changes will now be visible as you browse the store

    When you are happy with the result, you can deploy to production either from the Nosto Admin UI or directly using the CLI. This is the same workflow as the legacy Search Templates — the difference is only in how you develop and push your code locally.

    To start developing with Search Templates Starter, you'll need Node.js 24+ and familiarity with React/Preact. Here is the typical workflow end to end:

    1. Getting started — Clone the repository, install dependencies, and configure your merchant ID and CSS selectors

    2. Authenticate — Run nosto login to connect the CLI to your Nosto account

    3. Develop — Use npm run dev for fast component iteration in isolation, or nosto st dev to push changes and test them on your real store

    4. Preview — Enable the Nosto debug toolbar on your store (?nostodebug=true) and toggle Preview mode to review your changes before they go live

    5. — When ready, deploy from the CLI or from the Nosto Admin UI

    Maintain at least two long-lived branches in your repository — for example main for the current production state and dev for ongoing development. When you need to apply an urgent fix to the live store, you can branch off main, apply and deploy the patch, and merge back — without disrupting work in progress on dev. This is the same branching model you would use in any professional frontend project, and it maps naturally onto Nosto's preview/production deployment model.

    The repository should be owned by whoever is actively developing the template - typically the merchant or their agency. If Nosto built the template initially, ownership should be transferred over when development is handed off. See Repository Ownership for details and how to transfer it.

    Prerequisites

    Why use Search Templates Starter?

    How it works (new to Nosto?)

    Templates are hosted by Nosto

    The bundle is injected via the Nosto script

    Preview changes without going live

    Next steps

    Best practices

    Use separate branches for production and development

    Keep repository ownership with the active developer

    Setting up your account
  • Adding the Nosto Script

  • Adding the Cart Tagging

  • Adding the Customer information

  • Adding the Product Tagging

    • Default Product Tagging

    • Basic Tagging

  • Adding the Category/Brand Tagging

  • Adding the Search Tagging

  • Adding the Order Tagging

  • Defining Nosto placements

  • Tagging your page types

  • Advanced Usage

    • Extending tagging with SKUs

    • Adding support for multi-currency

  • FAQ

  • If the store is built on explicitly supported platforms like Magento, Magento 2, Shopify, Prestashop or Shopware you should go through their platform specific guides instead.

    The tagging context can be provided in two ways

    • programmatically via tagging providers

    • as dedicated Nosto elements in page markup

    We recommend to utilize tagging providers due to the following benefits

    • tagging can be dynamically changed in JS without writing to the DOM

    • tagging context can be provided much earlier, since script tags can also be placed in head element

    It is possible to mix DOM based tagging and tagging providers in case the main tagging is provided as HTML elements, but individual parts should be managed via Javascript code dynamically. Also both DOM and tagging provider based tagging are treated in the same way in the client script and debug toolbar.

    introduction
    managing the session
    event tracking
    Manual Implementation - Essentials

    Adding the Order Tagging

    All thank-you and order-confirmation pages must have the conversion tracking markup.

    The conversion metadata is used for sending personalized order-followup emails, personalize the recommendations e.g. order-related, for segmentation insights and conversion statistics.

    nostojs(api => {
      api.setTaggingProvider("pageType", "order")
      api.setTaggingProvider("order", {
        payment_provider: "checkmo",
        order_status: "pending",
        info: {
          order_number: "1445",
          email: "john.doe@example.com",
          first_name: "John",
          last_name: "Doe",
          type: "order"
        },
        items: [
          {
            product_id: "Canoe123",
            quantity: 1,
            name: "Acme Canoe",
            unit_price: 999.0,
            price_currency_code: "EUR"
          },
          {
            product_id: "Canoe245",
            quantity: 3,
            name: "Acme Large Canoe",
            unit_price: 19.00,
            price_currency_code: "EUR"
          }
        ]
      })
    })

    To learn more about the api.setTaggingProvider usage, please refer to the .

    The full schema for order tagging is defined here

    or via DOM tagging

    <div class="nosto_page_type" style="display:none" translate="no">order</div>
    <div class="nosto_purchase_order" style="display:none" translate="no">
        <span class="order_number">1445</span>
    
        <div class="buyer">
            <span class="email">john.doe@example.com</span>
            <span class="first_name">John</span>
            <span class="last_name">Doe</span>
            <span class="marketing_permission">false</span>
        </div>
    
        <span class="payment_provider">checkmo</span>
        <span class="order_status_code">pending</span>
    
        <div class="purchased_items">
            <div class="line_item">
                <span class="product_id">Canoe123</span>
                <span class="quantity">1</span>
                <span class="name">Acme Canoe</span>
                <span class="unit_price">999.00</span>
                <span class="price_currency_code">EUR</span>
            </div>
    
            <div class="line_item">
                <span class="product_id">Canoe245</span>
                <span class="quantity">3</span>
                <span class="name">Acme Large Canoe</span>
                <span class="unit_price">19.00</span>
                <span class="price_currency_code">EUR</span>
            </div>
        </div>
    </div>

    Note: The product ID of the product tagging, cart tagging and order tagging must match. Failure to do so will lead to a mismatch in both attribution and statistics across the Nosto product.

    Tagging the buyer

    You can omit the buyer tagging either partially, or completely if you do not want Nosto to crawl this information. The user details are stored for possible marketing purposes and mainly observed email address is used in this context. Marketing permission is false by default but if this user has explicitly agreed to receive marketing then you can set it to true manually.

    Currencies should always be represented in the ISO-4471 three-letter format. For example, use the code USD instead of $ to represent the United States Dollar.

    Payment provider is the payment method or provider used by a shopper to pay the purchase. Omit the payment provider detail if you do not want Nosto to crawl this information.

    Status code will be used to track the order state. Different payment providers may use different status codes.

    Many ecommerce stores utilize SKU:s or "child" products that are sorted under the same "parent" product. To extend the above example with SKU support refer to

    In cases where a product might have multiple prices in differing currencies you can also add support for multi-currency. Refer to

    Once included on all pages, you can review if the site is transmitting data using the Nosto Debug Toolbar. If you can see order contents being picked up under "Tagging" → "Order" then the order details are correctly set up in the source code.

    You can further verify your session in the Nosto admin by using the live feed under: https://my.nosto.com/admin/$accountID/liveFeed to see if Nosto correctly picks up product view → product carted → product bought events. You can export all the order history from the store under Settings → Other → Order report under https://my.nosto.com/admin/$account/account/orders/report

    The translate attribute is a which specifies whether the value of the element and it's Text node children should be translated. If your tagging elements are being translated by e.g. Google Translator then this is the way to opt out elements being translated by Google and possibly other vendors.

    Event API: Listening to Bus Events with api.listen

    Overview

    Registers a listener for Nosto JS API events. Use this to react to specific lifecycle or user events dispatched by the Nosto client.

    api.listen(event: BusEvent, callback: (...args) => void)

    Check out the API documentation for listen

    Example Usage

    nostojs(api => {
      api.listen('taggingsent', (response) => {
        // 'response' from recommendation request 
        // consume response if necessary
        console.log('Tagging data was sent to Nosto');
      });
    });

    Supported BusEvent Types

    The following table lists all event types supported by the listen API. See this for API documentation on each of these event types and it's associated payload

    Lifecycle Events

    A Nosto recommendation request is sent to the /ev1 endpoint and returns the product recommendation(s) for placements injected on the page.

    A recommendation response is the response returned from the /ev1 endpoint.

    Event Name
    Description
    Event Name
    Description
    Event Name
    Description

    Use the unlisten method to remove a previously registered event handler for a specific event type, using the listen API method. Check out the API documentation for .

    Note: If the callback was not previously registered for the event, calling unlisten has no effect.

    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.

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

    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.

    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:

    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.

    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.

    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.

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

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

    Product images

    This article covers how Nosto provides product images in on-site recommendations and helps you how to change the image settings if these look unsharp or otherwise bad to you. On the contrary, if recommendation images load slowly the article covers also how to adjust settings to speed up image load times and other common errors with the images.

    Introduction

    The implementation of Nosto on a web-site maps out an original product image used on a product detail page (PDP) to Nosto automatically. In the background, an automated service fetches the default product image and creates eight different image versions out of the image, which are stored at Nosto and made available in our Content Delivery Network (CDN).

    Some image versions are processed slightly by resizing, cropping and zooming the image a bit. Applicable image versions in Nosto’s CDN are:

    Version
    Dimensions

    These are the the available image sizes via Nosto's CDN. Alternatively you can use original image , but in this case an image is loaded from your servers possibly affecting site load times, entirely depending on how images are hosted on your servers. Use following variable to use the original image.

    The Nosto CDN thumbnails are accessible via

    where size is a number between 1-7 basd on the version mapping above.

    For Shopify and Bigcommerce Nosto supports also direct access to Shopify and Bigcommerce CDN thumbnails.

    These are accessed via the following pattern:

    To access the first alternate image scaled down to a 300x300 image use

    The custom thumbnails are served without any cropping.

    It is also possible to leave width or height undefined to define only a limit for one of the dimensions:

    This would scale down the image to have a max width of 300 pixels.

    If a product image doesn’t appear or if the image link is broken this usually due to error in image url tagging, erroneous url mapping or a temporarily error while fetching the images from your site to Nosto’s CDN. If you very recently implemented Nosto occasionally errors might occur and re-indexation of images is needed.

    In any case, please review markup-example in tagging guide and then use the Nosto debug-toolbar and check that the image url mapped to Nosto is correct.

    If image tagging is correct, there might have been an unusual error with Nosto’s image processing. In this case a manual reindex of product details is required which is launched from the Nosto admin.

    In case Nosto is implemented on a site unaccessible from the Internet, this is expected behavior. Read about implementing Nosto on test environments.

    If you update a new image version for an old product, preferably save the image with a new file name, which typically generates a new image url for the product image as well.

    Nosto doesn’t recognize images by file size or image content but by URL, hence if a url for an image is unaltered, Nosto doesn’t update image version in it’s index and uses older version of the product image in recommendations. On the contrary, new products and product images are automatically fetched and processed and no manual input is needed.

    Manual re-index for an image version can be launched by enabling debug-mode and clicking Send Product Update Request-button on a product page.

    For a full re-index, log in to Nosto’s admin and navigate to Tools > Products > Update products.

    Typically when an image in a recommendation looks unsharp it’s simply because a small image version is upscaled to a bigger physical area in the template. Applying a bigger image version in a recommendation template will fix the issue, so essentially you simply need to adjust the styling slightly and use a bigger image version stored in Nosto’s CDN

    When creating a recommendation template on Nosto Admin Panel, you can choose which image size you would like to use. Nosto doesn’t improve or manipulate pictures, however image versions 1-6 are cropped into squares, sharpened a bit, and zoomed slightly closer to details. Two versions, seven and eight, are original versions, but with aspect limitations of 200px (7) and 400px (8) respectively.

    In the event of slow recommendation load time or increased page size, most likely a Nosto recommendation either uses an original product image or too big image version, which are downscaled to fit a smaller physical area in a browser, making it an opposite scenario to the unsharp images, with different consequences.

    Debug by reviewing that the template uses a suitable image version and not an original product image as high-resolution image versions can often exceed size of 1M, consequently slowing down the recommendation load time if multiple products are displayed in a recommendation.

    Nosto doesn’t recognize details or tell the difference between horizontal and vertical images, so be sure that most of the details of a product are in the center of the picture. Alternatively you might need to adjust the implementation slightly so that you would consistently map either horizontal or vertical version.

    Default Product Tagging

    Basic tagging

    The full schema for product tagging is defined

    or via DOM tagging

    Nosto also supports multiple optional values which may enrich the usage of the service, but are not required. These span elements should be inserted into the "nosto_product" parent container.

    Tagging attribute extension

    Prices must always be denoted in a simple numerical form using dot as the decimal separator. For example, 1.234,45 is invalid while 1234.45

    Adding support for customer group pricing

    In this article, you will learn how to implement multi-variants in Nosto. When the implementation is complete, you will be able to display different products at different prices to different customer groups.

    If either of the following cases applies to you, we recommend either an API implementation or fetching the prices via the frontend API of your platform instead of sending the price variations to Nosto. Shopify for example has the and our Shopware plugin has an that you can extend. Please or your Nosto onboarding manager for additional consultation.

    • You can only change the pricing and the availabilities using this feature.

    LLM Examples

    Large Language Models (LLMs) like GitHub Copilot, ChatGPT, Codex and Claude can significantly accelerate development with the Search Templates Starter. This guide provides proven prompts and strategies for common development tasks.

    The Search Templates Starter's well-structured codebase and modern tooling make it ideal for AI assistance:

    • Consistent patterns - The organized project structure helps LLMs understand context

    • TypeScript support - Type information provides better AI suggestions and error detection

    Implement Category pages

    Nosto provides functionality to retrieve all products for a specific category. This is useful when you want to implement category merchandising using the same API as for Search.

    Provide the API parameter to fetch all products associated with that category. Additionally should be provided for better analytics data.

    Provide the API parameter to fetch all products associated with that category. This parameter is the same as the categories product field.

    Depending on your configuration, fetching a parent category will also include products from the child categories. For example, fetching products for the category Pants would also include products from the categories Pants -> Shorts and

    REST API

    REST is the preferred option when:

    • LIM is called from a backend service

    • The agent orchestration layer is not MCP-native

    • You want maximum control over request timing and retries

    MCP

    For agent-native systems, LIM is also available via MCP, allowing it to be exposed as a callable tool within an LLM-orchestrated workflow.

    This enables:

    • Tool-based invocation from agents.

    • Cleaner separation between reasoning and retrieval.

    Create Autocomplete template

    The following data-* attributes are required by the library to handle attributions (click events) for products/keywords/history items rendered in the autocomplete result:

    data-ns-hit

    This attribute should be used on clickable keyword, product, history list elements. This attribute handles submit keyword/history as search, redirect to product, analytics (if enabled) request.

    Following table shows value for this attribute depending on the rendering context.

    Nosto MCP Server (beta)

    This MCP server provides a comprehensive set of GraphQL tools for integrating Nosto's personalization and recommendation engine into commerce applications.

    MCP (Model Context Protocol) is a standardized protocol that allows AI assistants like Claude to connect to external tools, databases and services. Think of it as a bridge that extends AI assistants capabilities with specialized functionality.

    The Nosto MCP server exposes 7 main GraphQL tools that handle different aspects of Nosto Integration:

    1. Session Managing - Creating and managing user sessions

    2. Event Tracking - Tracking user interactions and behaviour

    JS API
    Personalization via variant/SKU affinity
    multi currency or customer group pricing
    Deploy
    Adding support for customer group pricing

    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

    Detect search page

    Category query

    Category component

    Other features & implementation

    Analytics

    Implementing Search page

    6

    100x140 pixels

    7

    200x200 pixels (original aspect ratio)

    8

    400x400 pixels (original aspect ratio)

    9

    750x750 pixels (original aspect ratio)

    1

    170x170 pixels

    2

    100x100 pixels

    3

    90x70 pixels

    4

    50x50 pixels

    5

    Custom thumbnail sizes

    Troubleshooting Common Issues

    Product Images Don’t Appear

    New Product Image Doesn’t Update

    Unsharp Images

    Increased Load Time or Page Size

    Vertical and Horizontal Product Images

    30x30 pixels

    GraphQL: Placing Orders

    When a user places an order onsite or offsite, you must send the conversion tracking information to Nosto.

    Orders can be associated with a customer either by customer reference or by customer id. The customer id matches the Nosto cookie (this cookie is typically called 2c.cId).

    Tracking orders by customer id looks like the following:

    Working with recommendations

    On the Order-Confirmation Page

    To fetch the recommendations for the order-confirmation page, simply use the GraphQL field called forOrderPage to fetch all the recommendations for the order-confirmation page.

    Component-based architecture - Clear boundaries make it easier to generate focused code

  • Testing infrastructure - LLMs can generate tests alongside implementation code

  • The Search Templates Starter includes standard development patterns and documentation that LLMs can leverage:

    • AGENTS.md Standard - Follows the agents.md standardized pattern for providing AI coding agents with project-specific context, build commands, code style guidelines, and testing instructions

    • Copilot Instructions - Pre-configured GitHub Copilot instructions are included in the repository and should be customized for your specific use case

    • README patterns - Follow the established documentation structure for consistency

    Tip: Consider modifying the AGENTS.md file in your project root to align with your project's custom conventions. Following the agents.md standard ensures that all AI coding tools can consistently adjust their contributions to match your guidelines.

    Example: Replace FilterSidebar with FilterTopbar

    Example: Replace Pills with Checkboxes in Filters

    Example: Replace CSS Modules with Tailwind

    or for a full conversion

    Example: Replace Infinite Scroll with Load More Button

    Always review LLM-generated code for:

    • Adherence to project patterns and conventions

    • TypeScript type safety

    • Security considerations

    • Performance implications

    • Test coverage completeness

    Start with basic prompts and refine:

    1. Get a working implementation

    2. Ask for improvements and optimizations

    3. Add error handling and edge cases

    4. Enhance with additional features

    5. Optimize for performance and maintainability

    Use LLMs to:

    • Generate boilerplate code quickly

    • Explore different implementation approaches

    • Create comprehensive test suites

    • Document complex functionality

    But rely on human judgment for:

    • Architecture decisions

    • Security considerations

    • Performance trade-offs

    • User experience design

    Generated code doesn't follow project patterns:

    • Include more specific context about existing patterns

    • Reference specific files as examples

    • Provide the project structure in your prompt

    TypeScript errors in generated code:

    • Ask the LLM to review and fix TypeScript errors

    • Provide the exact error messages for targeted fixes

    • Include relevant type definitions in your prompt

    Tests fail or are incomplete:

    • Request test coverage for specific scenarios

    • Ask for tests that follow existing test patterns

    • Include example test files for reference

    Generated code lacks optimization:

    • Ask specifically for performance considerations

    • Request code review focusing on optimization

    • Include performance requirements in your initial prompt

    By following these patterns and examples, you can significantly accelerate your development workflow while maintaining code quality and project consistency.

    Utilizing LLMs for Development

    Why use LLMs with Search Templates Starter?

    Effective Prompting Strategies

    Follow Standard Development Patterns

    Common Development Tasks

    Component Modifications

    Styling Changes

    Search Functionality

    Best Practices for LLM-Assisted Development

    Code Review

    Iterative Refinement

    Combine with Human Expertise

    Troubleshooting LLM Issues

    Common Problems

    Pants -> Khakis
    .

    This is an admin-only setting. Please contact your Nosto representative to adjust this setting.

    In some rare cases categoryId or categoryPath is not enough. In these cases custom filters can be used to build any query for category & landing pages.

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

    To analyze user behavior you need to implement tracking. This can be achieved in two different ways, depending on the integration environment:

    • JavaScript library (recommended - more convenient, but requires a JavaScript environment).

    • GraphQL API (works anywhere).

    API Requests

    Using category ID and category path

    Using the category ID is only fully supported for Shopify merchants. Others should use the category path instead to benefit from full functionality.

    Query

    Product fields that can be requested in the hits object are documented here. All indexed fields are accessible via the API.

    Using only category path

    Query

    Product fields that can be requested in the hits object are documented here. All indexed fields are accessible via the API.

    Child category handling

    categoryId
    categoryPath
    categoryPath

    Using custom filters

    Query

    Other features & implementation

    Analytics

    Nosto Analytics

    Implement Search results page
    Requests must include a bearer token in the header.

    API keys are merchant-specific, and partners should allow merchants to provide their token as part of the integration user experience.

    Merchants can find their token by going to the Nosto Admin Portal > Account Settings > Authentication Tokens > API_LIM and click Decrypt (If as a merchant you don't see the token it's because LIM is not enabled in your account, please contact Support or your Customer Success Manager)

    At a minimum, LIM requires the fields below:

    • prompt : Free-form text describing the inferred user intent. This can be:

      • A direct user prompt

      • A normalized intent string generated by an agent

      • A short task-oriented description (e.g. “a gift for a tea lover”)

    • customerId : The Nosto unique identifier representing the shopper.

      Used to enable session-level and cross-session personalization when available. Partners can get the customerId by accessing the Nosto's 2c.cId cookie for each visitor.

    • partnerId (NEW): Your unique Nosto partner identifier provided by the partner management team.

    • chatId (NEW): Your unique identifier for the conversation, to help answer follow up questions within a conversation

    Example request:

    Required Fields for Shopify Merchants (NEW):

    For merchants using Shopify Markets, Nosto LIM resolves the correct catalog — currency, availability, and language — based on the storefront's country and locale. To support this, partners must send two additional fields for all Shopify merchants:

    • country : The ISO 3166 alpha-2 country code of the Shopify store. Supported values.

    • locale : The language code of the Shopify store. Supported values.

    Note: Shopify Markets merchants authenticate using the API token from their Nosto Markets Master account, not a per-storefront token. This is the single account Nosto uses to resolve catalog data across all connected markets — see Authentication.

    Optional Fields:

    LIM supports the following optional fields:

    • Ranking-Only Mode: The /api endpoint supports a rankingOnly (boolean) flag that controls how the LLM processes product results:

      • rankingOnly: false (default): The LLM returns both a ranked selection of products and the agent response (a natural language conversational response).

      • rankingOnly: true: The LLM only ranks/selects the best matching products without generating agent insights. Using this option reduces latency.

    • Max Agent Recommendations Controls: The /api endpoint supports the maxAgentRecommendations (integer | null) field that controls the number of products returned. The default is 3, and the value range is 1 to 20.

    • Agent Context: The /api endpoint supports the giveAgentContext (boolean) flag. When true, LIM returns a concise context describing why LIM selected the returned products.

    • Agent Insight Format: The /api endpoint supports the responseFormat (string) field to define how the natural language response is returned; by default is plain text, but for partners looking to show the response directly to shoppers, the markdown option is also supported.

    Nosto LIM returns a ranked list of product recommendations, suitable for direct rendering or further reasoning by an agent.

    • agentInsight : A natural language conversational response (returned by default. When the flag rankingOnly is set to true, it's returned as an empty string)

    Example “These shoes feature air-cushioned soles: Dr. Martens Cherry Shoe - An original 3-eye shoe with distinctive air-cushioned soles and classic Docs DNA, priced at €100. 3989 Bex Brogue - This shoe combines durability and comfort with Dr. Martens’ air-cushioned sole, priced at €120. NMD_R1 Primeknit ‘Tricolor Black’ - Offers responsive cushioning with a Boost sole, priced at €100.”

    • agentContext: A concise context for agents describing why LIM selected the returned products (returned when the giveAgentContext flag is set to true)

    Example: "User asks for a sweater recommendation for summer; shopping intent true; Selected in-catalog 100% cashmere sweaters as lightweight layering options; Chose 3 products from the keyword/vector summer cashmere search results"

    • agentProductRecommendations : A list of products based on agentic recommendations (which combines Nosto Semantic Search, Core Recommendation Pipeline with Predictive, Semantic, and Visual AI + LLM ranking)

    When to use REST?

    Endpoint

    Authentication

    Request Payload

    Response

    Easier orchestration in IDEs and agent frameworks that support MCP.

    You can use an MCP inspector to test the MCP protocol locally.

    The currently registered MCP tool is:

    • getRecommendation

    Requests must include a bearer token in the header.

    At a minimum, LIM requires the fields below:

    • prompt : Free-form text describing the inferred user intent. This can be:

      • A direct user prompt

      • A normalized intent string generated by an agent

      • A short task-oriented description (e.g. “a gift for a tea lover”)

    • customerId : The Nosto unique identifier representing the shopper.

      Used to enable session-level and cross-session personalization when available. Partners can get the customerId by accessing the Nosto's 2c.cId cookie for each visitor.

    • partnerId (NEW): Your unique Nosto partner identifier provided by the partner management team.

    • chatId (NEW): Your unique identifier for the conversation, to help answer follow up questions within a conversation

    Example request:

    Required Fields for Shopify Merchants (NEW):

    For merchants using Shopify Markets, Nosto LIM resolves the correct catalog — currency, availability, and language — based on the storefront's country and locale. To support this, partners must send two additional fields for all Shopify merchants:

    • country : The ISO 3166 alpha-2 country code of the Shopify store. Supported values.

    • locale : The language code of the Shopify store. Supported values.

    Note: Shopify Markets merchants authenticate using the API token from their Nosto Markets Master account, not a per-storefront token. This is the single account Nosto uses to resolve catalog data across all connected markets — see Authentication.

    Optional Fields:

    LIM supports the following optional fields:

    • Ranking-Only Mode: The tool supports a rankingOnly (boolean) flag that controls how the LLM processes product results:

      • rankingOnly: false (default): The LLM returns both a ranked selection of products and the agent response (a natural language conversational response).

      • rankingOnly: true: The LLM only ranks/selects the best matching products without generating agent insights. Using this option reduces latency.

    • Max Agent Recommendations Controls: The tool supports the maxAgentRecommendations (integer | null) field that controls the number of products returned. The default is 3, and the value range is 1 to 20.

    • Agent Context: The tool supports the giveAgentContext (boolean) flag. When true, LIM returns a concise context describing why LIM selected the returned products.

    • Agent Insight Format: The /api endpoint supports the responseFormat (string) field to define how the natural language response is returned; by default is plain text, but for partners looking to show the response directly to shoppers, the markdown option is also supported.

    Nosto LIM returns a ranked list of product recommendations, suitable for direct rendering or further reasoning by an agent.

    • agentInsight : A natural language conversational response (returned by default. When the flag rankingOnly is set to true, it's returned as an empty string)

    Example “These shoes feature air-cushioned soles: Dr. Martens Cherry Shoe - An original 3-eye shoe with distinctive air-cushioned soles and classic Docs DNA, priced at €100. 3989 Bex Brogue - This shoe combines durability and comfort with Dr. Martens’ air-cushioned sole, priced at €120. NMD_R1 Primeknit ‘Tricolor Black’ - Offers responsive cushioning with a Boost sole, priced at €100.”

    • agentContext: A concise context for agents describing why LIM selected the returned products (returned when the giveAgentContext flag is set to true)

    Example: "User asks for a sweater recommendation for summer; shopping intent true; Selected in-catalog 100% cashmere sweaters as lightweight layering options; Chose 3 products from the keyword/vector summer cashmere search results"

    • agentProductRecommendations : A list of products based on agentic recommendations (which combines Nosto Semantic Search, Core Recommendation Pipeline with Predictive, Semantic, and Visual AI + LLM ranking)

    The following configuration can be used with MCP-compatible tools and IDEs (e.g. Cursor):

    Once configured, LIM becomes available to the agent as a callable external capability.

    MCP tool

    Authentication

    Request Payload

    Response

    MCP server Configuration

    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>
        )
    }
    $!product.imageUrl
    $!product.thumb(size)
    $!product.image.getThumb(300, 300)
    $!product.alternateImages[0].getThumb(300, 300)
    $!product.image.getThumb(300, null)
    mutation {
      placeOrder(by:BY_REF, id: "514421fce84abcb61bd45241", params: {
        customer: {
          firstName: "Mridang"
          lastName: "Agarwalla"
          email: "mridang@nosto.com"
          marketingPermission: false
        }
        order: {
          number: "25435"
          orderStatus: "paid"
          paymentProvider: "klarna"
          ref: "0010"
          purchasedItems: [
            {
              name: "Shoe"
              productId: "1"
              skuId: "11"
              priceCurrencyCode: "EUR"
              unitPrice: 22.43
              quantity: 1
            }
          ]
        }
      }) {
        id
      }
    }
    mutation {
      placeOrder(by:BY_CID, id: "5d3ef53010b4f8a24c2acf9a", params: {
        customer: {
          firstName: "Mridang"
          lastName: "Agarwalla"
          email: "mridang@nosto.com"
          marketingPermission: false
        }
        order: {
          number: "25435"
          orderStatus: "paid"
          paymentProvider: "klarna"
          ref: "0010"
          purchasedItems: [
            {
              name: "Shoe"
              productId: "1"
              skuId: "11"
              priceCurrencyCode: "EUR"
              unitPrice: 22.43
              quantity: 1
            }
          ]
        }
      }) {
        id
      }
    }
    mutation {
      placeOrder(by:BY_REF, id: "514421fce84abcb61bd45241", params: {
        customer: {
          firstName: "Mridang"
          lastName: "Agarwalla"
          email: "mridang@nosto.com"
          marketingPermission: false
        }
        order: {
          number: "25435"
          orderStatus: "paid"
          paymentProvider: "klarna"
          ref: "0010"
          purchasedItems: [
            {
              name: "Shoe"
              productId: "1"
              skuId: "11"
              priceCurrencyCode: "EUR"
              unitPrice: 22.43
              quantity: 1
            }
          ]
        }
      }) {
        id
        pages {
          forOrderPage(value: "25435", params: {
            imageVersion: VERSION_3_90_70
            isPreview: true
          }) {
        divId
            resultId
            primary {
              productId
            }
          }
        }
      }
    }
    Replace the existing FilterSidebar component with a FilterTopbar component that displays filters horizontally at the top of the search results.
    
    Update the layout in Serp component to render FilterTopbar above the products grid instead of FilterSidebar in the sidebar. Maintain all existing filter functionality while adapting the responsive design for horizontal layout.
    Replace the Pill components used in FilterSidebar with checkbox inputs for better accessibility and mobile usability.
    
    Update the filter display to show checkboxes with labels instead of pill-style buttons, while maintaining the same filter state management and visual feedback for selected filters.
    Convert the Button component from CSS modules to Tailwind CSS classes.
    
    Replace the current Button.module.css imports and className usage with Tailwind utility classes, maintaining the same visual appearance and hover states.
    Convert the CSS module based styling with Tailwind using standard Tailwind build pipelines.
    Replace the current pagination in search results with a "Load More" button that appends new results to the existing list.
    
    Update the Pagination component to show a centered button instead of page numbers, and modify the search state to accumulate results rather than replace them.
    query {
      search(
        accountId: "YOUR_ACCOUNT_ID"
        products: {
          categoryId: "123456789",
          categoryPath: "Pants"
        }
      ) {
        products {
          hits {
            productId
            name
            url
            imageUrl
            price
          }
          total
          size
        }
      }
    }
    query {
      search(
        accountId: "YOUR_ACCOUNT_ID"
        products: {
          categoryPath: "Pants"
        }
      ) {
        products {
          hits {
            productId
            name
            url
            imageUrl
            price
          }
          total
          size
        }
      }
    }
    query {
      search(
        accountId: "YOUR_ACCOUNT_ID"
        products: {
          preFilter: [
            {
              field: "productId",
              value: [
                "2276",
                "2274"
              ]
            }
          ],
        }
      ) {
        products {
          hits {
            productId
            name
          }
          total
          size
        }
      }
    }
    POST https://lim.nosto.com/api
    Authorization: Bearer xxx-xx-xxxxxxxxxxxx
    {
      "prompt": "What dresses do you recommend for a summer party?",
      "customerId": "xxxxx",
      "partnerId": "xx-xxxxxx",
      "chatId": "xxxxx"
    }
    agentProductRecommendations (array<object> | null)
        Any of (array<object> | null)
                Items object
                    productId string
                    name (string | null)
                    url (string | null)
                    description (string | null)
                    imageUrl (string | null)
                    price (number | null)
                    currencyCode (string | null)
                    brand (string | null)
                    availability (string | null)
                    priceCurrencyCode (string | null)
                    listPrice (number | null)
                    ratingValue (number | null)
                    reviewCount (integer | null)
                    inventoryLevel (integer | null)
                    isExcluded (boolean | null)
                    onDiscount (boolean | null)
                    skus (array<object> | null)
                        Any of (array<object> | null)
                            Items object
                                id string
                                inventoryLevel (integer | null)
                                availability (string | null)
                                name(string | null)
    Authorization: Bearer xxx-xx-xxxxxxxxxxxx
    {
      "prompt": "What dresses do you recommend for a summer party?",
      "customerId": "xxxxx",
      "partnerId": "xx-xxxxxx",
      "chatId": "xxxxx"
    }
    agentProductRecommendations (array<object> | null)
        Any of (array<object> | null)
                Items object
                    productId string
                    name (string | null)
                    url (string | null)
                    description (string | null)
                    imageUrl (string | null)
                    price (number | null)
                    currencyCode (string | null)
                    brand (string | null)
                    availability (string | null)
                    priceCurrencyCode (string | null)
                    listPrice (number | null)
                    ratingValue (number | null)
                    reviewCount (integer | null)
                    inventoryLevel (integer | null)
                    isExcluded (boolean | null)
                    onDiscount (boolean | null)
                    skus (array<object> | null)
                        Any of (array<object> | null)
                            Items object
                                id string
                                inventoryLevel (integer | null)
                                availability (string | null)
                                name(string | null)
    {
      "mcpServers": {
        "nosto-lim": {
          "transport": "streamable-http",
          "url": "https://lim.nosto.com/mcp",
          "headers": {
            "Authorization": "Bearer xxxx-xxx-xxxxxxxxxxxxx"
          }
        }
      }
    }

    popupclosed

    When a popup is closed in the page. Refer to our API documentation on .

    popupribbonshown

    When a popup ribbon is activated on page load or when popup is minimized. Refer to our API documentation on .

    prerequest

    Before a recommendation request is sent to Nosto. Payload is the data that's sent in the recommendation request. Refer to our API documentation on prerequest.

    prerender

    After receiving response from the recommendation request but before recommendations are rendered. Refer to our API documentation on prerender.

    postrender

    **Only For HTML response mode.

    After recommendations are rendered/injected on the page. Refer to our API documentation on postrender.

    taggingsent

    After receiving recommendation response and the recommendations are rendered. Payload is the response data from the recommendation request, all placement - campaign markup mapping that will be injected (unFilledElements) and all placement -campaign markup mapping that's injected (filledElements). Refer to our API documentation on taggingsent.

    taggingresent

    When tagging data is resent. Associated with sendTagging (a.k.a resendAllTagging) API. Payload is the tagging data from the store page. Refer to our API documentation on taggingresent.

    carttaggingresent

    When cart contents are resent to Nosto either using the resendCartTagging or resendCartContent API. Payload is the cart items extracted from tagging (resendCartTagging) or the cart items from the supplied cart object (resendCartContent). Refer to our API documentation on carttaggingresent.

    customertaggingresent

    When customer info is resent to Nosto from page tagging. Associated with the resendCustomerTagging API. Payload is the customer info extracted from the page tagging. Refer to our API documentation on customertaggingresent.

    emailgiven

    When customer info is sent to Nosto either using the customer API or using the discount popup. Payload is the customer object which is being sent to Nosto. Refer to our API documentation on emailgiven.

    popupopened

    When a discount popup is opened and displayed on the store page. Payload is the campaign ID associated with the popup and the trigger that caused the popup to display. Refer to our API documentation on popupopened.

    popupmaximized

    When a popup is maximized from ribbon mode. Payload is the campaign ID associated with the popup. Refer to our API documentation on popupmaximized.

    popupminimized

    When a popup is minimized into a ribbon. Payload is the campaign ID associated with the popup. Refer to our API documentation on popupminimized.

    cartupdated

    **Only for Shopify merchants.

    Whenever the a product is added or removed from cart. Refer to our API documentation on cartupdated.

    // Register a listener
    api.listen('taggingsent', onTaggingSent);
    
    // Unregister the listener
    api.unlisten('taggingsent', onTaggingSent);
    
    function onTaggingSent(response) {
      console.log('Tagging sent:', response);
    }

    Popup Events

    Tagging Events

    Unregistering listener

    Example usage

    unlisten
    is valid.

    Categories must always be delimited by a slash. For example, /Home/Accessories is a valid category while Home > Accessories is not.

    Currencies should always be represented in the ISO-4471 three-letter format. For example, use the code USD instead of $ to represent the United States Dollar.

    The availability of a product is represented by InStock or http://schema.org/InStock for products that are in stock and saleable. For products that are out of stock or you don't want to be recommended, you can use OutOfStock or http://schema.org/OutOfStock

    The category of your item based on the Google product taxonomy. Use the schema provided by Google here (https://support.google.com/merchants/answer/6324436?hl=en)

    The rating of a product must be represented as a number between 0.0 and 5.0. For example, a product cannot be rated 9.1. You must normalize your rating value to fit our specified range.

    The three tag fields, tags1, tags2 and tags3 are simply labels that can be used to annotate tags like discounted, limited collection or other use cases where you might want to filter your Nosto recommendations by certain product groupings.

    Custom fields accept a key:value pair where the class of the attribute is the key. Common use cases are material, color or other similar unique identifiers.

    It is possible to tag also the currently viewed product sku. Typically, this would be done on a product detail page when the user chooses a specific color or size and you would like to update recommendations to highlight other products with similar attributes. Most common approach would be to implement it by calling the Session API or the JS API from a click-listener to send the sku information and update the recommendations. If, however, the preference is to use tagging to specify the selected sku instead, that can be done through tagging by adding a span under product with the class name selected_sku_id, for example: <span class="selected_sku_id">40822930473153</span>

    Nosto also supports two attributes that are not crawlable through tagging. This is due to the sensitive nature of the attributes. Those are: supplier_cost and inventory_level. To send these two values to Nosto you will need to use the Products API.

    Many e-commerce stores utilize SKU:s or "child" products that are sorted under the same "parent" product. To extend the above example with SKU support refer to this article

    In cases where a product might have multiple prices in differing currencies, you can also add support for multi-currency. Refer to this article

    If you want to use Nosto’s margin filter, you need to send supplier cost via API since it's a sensitive data that you might not want to expose in the product tagging.

    Once included on all pages, you can review if the site is transmitting data using the Nosto Debug Toolbar. If you can see product attributes being picked up under "Tagging" then the product details are correctly set up. You can further verify that products are being indexed to the catalog under the Nosto admin by navigating to the Catalog Explorer: https://my.nosto.com/admin/$accountID/products

    Nosto debug toolbar / products
    The UI of the Catalog Explorer (CE)

    The translate attribute is a HTML5 standard attribute which specifies whether the value of the element and it's Text node children should be translated. If your tagging elements are being translated by e.g. Google Translator then this is the way to opt out elements being translated by Google and possibly other vendors.

    nostojs(api => {
      api.setTaggingProvider("pageType", "product")
      api.setTaggingProvider("products", [
        {
          product_id: "Canoe123",
          name: "Acme Canoe",
          url: "https://example.com/canoe123",
          image_url: "https://image.example.com/canoe1.jpg",
          availability: "InStock",
          price: 999.5,
          price_currency_code: "USD"
        }
      ])
    })
    <div class="nosto_page_type" style="display:none" translate="no">product</div>
    <div class="nosto_product" style="display:none" translate="no"> 
      <span class="product_id">Canoe123</span>
      <span class="name">Acme Canoe</span>
      <span class="url">https://example.com/canoe123</span>
      <span class="image_url">https://image.example.com/canoe1.jpg</span>
      <span class="availability">InStock</span>
      <span class="price">999.50</span>
      <span class="price_currency_code">USD</span>
    </div>
    <span class="category">/Mens/Jackets</span>
    <span class="category">/Mens/Jackets/Ski Jackets</span>
    <span class="brand">Acme</span>
    <span class="description">This is a great product!</span>
    <span class="google_category">Interior > Towels</span>
    <span class="list_price">1299.00</span>
    <span class="tag1">sporty</span>
    <span class="tag2">new-in</span>
    <span class="tag3">limited-offer</span>
    <span class="tag3">add-to-cart</span>
    <span class="rating_value">3.8</span>
    <span class="review_count">36</span>
    <span class="alternate_image_url">https://image.example.com/canoe2.jpg</span>
    <span class="alternate_image_url">https://image.example.com/canoe3.jpg</span>
    <span class="custom_fields">
      <span class="material">Cotton</span>
      <span class="weather">Summer</span>
    </span>

    To learn more about the api.setTaggingProvider usage, please refer to the official API documentation.

    Tagging the prices

    here

    Tagging the categories

    Tagging the currencies

    Tagging the availability

    Tagging the Google Categories

    Tagging the Rating

    Tagging the Tags and/or Custom fields

    Tagging the currently viewed sku

    Fields that are not exposable in tagging

    Adding support for advanced use cases

    Supplier Cost / Margin Filter

    Troubleshooting

    Translate attribute

    You cannot use SKUs with this feature at the time of writing.
  • You cannot use Nosto multi currency with this feature.

  • You will need to implement the multi-variate tagging if you have any such scenarios:

    • Your store has different prices for B2B and B2C customers

    • Your store has different prices for logged-in and logged-out customers

    • Your store has different prices for loyalty customers

    • Your store has different prices and availabilities for different locations

    Prior to the multi-variate implementation, ensure that the Nosto tagging is correctly in place. Some of the tagging must be slightly amended to support multi-variants.

    The product page tagging must be amended to denote the primary variation code of the product.

    For example, a retailer who has different prices for normal and loyal customers would have GENERAL as the default variation id and LOYAL as an extra variation.

    or using DOM tagging

    Ensure that a span element with the class variation_id is added as a child of the nosto_product element within the product page tagging.

    Note: The code in the variation_id element must remain static, regardless of the current context. For example, if a loyal customer is logged in, the variation_id field would still GENERAL and not change.

    The cart and order tagging can be left as-is but the prices must be in the customer's currently active currency. For example, a customer shopping in Swiss Francs (CHF) should have all the cart items tagged in Swiss Francs (CHF). Failure to do so will result in incorrect prices in any triggered emails such as abandoned cart or order followup.

    Once you have amended the product tagging, an additional DIV element must be added to all the other pages (including the product page itself). The tag should not be encapsulated in the nosto_product DIV tag. The information sent in the tag refers to the segment of the customer.

    or via DOM tagging

    For example, on the site of a retailer, who has different prices for normal (GENERAL) and loyal (LOYAL) customers, if the customer is a logged in customer and is a known loyalty customer, the nosto_variation element should show LOYAL. If the customer logs out or a new customer visits, and there is no way to identify him as a loyal customer, the nosto_variation element should show GENERAL.

    The search/category merchandising (universal) API supports specifying the variation ID in requests using the variationId property of the products object.

    If specified, the selected variation's specific properties automatically replace the corresponding values in the top-level product. All features (e.g., merchandising rules, facets, filters, sorting) work with the selected variation's values automatically when the variation ID is provided.

    For Nosto code editor integrations, please refer to a simplified version of this process.

    Once the tagging changes have been done and the API implemented, you need to configure and enable it from your admin panel under Settings > Other > Multi-Currency. Toggle the Use Multiple Currencies switch on and Use Exchange Rates switch off and set the variation ID of the primary currency via the input field and toggle on the exchange rates switch.

    Note: Ensure that the Variation ID of the primary currency matches the value sent via the variation_id element in the product tagging.

    Note: Multi-variants cannot be used in conjunction with exchange-rates based multi-currency feature. You must keep the Use Exchange Rates switch off.

    You will also need to configure the price formatting for your primary and secondary currencies.

    Once you enabled multi-variants you can preview the product prices for different groups by navigating to Tools > Products and choosing a product.

    You will see one or more dropdowns that contain the prices and the availability for that group.

    When you have reviewed your set-up, you’re all set and ready to go live with our features. Nosto will automatically handle the different customer groups across its feature set.

    To learn more about the api.setTaggingProvider usage, please refer to the official API documentation.

    Edge cases

    Storefront API
    endpoint for multi-currency
    reach out to our team
    nostojs(api => {
      api.setTaggingProvider("products", [{
        variation_id: "GENERAL", // Primary variation
        variations: {
          LOYAL: {
            variation_id: "LOYAL",
            price_currency_code: "EUR",
            price: "27.00",
            list_price: "45.19",
            availability: "InStock"
          },
          B2B: {
            variation_id: "B2B",
            price_currency_code: "GBP",
            price: "24.00",
            list_price: "41.55",
            availability: "OutOfStock"
          }
        }
      }])
    });
    <div class="nosto_product" style="display: none;" translate="no">
      ...
      ...
      ...
      <!-- Variation ID for the primary currency --> 
      <span class="variation_id">GENERAL</span>
      <!-- Variation block for a secondary currency -->
      <div class="variation">
        <span class="variation_id">LOYAL</span>
        <span class="price_currency_code">EUR</span>
        <span class="price">27.00</span>
        <span class="list_price">45.19</span>
        <span class="availability">InStock</span>
      </div>
      <!-- Variation block for a secondary currency -->
      <div class="variation">
        <span class="variation_id">B2B</span>
        <span class="price_currency_code">GBP</span>
        <span class="price">24.00</span>
        <span class="list_price">41.55</span>
        <span class="availability">OutOfStock</span>
      </div>
    </div>
    nostojs(api => {
      api.setTaggingProvider("variation", "GENERAL")
    })
    <div class="nosto_variation" style="display: none;">GENERAL</div>

    Ideal use cases

    Changes to the product tagging

    What about the prices in the cart and the order tagging?

    Specifying the active variation

    Integration with search/category merchandising (universal)

    Enabling multi-variants from the admin

    Reviewing your changes

    See also

    Context
    Value

    keyword

    value from response.data.search.keywords

    Code example:

    Value example:

    product

    productId and url from response.data.search.products.hits

    Code example:

    Value example:

    history

    historyEnabled & historySize config. Refer

    data-ns-remove-history

    This attribute should be used to delete history entries in the autocomplete

    To make an element delete a single history entry when clicked, add data-ns-remove-history={hit.item} to an element. In order to delete all history entries, add data-ns-remove-history="all" to clear button.

    This section provides links to default startup templates for different rendering frameworks. These templates can be copied and customized as needed.

    Handlebars, Mustache, Liquid, React/Preact (HTML)

    Attributes

    Encode HTML content

    This is specific to cases where no template language like liquid/handlebars is used and the content is rendered using plain HTML.

    Make sure to HTML encode content passed to this attribute. Bacause JSON.stringify may produce result that can't be directly used in HTML especially when the content includes special characters.

    For example, consider the below example

    {
      "keyword": "new year's eve",
      "_highlight": { "keyword": "new year's eve" }
    }

    can be encoded as

    Starter templates

    Mustache helpers

    Mustache is based on logic-less templates which can be enhanced with helpers, e.g toJson, imagePlaceholder, showListPrice in example template.

  • Recommendations - Fetching personalized product recommendations

  • Complete Workflows - End-to-end integration patterns

  • Service Layer - Clean architecture with service classes

  • Cookie Management - Session persistence via cookies

  • Concept Explanation - Educational content about Nosto GraphQL concepts

  • There are also 2 Documentation tools

    1. Feature Documentation - RAG-powered feature documentation search

    2. Technical Documentation - RAG-powered code/technical documentation search

    Server URL https://dev.mcp.nosto.com/mcp

    Nosto MCP server specializes in:

    • Nosto GraphQL API Integration

    • Multi-framework support (React, Next Js, Shopify Hydrogen, etc.)

    • Production-ready code generation

    • Best practives enforcement

    • Complete workflow documantation

    • Technical documentation from

    Step 1

    Configure MCP

    Add the Nosto MCP server to your Claude Code configuration:

    Step 2

    Verify Connection

    Test the connection by asking Claude:

    Claude should respond with the 7 GraphQL specific tools and 2 (RAG) documentation tools.

    Step 3

    Start Building

    Now you can request complete integrations:

    Claude will automatically use the MCP tools to generate complete, production-ready code, including the creation of Nosto specific service, creating a newSession request, storing the sessionId, and calling updateSession and retrieve recommendations.

    MCP server is aware of the step by step workflow of how Nosto should be implemented and how graphQL mutations should look. This means that asking AI assistant such as:\

    Will look through multiple tools exposed by the Nosto MCP Server

    And will generate the Nosto service code which is aware of creating a new session, storing sessionId in the cookie, and using the sessionId for update session mutations and getting recommendations.

    End result is visible Nosto recommendations on home and product pages within minutes.

    The MCP server provides 7 specialized graphql tools that can be combined. to create complete e-commerce solutions.

    Common usage patters:

    1. Homepage integration "Add Nosto Recommendation to my React homepage" -> Uses generate_nosto_service + generate_complete_workflow tools

    2. Product page setup "Set up Nosto on my Shopify Hydrogen product page" -> Uses generate_nosto_service + generate_complete_workflow tools + Includes Shopify GID handling

    3. Explanation of the Nosto "Explain how Nosto can be integrated on my store" -> Uses generate_complete_workflow + concept_explainer

    If you have found MCP server useful, or if you found some issues or would like MCP server to support more use-cases please don't hesitate to contact us at platforms@nosto.com

    \

    Nosto MCP Server is in Beta state, and any generated code should be reviewed.

    What is an MCP Server?

    Overview of tools

    {
      "mcpServers": {
        "nosto-graphql": {
           "type": "http",
           "url": "https://dev.mcp.nosto.com/mcp",
           "env": {}
        }
      }
    }
    
    Can you list the available Nosto GraphQL tools?
    Add Nosto product recommendations to my home page

    Nosto MCP Server

    How to use Nosto MCP Server

    Nosto MCP server can be used with any AI Assistant that support Model Context Protocol. Claude code is used in the documentation below.

    Quick Start with Claude Code

    Prerequisites: You need access to Claude Code (Anthropic's IDE integration) to use Nosto MCP servers.

    Reasons to use MCP Server

    Available GraphQL Integration Patterns

    Feedback

    Tagging the currencies

    Tagging the payment provider

    Tagging the order status code

    Adding support for advanced use cases

    Troubleshooting order tagging:

    Translate attribute

    this article
    this article
    HTML5 standard attribute
    official API documentation
    Nosto debug toolbar order
    live-feed-product-order

    Building your implementation plan

    Nosto provides plugins for the most common eCommerce platforms like Shopify, Magento 2, Shopware 6, BigCommerce and Prestashop.

    • The plugins have implemented the Nosto backend APIs so in most cases 90% of the product catalog sync is already handled.

      • If your client has custom use cases, you likely will have to extend the Nosto plugin, change the data structure within your platform or make use of one of the Nosto backend APIs to ensure all the needed product data is in Nosto.

    • Depending on your platform, a Nosto plugin (like for Shopware or Magento 2) extends the default theme (like Dawn, Storefront, Luma or Hyvä) and implements the frontend mechanisms that are explained in the following.

      • Some platform plugins only include a very basic frontend component like adding the Nosto script and the page tagging.

      • If your client has a custom frontend (running headless or a SPA*), you will need to implement the frontend mechanisms one by one.

    *This guide focuses on classic web applications that work with full page reloads. For SPAs/PWAs like React, the same principles apply but they are handled differently. We encourage you to first read through the guiding principles and in a next step make yourself familiar with the implementation within a Single Page Application.

    1. Sending/updating the Product Catalog via GraphQL API (or REST API) to Nosto regularly.

      • For custom platforms (without Nosto plugin), you must use one of the APIs and build the product sync yourself (no CSV or feed option). If you’re working with PHP, you can use the Nosto PHP-SDK.

    2. Page tagging via JavaScript or hidden HTML on all pages

    Nosto provides like Shopify, Magento 2, Shopware 6, BigCommerce and Prestashop.

    We recommend using our plugins as a base since they follow platform- and Nosto-specific best practices, save you lots of development time and have been tested in hundreds of eCommerce sites.

    Every Nosto module falls into one of two module types which have their own set of possible implementation methods:

    1. Campaign Widgets: Product Recommendations, Dynamic Bundles and Onsite Content Personalization (via placements, see previous section, points 3 and 4)

    2. Listings: Search and Category Merchandising (replacing native content (Autocomplete/Search Preview, SERPs, PLPs), see previous section, point 5)

    All of the following questions can be answered with our general and platform-specific documentation. For custom builds and advanced implementations you find additional playbooks below. Our onboarding team is happy to assist you with the evaluation and planning.

    We have prepared dedicated playbooks for advanced setups.

    In these cases you might be able to use parts of the Nosto plugins (e.g. for a Shopify headless frontend you can use our app for the data sync but need to implement the frontend components yourself).

    We're happy to help you find the ideal approach for your particular tech stack. Please read the matching playbook prior to our conversation:

    • Headless and SPA (Single Page Application) Frontend:

    • Shopify Hydrogen:

    • Magento Hyvä:

    Depending on which Nosto modules your client has purchased, you need to determine how to implement each of those types. You can weigh the different methods and find the best approach for your tech stack, preferences and client needs with the resources below.

    1. Campaign Widgets:

    2. Listings:

    FAQ

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

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

    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()]

    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

    init({
        ...
    }).then(() => {
        if (location.hostname === 'de.website.com') {
            document.body.classList.add('nosto-search-de')
        }
    })

    Example CSS:

    .nosto-search-de .my-title {
        color: black;
    }

    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:

    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.

    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.

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

    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 .

    Adding support for multi-currency

    In this article, you will learn how to implement multi-currency in Nosto. When the implementation is complete, you will be able to display product prices (in any feature) in different currencies.

    Prior to the multi-currency implementation, ensure that the Nosto tagging is correctly in place. Some of the tagging must be slightly amended to support multi-currency.

    Shopify Multi-Currency

    For instructions on integrating with Shopify's multi-currency, please go here.

    Changes to the product tagging

    The product page tagging must be amended to denote the primary currency code of the product. Typically, most retailers have a primary currency which is the default currency of the inventory.

    For example, a US-based retailer who sells in Euros (EUR) and Sterling Pounds (GBP) would have US Dollar (USD) as the primary currency while Euro (EUR) and Sterling Pounds (GBP) would be secondary currencies whose exchange rates would need to be sent via an API.

    nostojs(api => {
      api.setTaggingProvider("products", [{
        ...
        variation_id: "USD"
      }])
    })

    or via DOM tagging

    Ensure that a span element with the class variation_id is added as a child of the nosto_product element within the product page tagging.

    Note: The code in the variation_id element must remain static, regardless of the currency active on-site. This is the primary currency of your catalog. Although variation_id element often has the same currency code as in the price_currency_code element and may seem redundant, they support different use cases and both need to be tagged.

    Yes. Prices for all SKUs will automatically be converted using the same logic. As long as your SKUs are tagged, no additional changes are needed.

    The cart and order tagging can be left as-is but the prices must be in the customer's currently active currency. For example, a customer shopping in Swiss Francs (CHF) should have all the cart items tagged in Swiss Francs (CHF). Failure to do so will result in incorrect prices in any triggered mails such as abandoned cart or order followup.

    Once you have amended the product tagging, an additional DIV element must be added to all the other pages (including the product page itself). The tag should not be encapsulated in the nosto_product DIV tag. The information sent in the tag refers to the currency active of the customer.

    or via DOM tagging

    For example, on the site of a US-based retailer who sells in Euros (EUR) and Sterling Pounds (GBP), if the customer changes the currency to Sterling Pounds (GBP), the nosto_variation element should show GBP. If the customer changes the currency to Euros (EUR), the nosto_variation element should show EUR.

    In order to send the exchange rate multipliers to Nosto, you will need to use . Below is a small snippet of what the payload looks like.

    In the example above, 0.77 is the exchange rate from US Dollars (USD) to British Pounds (GBP) and 0.91 is the exchange rate from US Dollars (USD) to Euros (EUR).

    The valid_until entry defines the expiration date. When the expiration date is reached, the exchange rates won't be applied anymore and prices will be hidden for all the secondary currencies to prevent displaying outdated prices.

    When recommendations are served, then exchange rates are dynamically applied to the product prices to reflect the active currency.

    Once the tagging changed have been done and the API implemented, you need to configure and enable it from your admin panel under Settings > Other > Multi-Currency. Toggle the Use Multiple Currencies and Use Exchange Rates switches on and set the variation ID of the primary currency via the input field and toggle on the exchange rates switch.

    Note: Ensure that the Variation ID of the primary currency matches the value sent via the variation_id element in the product tagging.

    You will also need to configure the price formatting for your primary and secondary currencies.

    The search/category merchandising (universal) API supports specifying the variation ID in requests using the variationId property of the .

    If specified, the selected variation's specific properties automatically replace the corresponding values in the top-level product. All features (e.g., merchandising rules, facets, filters, sorting) work with the selected variation's values automatically when the variation ID is provided.

    For Nosto code editor integrations, please refer to a .

    Once you enabled multi-currency and made an API call, you can review the exchange rates received by Nosto by navigating to Settings > Other > Multi-currency.

    You can also preview the product prices for different currencies by navigating to Tools > Products and choosing a product.

    You will see one or more dropdowns that contain the prices and price calculation for the currency.

    When you have reviewed your set-up, Nosto updates in real-time product prices for all the currencies and display the appropriate currency to the right target groups of users. You’re all set and ready to go live with our features.

    To learn more about the api.setTaggingProvider usage, please refer to the .

    Getting started

    Before you begin, ensure you have the following installed on your system:

    • Node.js (v24 or higher) -

    • npm (comes with Node.js) or yarn as your package manager

    Nosto CLI

    The Nosto CLI is a command-line tool that streamlines the development workflow for modern and legacy Nosto Search Templates.

    If you prefer to develop the search templates on your own machine, using your own tooling such as git or eslint, you may prefer to utilize the tool, .

    The primary purpose of the tool is to fetch and upload the sources and build artifacts of your search templates for local development, simplifying the process and minimizing friction. It supports local builds that do not rely on the VSCode Web extension, and it includes convenient development mode which automates build-and-upload loop you would have to perform manually through VSCode Web otherwise.

    Since October 2025, the Nosto CLI is the recommended way to work with Search Templates.

    Safety Notice If you have concerns about running Nosto CLI on your machine, you can examine the to ensure it meets your security requirements.

    Historically, Search Templates have been developed using a VSCode Web Extension as part of Nosto Admin UI. This introduced a number of limitations - such as inability to integrate TypeScript, ESLint or LLM tools - and Nosto is moving away from the web extension in favor of Nosto CLI and local development workflows.

    Initialization

    After installation, you can import and use the library in your JavaScript or TypeScript files. Library attaches to existing search input element. Once a search input is loaded, autocomplete function can initialize event listeners for the Search Autocomplete.

    ❗autocomplete should be called once.❗

    Property
    Required
    Default Value
    Description
    popupclosed
    popupribbonshown

    There are several page types, so please make sure they are covered in your QA checklist. (You can use ours as a blueprint.)

  • The current cart content and logged in user information need to be available on all pages.

  • Placements (empty divs) where Nosto content can be injected.

    • These placements should be added into the HTML, most Nosto plugins offer CMS blocks for easier use.

  • Injecting personalized content into the placements of the current page (banners, product recommendation carousels/grids, ...)

    • This involves a simple request/response pattern: Tell Nosto which placements are on the current page and you’ll receive the content as JSON or HTML.

  • Replacing native platform features like product listings (PLPs and SERPs) with facets, pagination and sorting options.

  • Attribution and recording of certain events when a user interacts with a Nosto module, for example:

    • Impressions: Products returned from a search query

    • Clicks/Showing interest in a product by clicking "view more" on a PLP or SERP, from the search overlay or from a product recommendations carousel:

      • Can be a redirect to a PDP or

      • When a popup/modal (quick view) is triggered

  • A client that defines merchandising rules, fine-tunes the search functionality, creates recommendation campaigns, A/B tests etc. and does not need to interact with your code.

  • Order Data Sync: How do you send Nosto the placed orders?

  • Custom Platform/Use Case: Product Data Sync GraphQL | REST
  • Custom Platform/Use Case: Order Data Sync GraphQL

  • Components of a stable Nosto Implementation

    Resources to evaluate and choose the Implementation Methods

    Nosto Module Types and Frontend Implementation Methods

    Project Plan Checklist

    Headless Builds, SPAs (Single Page Applications) and Custom Platforms

    Comparison Tables for Nosto Modules and Implementation Methods

    plugins for the most common eCommerce platforms
    Implementation Methods
    React Component Library
    Built-in support in the Nosto Magento plugin
    Product Recommendations, Dynamic Bundles and Onsite Content Personalization
    Search and Category Merchandising
    Big Picture of a Nosto implementation, details in the next section
    https://docs.nosto.com

    Use more than 100 facet values

    Get facet ID from the dashboard

    Override facet settings

    SEO

    the facet configuration
    SEO recommendations
    structured data
    API integration
    Search API FAQ
    Initialization
    Git for version control
  • A Nosto account with Search and Categories enabled

  • Basic familiarity with Javascript/Typescript development

  • First, clone the Search Templates Starter repository to your local machine. You can do this using the command line or via the GitHub UI:

    Option 1: GitHub UI (Recommended)

    1. Go to Search Templates Starter on GitHub

    2. Click the Use this template button to create your own repository, or click Code and select Download ZIP or Open with GitHub Desktop

    3. If using the template feature, clone your new repository; if downloading, extract the ZIP file

    Option 2: Command Line

    Install the required npm packages:

    This will install all necessary dependencies for the project.

    For local development, the Search Templates Starter requires your Nosto merchant ID to connect to your search data. This is not required for production builds. You can provide this in two ways:

    Option 1: Environment File (Recommended) Create a .env file in the root of the project:

    For Shopify merchants, you may also want to configure your store URL:

    Option 2: Environment Variable

    Finding your Merchant ID: You can find your merchant ID in the Nosto Admin UI under Account Settings, or it's typically in the format platform-storeid (e.g., shopify-12345678).

    The starter includes a configuration file at src/config.ts where you can customize:

    • CSS selectors for integration with your site — these define which DOM elements the search components are injected into via React Portals. They should match elements that exist on your store's search results page, category pages, and wherever your autocomplete input lives. If the selectors don't match anything on the page, the components won't appear.

    • Search behavior and settings

    • Component rendering options

    • Customized queries for SERP, category pages, and autocomplete

    Before you can push templates to your store, authenticate with the Nosto CLI:

    This opens a browser window. If you already have a valid Nosto session, you are logged in automatically for the rest of the working day. Once authenticated, configure the CLI for your merchant:

    This creates a .nosto.json configuration file in your project root. See Nosto CLI for full details on the CLI workflow.

    There are two ways to develop with Search Templates Starter. Choose based on what you need at a given moment — most developers use both during a project.

    npm run dev — Vite dev server

    Starts a local Vite dev server at http://localhost:8000. The page is a small mock store that mounts your components with your real Nosto data, but without your shop's styles or layout. Changes are reflected instantly in the browser via hot reload. This is the fastest way to work on component logic and appearance.

    nosto st dev — watch mode on your real store

    Watches your files, rebuilds on save, and pushes the artifacts to Nosto's infrastructure. Your changes become available in preview mode on your actual store — visit it with ?nostodebug=true and enable Preview in the debug toolbar, then refresh the page to see the update. This shows the templates exactly as they will appear when deployed, injected into your real store's pages.

    Note: nosto st dev pushes to a shared S3 bucket tied to the merchant account. If multiple developers run it simultaneously against the same merchant, they will overwrite each other's uploads. Coordinate with your team to avoid conflicts — or use npm run dev for local iteration and only switch to nosto st dev when you need to verify on the real store.

    Use npm run dev when iterating quickly on components. Use nosto st dev when you want to verify the result in context — with your real theme, real CSS, and real page structure.

    Search Templates Starter may operate in three modes: Injected, Native and Mocked. For a typical store setup, you will most likely be using the Injected mode, as it is designed to integrate with any store page. Native mode is useful if you want to develop your store from the ground up using Search Templates Starter, and Mocked is primarily used for development and Storybook.

    Injected Mode (Default)

    In this mode, the components are rendered into the page using React Portals, targeting the elements you define with CSS selectors in src/config.ts. Note that without correct selectors, the components will not appear in the page at all. After the injection step, the rest of the application behaves nearly the same as it would in native mode.

    Entry point: src/entries/injected.tsx

    Configure CSS selectors in src/config.ts to match your store's elements. To test the injected result on your actual store, use nosto st dev instead of the Vite dev server.

    Native Mode

    In this mode, the Starter behaves like a standard React/Preact app but still uses React Portals with a single injection point (#app). It creates the component tree and renders both search and results components together based on the page type (search results or category page). This is useful for:

    • Developing your store from the ground up using Search Templates Starter

    • Testing search functionality without existing page constraints

    • Rapid prototyping of new features

    Entry point: src/entries/native.tsx

    Mocked Mode

    Used automatically in Storybook and testing environments where components render with mock data for consistent development and testing.

    Storybook is a powerful tool for developing, testing, and documenting UI components in isolation. It's a workbench for your components, allowing you to work on them without needing to run the entire application.

    To start Storybook, run:

    This opens Storybook in your browser, where you can explore the component library.

    Why Use Storybook?

    • Focused Development: Concentrate on one component at a time.

    • Visual Testing: See how components look with different properties and states.

    • Rapid Prototyping: Quickly build and iterate on new components.

    • Component Library: Storybook acts as living documentation for your UI.

    No merchant ID is required for Storybook, as all components are displayed with mock data.

    AI and LLM Assistant Usage

    Storybook is also a key tool for AI and LLM assistants. It allows automated agents to:

    • Visually test components they create or modify.

    • Understand the available components and their props.

    • Work on UI tasks in a simplified, sandboxed environment.

    Here are the key commands for your development workflow:

    Testing

    Code Quality

    Building

    Once you have the development environment running, you can:

    • Use the Nosto CLI - See Nosto CLI for deployment workflows

    • Leverage AI assistance - See LLM Examples for development productivity tips

    Port already in use: If port 8000 is already in use, Vite will automatically try the next available port. You can also specify a custom port:

    Missing merchant ID: Ensure your VITE_MERCHANT_ID environment variable is set correctly. The application cannot connect to Nosto's search API without it.

    Build errors: Run npm run typecheck to identify TypeScript errors that might be causing build issues.

    Download from nodejs.org

    Installation

    1. Clone the repository

    2. Install dependencies

    Configuration

    Environment Setup

    Development Configuration

    Connecting to Nosto CLI

    Local Development

    Development approaches

    Understanding Modes

    Development Tools

    Storybook for Component Development

    Essential Development Commands

    Next Steps

    Troubleshooting

    Common Issues

    Search Templates built with the web extension are now referred to as Legacy Search Templates. Their counterparts are Modern Search Templates, built with Search Templates Starter.

    Nosto CLI fully supports both modes for local development.

    Nosto CLI aims to be as user-friendly as command line tools get. You should be able to get up and running by utilizing the built-in nosto help and nosto setup commands, but a quick-start guide is also provided here.

    If your template is based on the Search Templates Starter project, the Nosto CLI is already included as a dependency. In this case, you can run it directly using npx:

    Note: All the command examples in this article will omit npx, but you will need to add it every time unless you opt for a global install.

    For legacy templates or if you prefer to avoid npx, you may install it globally:

    The Nosto CLI supports two authentication methods - your Nosto user account or an API key.

    User Account Authentication

    This opens a browser window for secure authentication. If you are already logged in to Nosto, the authentication will succeed immediately. After logging in, your credentials are stored in your system's home folder for 8 hours. This method works across all merchant accounts you have access to.

    If your account has 2FA enabled, Nosto CLI login does not prompt for a verification code. You may enter the code manually in the browser before running nosto login.

    API Key Authentication

    Alternatively, you can use a private Search API key in your project configuration. Public API keys are not supported as the CLI requires read-write access. You can provide your API key as part of the configuration described below. The API keys are scoped to a single merchant, but they never expire.

    For each merchant account you're working with, create a new folder and set it as the current working directory.

    Create a .nosto.json configuration file in your project root:

    This creates a minimal configuration file. You can also create it manually:

    Required Configuration:

    • merchant - Your Nosto merchant ID (e.g., shopify-12345678)

    Optional Configuration:

    • apiUrl - API endpoint, defaults to production

      • Production URL: https://api.nosto.com

      • Staging URL: https://api.staging.nosto.com

      • Nosto internal development URL: https://my.dev.nos.to/api

    • apiKey - Private API key for authentication (if not using user login)

    Note: Refer to nosto setup for a full list of configuration options available in your version.

    Environment Variables: You can also use environment variables. If provided, they take precedence over the config file:

    • NOSTO_MERCHANT

    • NOSTO_API_URL

    • NOSTO_API_KEY

    Once configured, your development workflow typically looks like:

    To see your changes on your live store:

    1. Enable Debug Toolbar: Add ?nostodebug=true to your store URL

    2. Enable Preview: Toggle the "Preview" button in the debug toolbar

    3. See Changes: Refresh your page to see updates as you save files locally

    Status Check

    Shows the current status of your templates and configuration.

    Pull Remote State

    Fetches the remote state from the Nosto cloud storage locally. Primarily intended for merchants that do not rely on git for version control.

    Development Mode

    Watches files for changes and automatically uploads builds for preview. Essential for active development. Note that this does not upload the source code to speed up the development, only the minimal required set of files to see your changes live. After finishing your development for the day, you may want to run nosto st push as well.

    Push Sources (Upload)

    Builds and uploads the current state of the project to Nosto cloud storage. This includes both built artifacts and source files.

    Note: Refer to nosto --help and nosto st --help for more information about the commands available in your version.

    At the moment, Nosto CLI does not support production deployments. You may still use the Admin UI to create deployments as usual.

    The CLI tool is intended to be used in combination with Git. We recommend you create a git repository per merchant you develop for, and use that as the source of truth for your development. Then, you can use Nosto CLI to build and upload the sources and build artifacts to Nosto's cloud storage, making them immediately available on your store.

    In addition, Nosto CLI automatically takes the contents of your .gitignore file into account when deciding which files should be uploaded to the cloud, respecting the patterns you expect it to ignore.

    Authentication Expired:

    Re-authenticate if you see permission errors.

    Wrong Merchant ID: Check your .nosto.json file or NOSTO_MERCHANT environment variable.

    Upload Failures:

    • Ensure you have internet connectivity

    • Verify your API credentials are valid

    • Check that the merchant ID is correct

    Preview Not Showing:

    • Confirm debug toolbar is enabled (?nostodebug=true)

    • Ensure preview mode is toggled on

    • Try refreshing the page

    • CLI Help: Run nosto --help for command information

    • GitHub Issues: Report bugs at github.com/Nosto/nosto-cli

    • Nosto Support: Contact support through your Nosto Admin UI

    • GitHub Repository: https://github.com/Nosto/nosto-cli

    • NPM Package: https://www.npmjs.com/package/@nosto/nosto-cli

    • Nosto Documentation: https://docs.nosto.com/

    Why use the Nosto CLI?

    Legacy Search Templates

    Nosto CLI
    now available on NPM
    open source code on GitHub

    Installation and Setup

    Installation Options

    Authentication

    Project Configuration

    Development Workflow

    Preview Mode Setup

    Development Commands

    Production Deployments

    Nosto CLI and Git

    Troubleshooting

    Common Issues

    Getting Help

    Links and Resources

    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
                    }
                ]
            },
        }
    })
    {&quot;keyword&quot;:&quot;year's eve&quot;,&quot;_highlight&quot;:{&quot;keyword&quot;:&quot;<strong>year</strong>'s eve&quot;}}
    const { keywords } = response.data.search
    const contentToRender = keywords.map(keyword => 
        `
        <div data-ns-hit="${JSON.stringify(keyword)}" ....>
            ....
            ....
        </div>
        `
    )
    {
        "keyword": "midi dresses",
        "_highlight": { "keyword": "midi <strong>dress</strong>es" }
    }
    const { hits } = response.data.search.products
    const contentToRender = hits.map(({ productId, url }) => 
        `
        <div data-ns-hit="${JSON.stringify({ productId, url })}" ....>
            ....
            ....
        </div>
        `
    )
    {
        "productId": 123456,
        "url": "https://example.com/products/example-product-handle"
    }
    import { fromMustacheTemplate } from '@nosto/autocomplete/mustache'
    
    fromMustacheTemplate(template, {
        helpers: {
            toJson: function () {
                return JSON.stringify(this)
            },
        },
    })
    git clone https://github.com/nosto/search-templates-starter.git
    cd search-templates-starter
    npm ci
    VITE_MERCHANT_ID=your-merchant-id
    VITE_MERCHANT_ID=your-merchant-id
    VITE_SHOPIFY_STORE_URL=https://your-store.myshopify.com
    VITE_MERCHANT_ID=your-merchant-id npm run dev
    nosto login
    nosto setup -m your-merchant-id
    npm run dev
    nosto st dev
    npm run dev
    npm run dev:native
    npm run storybook
    # Run unit and integration tests
    npm run test
    
    # Run tests in watch mode during development
    npm run test:watch
    
    # Run end-to-end tests with Playwright
    npm run test:e2e
    # Check for linting errors and style issues
    npm run lint
    
    # Automatically fix linting issues where possible
    npm run lint:fix
    
    # Check for TypeScript compilation errors
    npm run typecheck
    # Create a production build
    npm run build
    
    # Preview the production build locally
    npm run preview
    npm run dev -- --port 3000
    npx nosto --help
    npm install -g @nosto/nosto-cli
    nosto login
    nosto setup -m YOUR_MERCHANT_ID
    {
      "merchant": "your-merchant-id"
    }
    # Ensure you're logged in
    nosto login
    
    # Start development mode with auto-upload
    nosto st dev
    
    # Open your store and enable Nosto Debug Toolbar preview mode
    # Your changes will appear automatically as you save files and reload the page
    nosto status
    nosto st pull
    nosto st dev
    nosto st push
    nosto login

    inputSelector

    Yes

    N/A

    Input element to attach the autocomplete to

    dropdownSelector

    Yes

    N/A

    Dropdown element to attach the autocomplete to (empty container's selector should be provided)

    render

    Yes

    import { useEffect } from "react"
    import {
        autocomplete,
        search,
        priceDecorator,
        Autocomplete,
    } from "@nosto/autocomplete/react"
    import { createRoot } from "react-dom/client"
    import "@nosto/autocomplete/styles.css"
    
    let reactRoot = null
    
    export function Search() {
        useEffect(() => {
            autocomplete({
                fetch: {
                    products: {
                        fields: [
                            "name",
                            "url",
                            "imageUrl",
                            "price",
                            "listPrice",
                            "brand",
                        ],
                        size: 5,
                    },
                    keywords: {
                        size: 5,
                        fields: ["keyword", "_highlight.keyword"],
                        highlight: {
                            preTag: `<strong>`,
                            postTag: "</strong>",
                        },
                    },
                },
                inputSelector: "#search",
                dropdownSelector: "#search-results",
                hitDecorators: [
                    // adds priceText & listPrice fields based on Nosto currency formatting rules
                    priceDecorator({ defaultCurrency: "USD" })
                ],
                render: function (container, state) {
                    if (!reactRoot) {
                        reactRoot = createRoot(container)
                    }
    
                    reactRoot.render(<Autocomplete {...state} />)
                },
                submit: async (query, config, options) => {
                    if (query.length >= config.minQueryLength) {
                        const response = await search(
                            {
                                query,
                            },
                            {
                                redirect: true,
                                track: config.nostoAnalytics ? "serp" : undefined,
                                ...options
                            }
                        )
                        // Do something with response. For example, update Search Engine Results Page products state.
                    }
                },
            })
        }, [])
    
        return (
            <form id="search-form">
                <input type="text" id="search" placeholder="search" />
                <button type="submit" id="search-button">
                    Search
                </button>
                <div id="search-results" className="ns-autocomplete"></div>
            </form>
        )
    }
    <div class="nosto_product" style="display: none;" translate="no">
      ...
      ...
      ...
      <!-- Variation ID for the primary currency --> 
      <span class="variation_id">USD</span>
    </div>
    nostojs(api => {
      api.setTaggingProvider("variation", "USD")
    })
    <div class="nosto_variation" style="display: none;">USD</div>
    {
      "rates":{
        "GBP":{
          "rate":0.77,
          "price_currency_code":"GBP"
        },
        "EUR":{
          "rate":0.91,
          "price_currency_code":"EUR"
        }
      },
      "valid_until":"2015-02-27T12:00:00Z"
    }

    Do child-products (SKUs) support multi-currency?

    What about the prices in the cart and the order tagging?

    Specifying the active currency

    Sending the exchange-rates

    Enabling multi-currency from the admin

    Integration with search/category merchandising (universal)

    Reviewing your changes

    See also

    our exchange-rates API
    products object
    simplified version of this process
    official API documentation

    Extending tagging with SKUs

    Nosto supports individual product SKUs under parent products. If you have not set up Product tagging you should start there and extend the tagging if needed.

    The SKU attributes should be listed on the last row of the nosto_product block that you have already implemented on the product pages.

    What is a SKU?

    Many e-commerce stores have a parent product with individual child products. The parent product is usually something along the lines of "Ski Jacket" whereas the SKUs would then be "Ski Jacket, Blue, Small", "Ski Jacket, Red, Medium". If your store uses SKUs you should add the following attributes to extend your product tagging.

    nostojs(api => {
      api.setTaggingProvider("products", [{
        ...
        skus: [
          {
            id: "1",
            name: "S-Orange",
            price: 1269.00,
            list_price: 1299.00,
            url: "http://www.example.com/product/CANOE123#/1-size-s/13-color-orange",
            image_url: "http://www.example.com/product/images/CANOE123-1.jpg",
            availability: "InStock",
            custom_fields: {
              size: "S",
              color: "Orange"
            }
          }
          {
            id: "2",
            name: "S-Blue",
            price: 1269.00,
            list_price: 1299.00,
            url: "http://www.example.com/product/CANOE123#/1-size-s/14-color-blue",
            image_url: "http://www.example.com/product/images/CANOE123-2.jpg",
            availability: "InStock",
            custom_fields: {
              size: "S",
              color: "Blue"
            }
          }
        ]
      }])
    })

    or via DOM tagging

    <div class="nosto_product" style="display:none" translate="no">
      ...
      ...
      ...
      <span class="nosto_sku">
        <span class="id">1</span>
          <span class="name">S-Orange</span>
          <span class="price">1269.00</span>
          <span class="list_price">1299.00</span>
          <span class="url">http://www.example.com/product/CANOE123#/1-size-s/13-color-orange</span>
          <span class="image_url">http://www.example.com/product/images/CANOE123-1.jpg</span>
          <span class="availability">InStock</span>
          <span class="custom_fields">
            <span class="size">S</span>
            <span class="color">Orange</span>
          </span>
      </span>
    
      <span class="nosto_sku">
        <span class="id">2</span>
          <span class="name">S-Blue</span>
          <span class="price">1269.00</span>
          <span class="list_price">1299.00</span>
          <span class="url">http://www.example.com/product/CANOE123#/1-size-s/14-color-blue</span>
          <span class="image_url">http://www.example.com/product/images/CANOE123-2.jpg</span>
          <span class="availability">InStock</span>
          <span class="custom_fields">
            <span class="size">S</span>
            <span class="color">Blue</span>
          </span>
      </span>
    
    </div>

    Note: The attribute custom_fields can contain whatever unique information for individual SKUs that you can consider helpful. Frequently used attributes would be size, color, material.

    Extending the cart tagging with SKU metadata

    When tagging the cart contents as outlined here, you can also tag information of the actual SKU that was added to cart.

    or via DOM tagging

    Notice the extra <span class="sku_id"> attribute.

    Extending the order tagging with SKU metadata

    When tagging the order contents as outlined here, you can also tag information of the actual SKU that was added to cart.

    or via DOM tagging

    Notice the extra <span class="sku_id"> attribute inside each of the purchased_items.

    Once included you can review if the SKUs are picked up by using the . If you can see individual SKUs being picked up below the original product details then this is correctly set up.

    You can further verify that products are being indexed to the catalog under the Nosto admin by navigating to Tools → Products ()

    No, Nosto does no recommend individual SKUs. While this is something on our roadmap, at the moment, you do not need to send any events when an SKU is selected.

    For example, assume you had a product page selling a shoe. In this case, the product tagging would always point to the id of the shoe. No events should be dispatched when the customer selects a particular size such as S, M, L.

    Implement Autocomplete

    Autocomplete provides keyword suggestions to assist users in completing their queries, supplemented by a selection of the most relevant products with the ability to see all products on the search results page. The feature also supports category and popular search suggestions. Please contact Nosto Support to have them enabled for your account.

    Example Autocomplete

    Check out autocomplete's look & feel guidelines.

    When integrating autocomplete you have the option to directly access the API, or you can use our existing that provides most of the required functionality out of the box.

    Requirements

    Some autocomplete features are available conditionally.

    • Keyword suggestions require searchable fields to be marked for autocomplete.

    • Popular searches require a function Nosto tracking integration for searches.

    • Category suggestions depend on available categories (including URLs) being sent to Nosto.

      • The Shopify integration sends categories to Nosto out-of-the-box, with no extra work required.

      • The Shopware 6 plugin automatically sends categories to Nosto without requiring extra work from version 5.1.4.

    To retrieve results for an empty query, you must explicitly set emptyQueryMatchesAll: true in your request. By default, emptyQueryMatchesAll is false and the API does not return any results when the query is empty. Setting it to true enables the API to return default suggestions. This behavior applies to all suggestion types — keywords, categories, and popular searches.

    For more details please check the

    The query below returns the most popular search terms for the specified account.

    API can return highlights indicating which parts of a keyword match the search query. This HTML can be used to render and emphasize the matching sections during display.

    Redirects are configured in the search dashboard and can be used to forward users to specific pages depending on what they type into the search field. For example, users searching for "shipping" could be directed to .

    To analyze user behavior you need to implement tracking. This can be achieved in two different ways, depending on the integration environment:

    • (recommended - more convenient, but requires a JavaScript environment).

    • (works anywhere).

    Each autocomplete product click should be tracked as a search page virtual view to ensure that the Google Analytics search feature displays the correct conversion rate. For example, if you click any product when the typed query is phone it should track a virtual page view with the URL /search?q=phone (adjust the search path and query parameter to match your search page).

    Since a product link click would redirect to a new page, to ensure that Google Analytics has time to send the tracking request, it's recommended to save the search query to local storage and track it on page load.

    Implement Search & Categories

    Nosto Search uses product and user behavior data from the Nosto Platform, so if you are implementing Nosto Search, first of all, you need to implement Nosto Platform to your website.

    Your search engine will be ready after the Nosto representative enables the Search module for your account. Then you will need to integrate Nosto Search to the website.

    Implementation methods

    Search Templates Starter

    For developers who prefer a modern, local development workflow, the Search Templates Starter provides a complete Preact-based project. This approach offers full source code control with Git, a local development environment with hot reloading, and a component library with pre-built, customizable search components. It's ideal for teams that want maximum flexibility and integration with their existing development practices.

    Using Search Templates Starter

    Search Templates

    By using a pre-built template that can be customized to fully match a website's design using built-in code editor directly in https://my.nosto.com/. In the code editor, you can fully customize the pre-built template's JavaScript, HTML, and CSS code. Changes to the template can be implemented either by client’s developers or Nosto team. Frontend integration uses Preact and JSX templates and renders search results page dynamically in website’s frontend, so no additional integration is needed to the backend. When using Nosto services, no development is needed from the client.

    Using Search Templates

    API

    API integration - a robust Search GraphQL API allows to implement Nosto Search into any website or app and gives complete flexibility for developers to build frontend and backend features.

    For frontend integrations you can also use our JavaScript library. This library wraps the Search GraphQL API and provides its functionality in a programmatic way.

    Search Templates Starter
    Search Templates
    API
    JavaScript Library

    With appropriate care, it's possible to mix and match different integration types. The most commonly observed combinations are:

    • Use JavaScript library for tracking and API for retrieving search/category results.

    • Use search templates (starter) for autocomplete, and API for search/category results.

    When choosing such an approach, it's important that all components involved use the same session parameters throughout to ensure that all interactions are connected to the same visitor. This is easiest to achieve by managing the session in the frontend using the JavaScript library (included in search templates (starter)), and passing session parameters (including active segments and personalization affinities) to other components connected to Nosto (e.g., a backend interacting with Nosto search via API), so these can include the session parameters in API requests.

    To avoid pitfalls and ensure correct attribution of sales to search, tracking search impressions and clicks should be done with the same integration types.

    Please refer to documentation for retrieving session parameters via or for more details.

    Feature
    Search Templates
    Search Templates Starter

    If you are a developer who prefers a modern, local development workflow with full version control (Git), we strongly recommend using the Search Templates Starter. It offers the most flexibility and integrates seamlessly with professional development practices.

    If you are looking for a faster, more straightforward setup and are comfortable using an in-browser code editor, Search Templates are a great alternative.

    For use cases that require deep backend integration, or if you're building for a non-web platform (e.g., native mobile apps), the API or JavaScript Library integrations provide the necessary control and access to the raw search data.

    Using the API

    In order to use the GraphQL endpoints, you'll need to authenticate yourself. You will need a Apps token to access this endpoint. Only a subset of the endpoints can be accessed with a public token. This makes it possible to access functionality like querying product recommendations in an environment where it's not possible to protect the token, for example in a web browser. Each publicly accessible endpoint is denoted in the embedded documentation inside the playground.

    Note: Nosto does not rate-limit the API usage but follows a fair-use policy. Nosto reserves the right to revoke API access for any abusive API usage patterns.

    Sending JSON GraphQL queries

    You can send your GraphQL requests as JSON to our API and have it correctly interpolate variables passed into it. To do so, set the Content-Type header to application/json.

    Authentication
    Token
    Method
    Endpoint

    If you want to send raw GraphQL queries to the API, you can still do so but you must set the Content-Type header to application/graphql.

    Note: If you do not set the correct Content-Type header, the request will be interpreted as JSON and will fail.

    Authentication
    Token
    Method
    Endpoint

    You can use the browser's fetch API to request data from GraphQL. You will need to authenticate yourself and set the appropriate content-type headers.

    You can send your GraphQL requests as JSON to our API and have it correctly interpolate variables passed into it. To do so, set the Content-Type header to application/json.

    If you want to send raw GraphQL queries to the API, you can still do so but you must set the Content-Type header to application/graphql.

    **NOTE:**If you do not set the correct Content-Type header, the request will be interpreted as JSON and will fail.

    Check your setup

    Step by step guide for checking that our client script is tracking events on your site.

    Session view in the debug toolbar

    1. Navigate to your site in incognito mode.

    2. Load the debug toolbar by appending nostodebug=true to your store's URL e.g. https://example.com?nostodebug=true

    3. Click the login link and navigate to the session view.

    Navigate to a category page and verify that the View Category event is tracked with the following details:

    • Target matches the viewed category.

    If the category doesn't appear in the session, these pages might help

    Navigate to a product page and verify that the View Product event is tracked with the following details:

    • Target matches the viewed product ID.

    If the product doesn't appear in the session, these pages might help:

    Click on a product recommendation and verify that the View Product event is tracked with the following details:

    • Target matches the viewed product ID.

    • Ref matches the previously viewed recommendation slot ID.

    If the product id and recommendation slot id don't appear in the session, these pages might help:

    Add the product to cart and verify that the cart’s contents are correct. Ensure that the product details in the cart match the product that was previously viewed.

    • Product ID

    • Price & Currency

    • Quantity

    If the cart doesn't correctly show in the session view, these pages might help:

    Purchase the product and verify that the Buy Product event is tracked with the following details:

    • Ref matches the recommendation slot id

    • Target matches the product id

    Wait 30 minutes for the session to expire and then verify that the order shows up in the orders page of the Nosto admin UI.

    If the order doesn't correctly show in the session view, these pages might help:

    We’ve integrated with the to be able to show some performance measurements of our client script. The supported measurements are:

    • nosto.get_dynamic_placements Parse the DOM and find placements on the page. Performed before a recommendations request is sent.

    • nosto.load_recommendations Send the recommendations request and handle the recommendation result. This encompasses measuring the recommendations request and nosto.inject_campaigns.

    Chrome's tool can be opened by doing the following:

    1. Right click and select Inspect.

    2. Navigate to the Lighthouse tab.

    3. Ensure the Performance category is enabled.

    Once the report is generated, you can expand the “User Timing marks and measures” section under the Diagnostics section to see some basic information like how long after the page started to load the measurements start and how long each measurement took.

    You can scroll back up and click the View Original Trace button to view a timeline of the measurements. You may need to expand the Timings section to view the client script’s recorded measurements. You can zoom in an out of this graph and swipe side to side to get a better view of things.

    N/A

    Function to render the dropdown

    fetch

    Yes

    N/A

    Function to fetch the search state

    submit

    No

    Search API request

    Function to submit the search

    minQueryLength

    No

    2

    Minimum length of the query before searching (applied on typing in autocomplete and used in default submit implementation)

    historyEnabled

    No

    true

    Enables search history component

    historySize

    No

    5

    Max number of history items to show

    nostoAnalytics

    No

    true

    Enable Nosto Analytics

    googleAnalytics

    No

    { serpPath: "search", queryParamName: "query", enabled: true }

    Google Analytics configuration. Set to false to disable

    hitDecorators

    No

    N/A

    Decorate each search hit before rendering

    nosto.inject_campaigns Inject both dynamic and static campaigns into the DOM. This encompasses both nosto.inject_static_campaigns and nosto.inject_dynamic_campaigns.
  • nosto.inject_static_campaigns Inject static campaigns into the DOM.

  • nosto.inject_dynamic_campaigns Inject dynamic campaigns into the DOM.

  • nosto.evaluate_js Extract and evaluate embedded JavaScript from recommendation templates.

  • Click Generate Report

    View Category

    View Product

    View a Recommended Product

    Cart

    Order

    If it’s not possible to perform a test order, the Nosto admin UI's orders page can be used to review that orders link to known products in the catalog and that some items have click attribution towards the visible recommendations.

    Measuring Performance

    Lighthouse

    Tagging: category
    Session API: category
    Tagging: product
    JS API: product
    Session API: product
    JS API: Attribution for recommended Products
    JS API: Sending Add to Cart-Events
    Session API: handling attribution
    Tagging: cart
    JS API: cart
    Session API: cart
    Tagging: order
    Session API: order
    User Timing API
    lighthouse
    Click the login button
    After logging in, the session tab should be visible on the left-hand side
    Viewing the "Bags" category
    Viewing product 784735895612
    Viewing product 789528674364 from recommendation slot productpage-nosto-14-high-upsell
    Buy product 789528674364 from recommendation slot productpage-nosto-14-high-upsell.

    Expected time to launch live

    2-4 weeks*

    1-3 weeks*

    4-8 weeks

    3-6 weeks

    Headless compatible

    Yes

    Yes

    Yes

    Yes

    Fully customizable frontend

    Yes

    Yes

    Yes

    Yes

    Customized and managed only in Nosto dashboard

    No

    Yes

    No

    No

    Suitable for complex use cases

    Yes

    Sometimes

    Yes

    Yes

    Merchandising rules applied automatically**

    Yes

    Yes

    Yes

    Yes

    Analytics

    Yes

    Yes

    Yes

    Yes

    Segmentation

    Yes

    Yes

    Yes

    Yes

    Individual personalization (affinities)

    Yes

    Yes

    Yes

    Yes

    A/B testing

    Yes

    Yes

    Yes

    Yes

    SPA suitable

    Yes

    Limited***

    Yes

    Yes

    Workflow

    Edit and preview directly in the browser

    Local development with nosto-cli for uploads

    Best For

    Quick setup and users comfortable with an online editor

    Developers wanting a modern, local workflow

    Can be implemented by Nosto team

    Yes

    Yes

    No

    Development Environment

    In-browser code editor in the Nosto Admin UI

    Local development with your preferred IDE

    Version Control

    Managed within the Nosto platform

    JavaScript Library

    Compare implementations

    * This estimation is based on the merchant's team building the templates. When Nosto's frontend team builds templates via the Code Editor, this can take longer due to overall bandwidth from the team.

    ** Matching merchandising rules are applied automatically based on requested search queries, categories, and segments, without the need to request them in API requests.

    *** Using search templates with SPAs comes with challenges related to routing and dynamic content injection that tend to be solvable, but are more technically involved. We highly recommend using the JavaScript library or API instead.

    Mixing integration types ("hybrid integration")

    Search Templates vs. Search Templates Starter

    Using the API
    Using the JavaScript Library
    JavaScript library
    API

    No

    Full source code control with Git

    For other platforms and custom integrations, send categories to Nosto via GraphQL API.

    API Requests

    Example

    Query

    Response

    Empty query

    Example query for default popular search suggestions

    Highlight

    Redirects

    API only returns redirect URL, the actual browser redirect must be implemented by the merchant on keyword selection

    Analytics

    Nosto Analytics

    Google Analytics

    Search request schema
    https://example.com/shipping.html
    JavaScript library
    GraphQL API
    Autocomplete JavaScript library

    Basic

    API_APPS

    POST

    https://api.nosto.com/v1/graphql

    Basic

    API_APPS

    POST

    https://api.nosto.com/v1/graphql

    Sending raw GraphQL queries

    Using Fetch

    Sending JSON GraphQL queries

    Sending raw GraphQL queries

    query {
      search(
        accountId: "YOUR_ACCOUNT_ID"
        query: "green"
        products: { size: 5 },
        keywords: { size: 5 },
        categories: { size: 5 },
        popularSearches: { 
          size: 5,
          emptyQueryMatchesAll: true
        }
      ) {
        products {
          hits {
            productId
            name
          }
          total
        }
        keywords {
          hits {
            keyword
            _redirect
            _highlight {
              keyword
            }
          }
        }
        categories {
          hits {
            name
            fullName
            externalId
            parentExternalId
            url
            urlPath
          }
          total
        }
        popularSearches {
          hits {
            query
            total
          }
          total
        }
        query
      }
    }
    {
      "data": {
        "search": {
          "query": "green",
          "products": {
            "hits": [
              {
                "productId": "1",
                "name": "My product"
              }
            ],
            "total": 1
          },
          "keywords": {
            "hits": [
              {
                "keyword": "green",
                "_redirect": "https://example.com/green.html",
                "_highlight": {
                  "keyword": "<em>green</em>"
                }
              },
              {
                "keyword": "green energy",
                "_redirect": null,
                "_highlight": {
                  "keyword": "<em>green</em> energy"
                }
              }
            ]
          },
          "categories": {
            "hits": [
              {
                "name": "Home and Garden > Plants > Green Plants",
                "fullName": "Home and Garden > Plants > Green Plants",
                "externalId": "1234",
                "parentExternalId": "5678",
                "url": "https://www.example.com/category/home-and-garden",
                "urlPath": "home-and-garden"
              },
              {
                "name": "Fashion > Jackets > Green Jackets",
                "fullName": "Fashion > Jackets > Green Jackets",
                "externalId": "4321",
                "parentExternalId": "8765",
                "url": "https://www.example.com/category/fashion",
                "urlPath": "fashion"
              }
            ],
            "total": 86
          },
          "popularSearches": {
            "hits": [
              {
                "query": "green pants",
                "total": 3024
              },
              {
                "query": "green shirt",
                "total": 480
              }
            ],
            "total": 2
          }
        }
      }
    }
    query {
        search(
            accountId: "YOUR_ACCOUNT_ID"
            query: ""
            popularSearches: {size: 5, emptyQueryMatchesAll: true}
        ) {
            popularSearches {
                hits {
                    query
                    total
                }
                total
            }
            query
        }
    } 
    curl -0 -v -X POST https://api.nosto.com/v1/graphql \
    -u ":<token>" \
    -H 'Content-Type: application/json' \
    -d @- << EOF
    {
      "query": "query { recos (preview: false, image: VERSION_7_200_200) { toplist(hours: 168, sort: BUYS, params: { minProducts: 1 maxProducts: 10 } ) { primary { name productId } } } }"
    }
    EOF
    curl -0 -v -X POST https://api.nosto.com/v1/graphql \
    -u ":<token>" \
    -H 'Content-Type: application/graphql' \
    -d @- << EOF
    query {
      recos (preview: false, image: VERSION_7_200_200) {
        toplist(hours: 168, sort: BUYS, params: {
          minProducts: 1
          maxProducts: 10
        }) {
          primary {
            name
            productId
          }
        }
      }
    }
    EOF
    const body = JSON.stringify({
      query: `
      query {
        order(id: "5192009") {
          number
          reference
          items {
            productId
            skuId
            unitPrice
            priceCurrencyCode
            quantity
          }
          recos(preview: true, image: VERSION_1_170_170) {
            related(params: {
              minProducts: 1,
              maxProducts: 5
            }) {
              primary {
                productId
                name
                price
              }
            }
          }
        }
      }
    `});
    
    fetch('https://api.nosto.com/v1/graphql', {
      method: 'POST',
      headers: new Headers({
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + btoa(":" + "<token>")
      }),
      mode: 'cors',
      body
    })
      .then((response) => response.json())
      .then((json) => {
        console.log(json);
      });
    const body = `
    query {
      order(id: "5192009") {
        number
        reference
        items {
          productId
          skuId
          unitPrice
          priceCurrencyCode
          quantity
        }
        recos(preview: true, image: VERSION_1_170_170) {
          related(params: {
            minProducts: 1,
            maxProducts: 5
          }) {
            primary {
              productId
              name
              price
            }
          }
        }
      }
    }
    `;
    
    fetch('https://api.nosto.com/v1/graphql', {
      method: 'POST',
      headers: new Headers({
        'Content-Type': 'application/graphql',
        'Authorization': 'Basic ' + btoa(":" + "<token>")
      }),
      mode: 'cors',
      body
    })
      .then((response) => response.json())
      .then((json) => {
        console.log(json);
      });
    nostojs(api => {
      api.setTaggingProvider("cart", {
        [
          {
            product_id: "Canoe123",
            sku_id: "1",
            ...
          },
          {
            product_id: "Canoe123",
            sku_id: "2",
            ...
          },
          {
            product_id: "Canoe245",
            sku_id: "1",
            ...
          }
        ]
      })
    })
    <div class="nosto_cart" style="display:none" translate="no">
    
        <div class="line_item">
            <span class="product_id">Canoe123</span>
            <span class="sku_id">1</span>
            <span class="quantity">1</span>
            <span class="name">Acme Canoe</span>
            <span class="unit_price">999.00</span>
            <span class="price_currency_code">EUR</span>
        </div>
    
        <div class="line_item">
            <span class="product_id">Canoe123</span>
            <span class="sku_id">2</span>
            <span class="quantity">1</span>
            <span class="name">Acme Canoe</span>
            <span class="unit_price">999.00</span>
            <span class="price_currency_code">EUR</span>
        </div>
    
        <div class="line_item">
            <span class="product_id">Canoe245</span>
            <span class="sku_id">1</span>
            <span class="quantity">3</span>
            <span class="name">Acme Large Canoe</span>
            <span class="unit_price">19.00</span>
            <span class="price_currency_code">EUR</span>
        </div>
    
    </div>
    nostojs(api => {
      api.setTaggingProvider("order", {
        info: {
          order_number: "1445",
          email: "john.doe@example.com",
          first_name: "John",
          last_name: "Doe"
        },
        items: [
          {
            product_id: "Canoe123",
            sku_id: "1",
            ...
          },
          {
            product_id: "Canoe245",
            sku_id: "2",
            ...
          }
        ]
      })
    })
    <div class="nosto_purchase_order" style="display:none" translate="no">
        <span class="order_number">1445</span>
    
        <div class="buyer">
            <span class="email">john.doe@example.com</span>
            <span class="first_name">John</span>
            <span class="last_name">Doe</span>
        </div>
    
        <div class="purchased_items">
            <div class="line_item">
                <span class="product_id">Canoe123</span>
                <span class="sku_id">1</span>
                <span class="quantity">1</span>
                <span class="name">Acme Canoe</span>
                <span class="unit_price">999.00</span>
                <span class="price_currency_code">EUR</span>
            </div>
    
            <div class="line_item">
                <span class="product_id">Canoe245</span>
                <span class="sku_id">2</span>
                <span class="quantity">3</span>
                <span class="name">Acme Large Canoe</span>
                <span class="unit_price">19.00</span>
                <span class="price_currency_code">EUR</span>
            </div>
        </div>
    </div>

    To learn more about the api.setTaggingProvider usage, please refer to the official API documentation.

    Validating

    FAQ

    Do I need to view events for when an SKU is viewed?

    Nosto Debug Toolbar
    https://my.nosto.com/admin/$accountID/campaigns/products/list

    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.

    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'
                ],
            },
        }
    })

    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:

    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

    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.

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

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

    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.

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

    Render results

    Once the autocomplete component binds to input via inputSelector and dropdownSelector, it then renders autocomplete provided in a render function. It is called on input focus and change events, and renders a dropdown element with the current search result state:

    • if input is empty and history entries exist, it renders dropdown with history list,

    • if input is not empty and it passes minQueryLength rule, it render dropdown with keywords and products.

    Render can be adjusted to the desired framework. Moreover, the library provides helpers for Mustache/Liquid template languages.

    Examples

    Suppose index.html is

    List of autocomplete initialization examples:

    1. Liquid Example below uses fromLiquidTemplate helper which renders string template. Library provides default autocomplete template via defaultLiquidTemplate and default css for default template:

    The template also can be loaded from a file. The library includes a default template, equivalent to string template in above example:

    1. Mustache Mustache template is rendered similarly as Liquid template in the above example:

    Or from a file:

    1. React/Preact One way to initialize autocomplete in a React app, is to call autocomplete from the useEffect on component mount, using default <Autocomplete /> component and styles:

    The Preact solution does not differ from React a lot:

    Customizing dropdown position

    Autocomplete component

    Voice to text search

    Element selection

    Search submit

    History component

    Analytics

    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>
        )
    }
    <form id="search-form">
        <input type="text" id="search" placeholder="search" />
        <button type="submit" id="search-button">Search</button>
        <div id="search-results" className="ns-autocomplete"></div>
    </form>
    import {
        autocomplete,
        search,
        fromLiquidTemplate,
        defaultLiquidTemplate,
    } from "@nosto/autocomplete/liquid"
    import "@nosto/autocomplete/styles.css"
    
    autocomplete({
        fetch: {
            products: {
                fields: ["name", "url", "imageUrl", "price", "listPrice", "brand"],
                size: 5,
            },
            keywords: {
                size: 5,
                fields: ["keyword", "_highlight.keyword"],
                highlight: {
                    preTag: `<strong>`,
                    postTag: "</strong>",
                },
            },
        },
        inputSelector: "#search",
        dropdownSelector: "#search-results",
        render: fromLiquidTemplate(defaultLiquidTemplate),
        submit: async (query, config, options) => {
            if (query.length >= config.minQueryLength) {
                const response = await search(
                    {
                        query,
                    },
                    {
                        redirect: true,
                        track: config.nostoAnalytics ? "serp" : undefined,
                        ...options
                    }
                )
                // Do something with response. For example, update Search Engine Results Page products state.
            }
        },
    })
    import {
        autocomplete,
        search,
        fromRemoteLiquidTemplate,
    } from "@nosto/autocomplete/liquid"
    import "@nosto/autocomplete/styles.css"
    
    autocomplete({
        // ...
        render: fromRemoteLiquidTemplate(
            `./node_modules/@nosto/autocomplete/dist/liquid/autocomplete.liquid`
        ),
    })
    import {
        autocomplete,
        search,
        fromMustacheTemplate,
        defaultMustacheTemplate,
    } from "@nosto/autocomplete/mustache"
    import "@nosto/autocomplete/styles.css"
    
    autocomplete({
        // ...
        render: fromMustacheTemplate(defaultMustacheTemplate),
    })
    import {
        autocomplete,
        search,
        fromRemoteMustacheTemplate,
    } from "@nosto/autocomplete/mustache"
    import "@nosto/autocomplete/styles.css"
    
    autocomplete({
        // ...
        render: fromRemoteMustacheTemplate(
            `./node_modules/@nosto/autocomplete/dist/mustache/autocomplete.mustache`
        ),
    })
    import { useEffect } from "react"
    import { createRoot } from "react-dom/client"
    import {
        autocomplete,
        search,
        Autocomplete,
    } from "@nosto/autocomplete/react"
    import "@nosto/autocomplete/styles"
    
    let reactRoot = null
    
    export function Search() {
        useEffect(() => {
            autocomplete({
                fetch: {
                    products: {
                        fields: [
                            "name",
                            "url",
                            "imageUrl",
                            "price",
                            "listPrice",
                            "brand",
                        ],
                        size: 5,
                    },
                    keywords: {
                        size: 5,
                        fields: ["keyword", "_highlight.keyword"],
                        highlight: {
                            preTag: `<strong>`,
                            postTag: "</strong>",
                        },
                    },
                },
                inputSelector: "#search",
                dropdownSelector: "#search-results",
                render: function (container, state) {
                    if (!reactRoot) {
                        reactRoot = createRoot(container)
                    }
    
                    reactRoot.render(<Autocomplete {...state} />)
                },
                submit: async (query, config, options) => {
                    if (query.length >= config.minQueryLength) {
                        const response = await search(
                            {
                                query,
                            },
                            {
                                redirect: true,
                                track: config.nostoAnalytics ? "serp" : undefined,
                                ...options
                            }
                        )
                        // Do something with response. For example, update Search Engine Results Page products state.
                    }
                },
            })
        }, [])
    
        return (
            <form id="search-form">
                <input type="text" id="search" placeholder="search" />
                <button type="submit" id="search-button">
                    Search
                </button>
                <div id="search-results" className="ns-autocomplete"></div>
            </form>
        )
    }
    import { render } from "preact/compat"
    import { useEffect } from "preact/hooks"
    import {
        Autocomplete,
        autocomplete,
        search,
    } from "@nosto/autocomplete/preact"
    import "@nosto/autocomplete/styles.css"
    
    export function Search() {
        useEffect(() => {
            autocomplete({
                // ...
                render: function (container, state) {
                    render(<Autocomplete {...state} />, container)
                },
            })
        }, [])
    
        return <form id="search-form">{/* ... */}</form>
    }

    Use the Search & Categories API

    Not all of Nosto's functionality is available for pure GraphQL API integrations. The following features require :

    • Debug toolbar

    Both Search and Categories are built on the same technical foundation and use the same API and product data. For simplicity, we will just refer to Search API in the following documentation.

    Playground and API reference

    Use the to try out search queries and browse API reference.

    It provides:

    1. Search request schema - you can see field types and inspect what fields are needed for a search request.

    2. Search result schema - you can see return field types with descriptions.

    3. Send requests to the search engine and preview the response.

    In the majority of cases, authentication is not a requirement for using search APIs. However in rare case may need to:

    • Access sensitive data- all sensitive data is restricted for public access (e.g. sorting by & returning sales).

    • Return all documents - public access require to specify search query, category ID or category path to avoid returning all documents.

    Note: Keep your API key secret and do not expose it to the frontend!

    HTTP Header
    Value

    Search use different API endpoint than other Nosto queries: https://search.nosto.com/v1/graphql

    All requests require an account ID, which can be found in the top-right corner of the Admin dashboard, under the shop name.

    Replace YOUR_ACCOUNT_ID with your account id retrieved from the Nosto dashboard.

    Replace YOUR_ACCOUNT_ID with your account id retrieved from the Nosto dashboard.

    JS API includes full-featured with tracking support.

    Replace YOUR_ACCOUNT_ID with your account id retrieved from the Nosto dashboard.

    Upon a successful request, the API will return a 200 status code response, which includes the search data:

    API returns a list of errors with explanations. These errors should be logged internally and not displayed to the end user. Instead, display a general message, such as Search is unavailable at the moment, please try again. This approach ensures that no sensitive information is leaked to the end user.

    The API may return some errors even when data is returned. This means that some parts of the response may be missing, but you should still display the available data instead of showing an error to the user. These errors should be logged internally for future reference and troubleshooting.

    For features like personalized results and user segments to function effectively, the search function needs access to the user's session information. Session information can be .

    Alternatively, it's possible to get search session data using the :

    The results of this function should be passed to search query parameter. In case search is called from backend, it should pass this data to backend (e.g. using ).

    It's also possible to save session data to on page load:

    In the search application, you should use variables instead of hardcoded arguments to pass search data. This means that filters, sort, size, and 'from' options should be passed in the 'products' variable. For a full list of available options, please see the .

    Variables should encompass all dynamic query data because it is the most efficient method to pass data, and data is automatically escaped to prevent injection attacks. Avoid generating dynamic queries, as they can lead to security issues if user input is not properly escaped.

    To analyze user behavior you need to implement tracking. This can be achieved in two different ways, depending on the integration environment:

    • (recommended - more convenient, but requires a JavaScript environment).

    • (works anywhere).

    Sku debug toolbar
    Sku debug toolbar
    Sku product catalog
    Sku product catalog

    Replace YOUR_ACCOUNT_ID with your account id retrieved from the Nosto dashboard.

    Authorization

    Bearer SEARCH_KEY

    Authentication

    Authentication Token with API_SEARCH Role is available on dashboard settings page

    Making requests

    When integrating Search you have the option to directly access the API, or you can use our existing Search JavaScript library that provides most of the required functionality out of the box.

    API endpoint

    Account ID

    Request example

    Response example

    Error handling

    Error response

    Partial response

    Session params

    Using variables

    Query

    To optimize search speed and reduce network load, select only the necessary data when performing a search query.

    Variables

    Analytics

    search function
    queried from the session API
    JS API
    sessionParams
    form data
    cookies
    reference
    JavaScript library
    GraphQL API
    Using the JavaScript Library
    Search API Playground
    https://my.nosto.com
    import requests
    
    r = requests.post(
        "https://search.nosto.com/v1/graphql",
        headers={
            "Content-Type": "application/json"
        },
        json={
            "query": """query ($query: String) {
                search (accountId: "YOUR_ACCOUNT_ID", query: $query) {
                    products {
                    hits {
                        name
                    }
                    total
                    }
                }
            }""",
            "variables": {
                "query": "green"
            }
        }
    )
    print(r.json())
    curl -X POST 'https://search.nosto.com/v1/graphql' \
    -H 'Content-Type: application/json' \
    -d @- << EOF
    {
      "query": "query (\$query: String) { search (accountId: \"YOUR_ACCOUNT_ID\", query: \$query) { products { hits { name } total } } }",
      "variables": {
        "query": "green"
      }
    }
    EOF
    fetch('https://search.nosto.com/v1/graphql', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: `query ($query: String) {
          search (accountId: "YOUR_ACCOUNT_ID", query: $query) {
            products {
              hits {
                name
              }
              total
            }
          }
        }`,
        variables: {
          query: "green"
        },
      }),
    })
      .then((res) => res.json())
      .then((result) => console.log(result))
    $ch = curl_init();
    
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 0);
    curl_setopt($ch, CURLOPT_URL, "https://search.nosto.com/v1/graphql");
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: application/json"));
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array(
        "query" => <<<EOT
            query (\$query: String) {
                search (accountId: "YOUR_ACCOUNT_ID", query: \$query) {
                    products {
                    hits {
                        name
                    }
                    total
                    }
                }
            }
        EOT,
        "variables" => array(
            "query" => "green"
        )
    )));
    
    $result = curl_exec($ch);
    var_dump($result);
    {
      "data": {
        "search": {
          "products": {
            "hits": [
              {
                "name": "My Product",
              }
            ],
            "total": 1
          }
        }
      }
    }
    {
      "data": {
        ...
      },
      "errors": [
        {
          "message": "Error explanation"
        }
      ]
    }
    nostojs(api => {
        api.getSearchSessionParams().then(response => {
            console.log(response);
        });
    });
    <script>
        nostojs(api => {
            api.getSearchSessionParams().then(response => {
                document.cookie = `nostoSearchSessionParams=${encodeURIComponent(JSON.stringify(response))};`;
            });
        })
    </script>
    query (
      $query: String,
      $products: InputSearchProducts,
      $sessionParams: InputSearchQuery
    ) {
      search(
        accountId: "YOUR_ACCOUNT_ID"
        query: $query
        products: $products,
        sessionParams: $sessionParams
      ) {
        query
        products {
          hits {
            productId
            url
            name
            imageUrl
            thumbUrl
            brand
            availability
            price
            listPrice
            customFields {
              key
              value
            }
            skus {
              name
              customFields {
                key
                value
              }
            }
          }
          facets {
            ... on SearchTermsFacet {
              id
              field
              type
              name
              data {
                value
                count
                selected
              }
            }
            ... on SearchStatsFacet {
              id
              field
              type
              name
              min
              max
            }
          }
          total
          size
          from
          fuzzy
        }
      }
    }
    {
      "query": "red",
      "products": {
        "size": 5
      },
      "sessionParams": {}
    }

    GraphQL: Onsite Sessions

    is now available to help out with documentation and implementation of GraphQL onsite sessions

    Creating a session

    When a new user comes to the app, you can use this method to get a new session. It will return you a customer-id that can save on the device and use for future requests. This would be ideal.

    mutation {
      newSession(referer: "https://google.com?q=shoes")
    }

    Updating a session

    During a user session two things will happen that are likely not tied to viewing a page. Use the following mutations to keep the user session up to date.

    • If you use by: BY_CID, pass a Nosto session ID (see above).

    • If you use by: BY_REF, pass an external

    You will likely want to set skipEvents to true to prevent incrementing the page views. A common use case is to update the session after the "add to cart"-button on a product card has been clicked.

    You also have the option to include the cart and customer information in the mutations per page type. You can explore details in the , the general structure of the updateSession mutation is:

    The cart content must be updated whenever the cart contents change. The cart contents are the 1:1 representation of the user's cart.

    When a customer logs in, you can update the existing customer with the their data and potentially an external . This would merge the online and mobile sessions. If not needed, you can omit this.

    In order to use the GraphQL session mutation to fetch recommendations for your product page, the event, in this case, must be VIEWED_PRODUCT and you should specify the product-identifier of the current product being viewed.

    This optional event that can be sent to signal that a specific product variant (SKU in Nosto terms) is being viewed.

    • Typical use case for sending this event would be from product detail page when the user selects a product variant, such as some specific color and/or size.

    • The recommendations can then be configured in the Nosto admin UI to update and give preference for products that have similar variants available. For example "Other products also available in the same size", .

    Product variant views are added with targetFragment=skuId in the event the updateSession.params.

    Example for a product page after the SKU ID "589053" was selected by a user:

    In order to use the GraphQL session mutation to fetch recommendations for your category page, the event, in this case, must be VIEWED_CATEGORY and you should specify a fully qualified category path of the current category. For example, if you have a category called "Dresses" under the category "Women", the FQCN would be "/Women/Dresses".

    In order to use the GraphQL session mutation to fetch recommendations for your search page, the event, in this case, must be SEARCHED_FOR and you should specify the search term of the query.

    In order to use the GraphQL session mutation to fetch recommendations for your cart or checkout page, the event, in this case, must be VIEWED_PAGE and you should specify a fully qualified URL of the page as the target.

    In order to use the GraphQL session mutation to fetch recommendations for your home or front page, the event, in this case, must be VIEWED_PAGE and you should specify a fully qualified URL of the page as the target

    Please review the PageRequestEntity in the to ensure all page types are tracking the user behavior.

    You can apply the same concept as in the examples above with e.g. forNotFoundPage() and forOtherPage().

    Recommendation results can be attributed to events by setting a session event's ref to the recommendation result's resultId.

    Here is an example of some recommendations for a front page. You can see recommendation result's resultId is "frontpage-nosto-1".

    If a customer selects to view the Cool Kicks product, you can generate the following request. Note that the event's ref is set to "frontpage-nosto-1".

    If your site uses Nosto variants for or , you must pass the variantId (e.g. USD, EUR or GENERAL, GUEST, WHOLESALE) to the params inside of pages and the respective forXXPage() field to retrieve the correct price and availability for the products.

    Example for a product page to retrieve WHOLESALE prices and availability:

    When making GraphQL queries from mobile applications, it's essential to define the user agent string in your HTTP headers. Ideally, the user agent should represent the mobile environment, including details such as the platform, device type, and application version. Avoid using terms like "bot" in the user agent string, as this might lead to unintended behavior or rejection of the query/session. Sending an empty user agent will also lead to be catch by the bot detection mechanism.

    Implement Personalized Campaigns

    How to implement Product Recommendations, Dynamic Bundles and Onsite Content Personalization.

    If you have custom requirements like customer group pricing/visibility or a highly complex product card, we recommend using one of the Nosto APIs to only retrieve the core product data via JSON and get prices and visibility from your platform instead of sending it to Nosto.

    If you only have a complex product card but are using a Shopify theme, you can consider using our

    Set the cart

    Set the customer

    Working with recommendations

    On the Product Page

    Tracking Product Variant Views

    On the Category Page

    On the Search Page

    On the Cart Page

    On the Front Page

    On additional page types

    Attribution of Recommendation Results

    Customer Group Pricing and Multi Currency

    GraphQL from mobile applications

    customer-reference
    GraphQL playground
    customer-reference
    read more about variant affinities here
    GraphQL Playground
    customer group pricing
    multi currency
    Nosto MCP Server Beta
    mutation {
      updateSession(
        by: ...,
        id: "...",
        params: {
          cart: { ... },
          customer: { ... },
          event: { ... },
          skipEvents: ...
        }
      ) {
        id
        cart: { ... }
        customer: { ... }
        events: { ... }
        pages: { ... }
        segments: { ... }
      }
    }
    mutation {
      updateSession(by: BY_CID, id: "5b1a481060b221115c4a251e",
        params: {
          cart: {
            items: [
              {
                productId: "100",
                skuId: "100-1",
                name: "#100",
                unitPrice: 199,
                priceCurrencyCode: "EUR",
                quantity: 1
              },
              {
                productId: "200",
                skuId: "200-1",
                name: "#200",
                unitPrice: 299,
                priceCurrencyCode: "EUR",
                quantity: 2
              }
            ]
          },
          skipEvents: true,
          event: {
            // Same data as below from the different page types, will get ignored if skipEvents: true
          }
        }
      ) {
        id
      }
    }
    mutation {
      updateSession(by: BY_CID, id: "5b1a481060b221115c4a251e",
        params: {
          customer: {
            firstName: "Mridang"
            lastName: "Agarwalla"
            email: "mridang@nosto.com"
            customerReference: "b369f1235cf4f08153c560.82515936"
            marketingPermission: false
            doNotTrack: false
          }
          skipEvents: true,
          event: {
            // Same data as below from the different page types, will get ignored if skipEvents: true
          }
        }
      ) {
        id
      }
    }
    mutation {
      updateSession(by: BY_CID, id: "5b1a481060b221115c4a251e",
        params: {
          event: {
            type: VIEWED_PRODUCT
            target: "11923861519"
            ref: "front-page-slot-1"
          }
        }
      ) {
        pages {
          forProductPage(params: {
            isPreview: false, imageVersion:  VERSION_8_400_400
          }, product: "11923861519") {
            divId
            resultId
            primary {
              productId
            }
          }
        }
      }
    }
    mutation {
      updateSession(by: BY_CID, id: "5b1a481060b221115c4a251e",
        params: {
          event: {
            type: VIEWED_PRODUCT
            target: "11923861519",
            targetFragment: "589053",
            ref: "front-page-slot-1"
          }
        }
      ) {
        pages {
          forProductPage(params: {
            isPreview: false, imageVersion:  VERSION_8_400_400
          }, product: "11923861519") {
            divId
            resultId
            primary {
              productId
            }
          }
        }
      }
    }
    mutation {
      updateSession(by: BY_CID, id: "5b1a481060b221115c4a251e",
        params: {
          event: {
            type: VIEWED_CATEGORY
            target: "/Shorts"
          }
        }
      ) {
        pages {
          forCategoryPage(params: {
            isPreview: false, imageVersion:  VERSION_8_400_400
          }, category: "Shorts") {
            divId
            resultId
            primary {
              productId
            }
          }
        }
      }
    }
    mutation {
      updateSession(by: BY_CID, id: "5b1a481060b221115c4a251e",
        params: {
          event: {
            type: SEARCHED_FOR
            target: "black shoes"
          }
        }
      ) {
        pages {
          forSearchPage(params: {
            isPreview: false, imageVersion:  VERSION_8_400_400
          }, term: "black shoes") {
            divId
            resultId
            primary {
              productId
            }
          }
        }
      }
    }
    mutation {
      updateSession(by: BY_CID, id: "5b1a481060b221115c4a251e",
        params: {
          event: {
            type: VIEWED_PAGE
            target: "https://example.com/cart"
          }
        }
      ) {
        pages {
          forCartPage(params: {
            isPreview: false, imageVersion:  VERSION_8_400_400
          }, value: 100) {
            divId
            resultId
            primary {
              productId
            }
          }
        }
      }
    }
    mutation {
      updateSession(by: BY_CID, id: "5b1a481060b221115c4a251e",
        params: {
          event: {
            type: VIEWED_PAGE
            target: "https://example.com"
          }
        }
      ) {
        pages {
          forFrontPage(params: {
            isPreview: false, imageVersion:  VERSION_8_400_400
          }) {
            divId
            resultId
            primary {
              productId
            }
          }
        }
      }
    }
    {
      "data": {
        "updateSession": {
          "pages": {
            "forFrontPage": [
              {
                "resultId": "frontpage-nosto-1",
                "primary": [
                  {
                    "productId": "9497180547",
                    "name": "Cool Kicks",
                    "url": "https://example.com/products/cool-kicks"
                  },
                  {
                    "productId": "9497180163",
                    "name": "Awesome Sneakers",
                    "url": "https://example.com/products/awesome-sneakers"
                  },
                  {
                    "productId": "4676165861430",
                    "name": "Free gift",
                    "url": "https://example.com/products/free-gift"
                  },
                  {
                    "productId": "2188051218486",
                    "name": "Furry Dice",
                    "url": "https://example.com/products/furry-dice"
                  },
                  {
                    "productId": "9497180611",
                    "name": "Gnarly Shoes",
                    "url": "https://example.com/products/gnarly-shoes-1"
                  }
                ]
              }
            ]
          }
        }
      }
    }
    mutation {
      updateSession(by: BY_CID, id: "5b1a481060b221115c4a251e",
        params: {
          event: {
            type: VIEWED_PRODUCT
            target: "9497180547"
            ref: "frontpage-nosto-1"
          }
        }
      ) {
        pages {
          forProductPage(params: {
            isPreview: false, imageVersion:  VERSION_8_400_400
          }, product: "9497180547") {
            divId
            resultId
            primary {
              productId
            }
          }
        }
      }
    }
    mutation {
      updateSession(by: BY_CID, id: "5b1a481060b221115c4a251e",
        params: {
          event: {
            type: VIEWED_PRODUCT
            target: "11923861519"
            ref: "front-page-slot-1"
          }
        }
      ) {
        pages {
          forProductPage(params: {
            isPreview: false,
            imageVersion:  VERSION_8_400_400,
            variantId: "WHOLESALE"
          }, product: "11923861519") {
            divId
            resultId
            primary {
              productId
            }
          }
        }
      }
    }
    Nosto script in the frontend (Nosto Debug Toolbar is loading)
    1. Every Nosto account comes with a set of default product recommendation campaigns and "placements" (empty <div/> elements).

      • You can see the mapping of campaigns and placements in the Nosto Admin UI: Product Experience Cloud -> Recommendations.

      • These are sorted into the different page types like homepage (e.g. #nosto-frontpage-1), PLP, PDP, SERP, 404 page as well as general layout areas like the mini-cart drawer or search overlay/autocomplete.

    2. Placements need to be set up in your store templates. On Shopify and Shopware, you can use our content blocks. For assistance in setting up placements on your store, please reach out to your Nosto POC or Nosto's support team.

    • Nosto campaigns need to be injected into the placements - automatically or manually, depending on your tech stack and implementation method.

    • Nosto offers you several helper methods to inject campaign into the DOM, you'll find details at the end of this page.

    1. Templates for Recommendation campaigns can be hosted and maintained in Nosto or built within your own code base (API approach, recommended for headless and SPAs when using a custom code setup).

    2. Depending on your implementation method and tech stack, different options to attribute clicks from Nosto campaigns are available (it might need a few lines of custom code, you'll find details below per implementation method).

      • Please note: If you have the Nosto preview enabled, attribution/references will not show in the Nosto debug toolbar. You can do QA via the ev1 request in your network tab (details below).

    3. OCP and Recommendation campaigns are always associated with exactly one placement.

      • The placements are also used for A/B testing, e.g. testing campaign A vs. campaign B inside of placement #nosto-productpage-1.

      • Requesting campaigns via GraphQL is limited (no A/B testing, no dynamic filtering).

    1. After the Nosto script is loaded on a page, you can send a request to Nosto and will receive the campaigns for this specific page.

    2. Your request to Nosto needs to include certain data like the current page type, what's on the page (e.g. product data, the current category or search term), what's in the cart, customer details if the customer is logged in and what placements are on the page.

      • You'll see this ev1 request in your network tab and will monitor it extensively while implementing Nosto.

    3. The includes mostly the but also meta data like the in the current session, a session ID, a customer ID and more.

      • OCP campaigns are always returned as raw .

      • Product Recommendation campaigns can be returned as raw HTML or JSON ( with []).

    4. Depending on your tech stack and templating method, the campaigns get automatically injected into the page (conventional) or need to be explicitly rendered (advanced).

    5. Interactions with Nosto campaigns (like clicking on a product, selecting a variant/color swatch or clicking on a banner) need a certain attribution that always follows the same pattern: "This event X (page/product/variant/… has been viewed/selected) after an interaction with the campaign Y (on page Z (optional)).":

      • Product ID 8 was viewed after a click in Recommendation campaign nosto-pdp-top on the PDP with product ID 4.

      • Product ID 6 was viewed after a click (quick view modal or PDP redirect) in Recommendation campaign

    Example Nosto response:

    Since OCP campaigns always return HTML content, this guide only compares product recommendations (Recommendations and Bundles). (Example response and type reference above.)

    This method is the fastest and works best for conventional builds where the templates are built within the Nosto backend with Apache Velocity. This approach is not suitable for SPAs since interactions trigger a full page load.

    • By default, the Nosto autoloader is enabled and content will be automatically injected into the templates on the page.

    • Attribution is automatically handled by Nosto as it knows which HTML template was used in what campaign.

    • If you are on Shopify, we recommend to evaluate our dynamic product cards which allow you to re-use your existing product cards.

    • You can make use of several inside of your template.

      • You will find a full reference and examples in the Nosto backend when you're building your template.

    In case you want more control about the campaign loading, you can disable autoloading and request the campaigns yourself. This approach is also suitable if you only want to request the product data in JSON per campaign and build the templates yourself. This approach is not suitable for SPAs or headless frontends, please see Session API: defaultSession() below.

    • The campaigns can return HTML (default, for templates hosted at Nosto) or JSON, depending on how you build the request.

    • By default, the request does not know anything about the current page, placements on the current page, logged in customer, cart content etc. and you have two options of passing that data:

      • Including the HTML tagging with {includeTagging: true}

      • Setting the data manually via JS, e.g. setPageType("product").setProducts([product_id: "4"])

        • In most cases it will be sufficient to only include the page tagging since it reads the current product, cart content etc. and add a .setPlacements(api.placements.getPlacements()) call.

        • Advanced cases where you need to explicitly set data occur when e.g. a variant has been selected on a PDP or if products should be filtered by a certain tag (e.g. for cannabis state-specific regulation or for vehicle-specific parts).

    • Attribution is automatically handled by Nosto when using the default response mode HTML.

      • If you use the JSON response mode, you can with Nosto's helper methods.

    • This approach is recommended for custom frontend builds, we recommend looking into the .

    In case you are running a SPA or headless frontend, you want more control about the campaign loading and need to disable autoloading to request the campaigns yourself.

    You can also find a video for the Session API in the Nosto Partner Academy, just reach out to your Nosto contact if you don't have access yet. We also recommend to bookmark the video to debug the Session API.

    • The campaigns return JSON by default, but in comparison to the JS API, you incorporate requesting campaigns with your page tagging/tracking via defaultSession() (reference).

      • Page tagging refers to the JS API (taggingProvider) and uses HTML and MUST NOT be mixed with the Session API.

      • Page tracking is similar to the page tagging but uses JavaScript calls to let Nosto know what is on the current page. For example:

    • The page tracking sends the same which responds with the same campaign data and the same principles as the JS API.

    • Since requesting campaigns is tied to the defaultSession() for page tracking, you can run a very similar code block on the different page types ( and in the ).

      • The methods like viewFrontPage() or viewProduct("4") are the main indicator that campaigns will be returned.

    • Calling load() sends the request to Nosto, the returned Promise can be handled async or by chaining a then() to the request.

    • Since there is no page tagging, you need to use the and the in your network tab for verification and QA.

    • There are several advanced cases to keep in mind and cover:

      • Using the category path that's in the Nosto backend (use the "Preview" feature and ), not the URL slug.

    • You can still setResponseMode("HTML") and request the Nosto-hosted templates if you're not running a SPA. The click attribution and template injection can be automated by calling enableCampaignInjection() ().

    In case you don't want follow one of the client-based approaches, you can manage the Nosto session and campaign rendering via GraphQL.

    Please beware of the following limitations:

    • You request the campaigns for a specific product ID or category (without placements) and will receive the Recommendation campaign IDs directly.

      • Therefore, you can't use Nosto built-in A/B testing for campaign widgets.

      • You need an alternative, full page A/B testing like Omniconvert in this case.

    • is not possible via GraphQL. We highly recommend to go with the Session API and use

    • Nosto OCP cannot be retrieved via GraphQL (personalized banners or other HTML content).

    • Adding explicit affinity signals manually like with the is not supported. as well as are supported.

    • The page tagging/event tracking (current customer data and shopping cart) can also be done via , example:

    • The request/response concept is the same as with the Session API: specify data about the session (cart and customer, see above), request product recommendations for a given page type and render the template while keeping attribution in mind. You will need to:

      • Set the params.event to match the current page type and specific e.g. the product ID, category path or search term for correct tracking and attribution.

        • The ref parameter must match the resultId (= Nosto recommendation campaign slot ID) from the response ().

      • Set the correct page type (PageRequestEntity in the API reference) under pages to specify the context from which you want to receive Nosto campaign data.

    • Parse the and render your template.

      • Make sure you save the resultId and pass it to your next updateSession(params: { event: { type: VIEWED_PRODUCT } } ) as ref for attribution when a shopper clicks on one of the products inside of your template.

    Typical use case, options and adjustment depending on your implementation method:

    • A shopper is on a PDP (e.g. product ID 42), sees a Nosto product recommendation and clicks on a shown product (ID 200). You don't explicitly react to the click, you instead make sure the click listener for parameterless attribution is firing. See the parameterless attribution documentation for more details.

      • If you're using Nosto-hosted templates with autoloading, you don't need to do anything. Parameterless attribution is enabled and set up by default.

      • If you're manually requesting Nosto-hosted HTML templates via the JS API, you have two options:

        • Let Nosto inject the campaigns into the DOM via injectCampaigns(), this is recommended, attribution is set up automatically.

        • Inject the campaigns into the DOM yourself and set up attribution manually via after rendering the campaign ().

      • If you're manually requesting Nosto-hosted HTML templates via the Session API, we recommend to add enableCampaignInjection() to your defaultSession(). Nosto will automatically inject the campaigns into the DOM, parameterless attribution is enabled and set up by default.

      • If you're manually requesting only the product data via JSON from a Nosto campaign via the Session API, you have two options after you've built the HTML for your campaigns in your code base:

        • Let Nosto inject the campaigns into the DOM via injectCampaigns(), this is recommended, attribution is set up automatically.

        • Inject the campaigns into the DOM yourself and set up attribution manually via after rendering the campaign.

    • When a user then clicks on a product link within a Nosto recommendation (e.g. slot ID "productpage-nosto-2-fallback"), Nosto's event listener (previously set up via enableCampaignInjection() for HTML templates and injectCampaigns() or attributeProductClicksInCampaign() for JSON-based templates) captures and stores the current URL and reference of the result_id in the shopper's browser local storage.

    • The browser then navigates to the product URL.

      • The Nosto client script reads the previous URL and reference from the local storage and sets it as ref parameter for the ev1 request, which results in the attribution of the current product view to the previously shown recommendation campaign.

    Feature

    Automatic Injection with Nosto Autoloading

    JS API: createRecommendationRequest()

    Session API: defaultSession()

    Nosto Content via GraphQL

    Best For

    Conventional builds

    Custom frontend builds (non-SPA) with e.g. customer group pricing/visibility (every platform) or highly complex product cards (non-Shopify)

    SPAs and Headless frontends

    Prerequisites

    dynamic product cards
    { 
        "campaigns": {
            "content": {
                "frontpage-banner": {
                    "div_id": "frontpage-banner",
                    "result_id": "5fc6390c60b2ecd3cc0c2d4f",
                    "html": "< Campaign html content >",
                    "params": ...
                }
            },
            "recommendations": {
                "frontpage-center-1": {
                    "div_id": "frontpage-center-1",
                    "result_id": "frontpage-center-1-fallback",
                    "products": [ /* Array of products, typed as JSONProduct */ ],
                    "params": ...
                }
            }
        }
    }
    mutation {
        updateSession(id: "ad8f0d0e-1156-4df2-b385-10e03f8f8a44",
        params: {
          customer: {
            firstName: "John"
            lastName: "Doe"
            marketingPermission: true
            customerReference: "319330"
          }
          event: {
            type: VIEWED_PRODUCT
            target: "400"
          }
          cart: {
            items: [
              {
                productId: "100",
                skuId: "100-1",
                name: "Product 100",
                unitPrice: 199,
                priceCurrencyCode: "EUR",
                quantity: 1
              }
            ]
          }
        }) {
          id
        }
    }

    Good to know before you start

    How it works

    Implementation Methods

    Client: Automatic Injection with Nosto Autoloading

    Client: JS API: createRecommendationRequest()

    Client: Session API: defaultSession()

    Server: GraphQL API: updateSession()

    DOM Injection and Click Attribution

    Comparison Table

    Using the JavaScript Library

    Search

    For client-side/frontend integrations, Nosto's JavaScript library can be used to simplify the integration. It provides a programming interface to access the Search & Categories API.

    For the most basic search the fields parameter should be provided to specify what product/keyword fields should be returned. Both products and keywords can be used separately and together, depending on the use case. For all parameters, see the reference.

    nostojs(api => {
        api.search({
            query: 'my search',
            products: { fields: ["name"] },
            keywords: { fields: ["keyword"] }
        }).then(response => {
            console.log(response);
        });
    });

    Note: The first parameter of api.search generally corresponds to the GraphQL schema accepted by the search backend. The second parameter includes more frontend-specific logic like tracking or following redirects.

    The second parameter of the api.search function also accepts the following optional fields:​

    Option
    Accepted values
    Default
    Description

    The function automatically loads session parameters required for personalization & segments in the background.

    In order to request custom fields, add the entries "customFields.key" and "customFields.value" to the requested product fields. This changes the example above like this

    For a search page, the facets parameter should generally be provided. In many cases, * is sufficient as a wildcard to include all facets.

    In order to automatically track search request to Nosto analytics, track parameter should be provided with the correct page type.

    isKeyword should be set to true if search is triggered by selecting a keyword suggested in the autocomplete.

    The redirect parameter, if set to true, causes the JS library to automatically follow any redirects returned by the backend. These are triggered by the merchant's configuration, redirecting certain user queries to specific pages. For example, query "summer" may get redirected to the "summer sale" collection.

    The default redirection mechanism simply updates the window location:

    Note: Do mix up response.redirect with the query's redirect property. The former contains the redirected target URL, while the latter is a boolean parameter on the query.

    You may want to set query.redirect to false when this redirection mechanism is insufficient and you want to use another method, such as your framework's routing library. You may obtain the redirect target from response.redirect field and act accordingly.

    track should be enabled to automatically track searches to Nosto analytics.

    For category pages in most cases the facets parameter should be provided. Additionally (for Shopify) and should be also provided.

    Furthermore redirect & track should be enabled to automatically track searches to Nosto analytics & redirect if API returns a redirect request.

    For some of the search features to work properly, such as personalized results and segments, the search function needs to be able to access information about the user's session from the front-end.

    To get all session data the following snippet can be used:

    The results of this function should be passed to search query parameter. In case search is called from backend, it should pass this data to backend (e.g. using ).

    The function accepts the following options:

    Option
    Default
    Description

    Tracking search events to analytics can be divided into three parts: search, search submit, search product click. These are user behaviors that should be tracked:

    • search submit (type = serp)

    • faceting, paginating, sorting (type = serp) or (type = category)

    JS API library provides tracking helpers for all of these cases.

    On errors from api.search calls the error object contains a status field that has the HTTP response status which can be used to determine the cause of the error in addition to the error message. The ranges of the status codes used for errors are 400-499 and 500-599. For details on these error codes, go to

    User actions that lead to search results should be tracked with api.recordSearch() after search request is done:

    • search submit (type = serp) - user submits search query in autocomplete component and is landed to SERP

    • faceting, paginating, sorting (type = serp) or (type = category) - user adjusts current search results by filtering (e.g. brand), selecting search page, sorting results

    Parameter
    Description

    Example:

    Example:

    Example:

    The tracking metadata is primarily taken from the third parameter. The and objects must be provided in the api.recordSearch call instead of partials.

    Search queries are categorised into two groups: organic and non-organic searches. In order to mark a search query as an organic search you need to call api.recordSearchSubmit(query: string). You should call it on search input submit only, before search request is sent.

    Product clicks should be tracked in autocomplete component, SERP, category page with api.recordSearchClick() by providing component (type), where click occurred, and clicked product data:

    Parameter

    Example:

    When shopping cart additions happen directly as part of the search, category or autocomplete results without a navigation to the product page, the api.recordSearchAddToCart() method should be called with the component (type), where addition occurred and the product data:

    Parameter

    Example:


    When tracking events, adherence to the following criteria is essential for capturing detailed and valid data:

    • query parameter:

      • The query string is an essential component for event tracking.

    💡 Tip: In case of API integration, use this example GraphQL partial query to integrate with the API and retrieve the necessary response data for precise event tracking.

    Bear in mind that search queries are split between organic and non-organic searches. To classify a search as organic, it is crucial to invoke api.recordSearchSubmit() upon the search input submission, before the actual search request is dispatched. This step is pivotal in ensuring the seamless tracking of organic searches through to the SERP.

    Tracking product clicks is fundamental for understanding user interaction. Use api.recordSearchClick() to monitor this actions correctly, specifying the type and relevant hit data.

    In case of an SPA based integration the api.recordSearchClick calls should be complemented with Session API or api.createRecommendationRequest() usage to couple the search analytics events to generic Nosto events for accurate attribution.


    nosto-frontpage-mid
    .
  • You can overwrite parts of the page tagging and filter products in a recommendation request (can include one or more placements) using dynamic filtering.

  • Adding setPlacements(api.placements.getPlacements()) or passing the placement IDs explicitly as an array determines from where campaigns will be requested. We recommend the first approach, getting all campaigns for all placements instead of requesting them one by one.
  • Nosto provides you with ways to easily inject campaigns into placements and sets up event listeners for click attribution.

  • You can filter products in a recommendation using viewCustomField() which is the equivalent to dynamic filtering via the JS API (you MUST NOT mix these APIs).
  • If you are using the setRef() method (reference), pay close attention - the second parameter is the recommendation slot id (result_id of the response), not the placement div id.

  • Using load() only on the first request on the current page because it increments the page view counter (pv in the "ev1" response). On subsequent requests on the same page you must send the request with update() or pass a recommendation request flag like .load{skipPageViews: true} (details here).

  • Since you don't use JavaScript, there is no event listener or helper function Nosto can provide. You will build the process yourself that is outlined in the parameterless attribution documentation (detailled example below).

    Mobile apps or server-side rendered builds (when Nosto A/B testing, dynamic filtering and OCP isn't needed)

    How it Works

    Content is automatically injected into page templates.

    Manually request campaigns after disabling autoloading.

    Request campaigns as part of the page tracking/tagging flow.

    Request campaigns as part of the page tracking/tagging flow.

    Campaign Response Type

    HTML (for Nosto-hosted templates)

    HTML (default) or JSON

    JSON (default), but can be set to HTML

    Recommendation campaign slot IDs (no placements, OCP campaigns are not available)

    Campaign Injection and Click Attribution

    Handled automatically by Nosto.

    Automatic for HTML mode with enableCampaignInjection(). For JSON mode, use injectCampaigns() or inject campaigns yourself and add api.attributeProductClicksInCampaign().

    For JSON mode, use injectCampaigns() or inject campaigns yourself and add api.attributeProductClicksInCampaign(). Automatic for HTML mode.

    Manual. Requires careful use of the event params in updateSession() mutation.

    SPA Suitable

    No (triggers a full page load)

    No (recommended for custom builds, but not SPAs)

    Yes (designed for SPAs and Headless)

    Yes

    Headless compatible

    No

    No

    Yes

    Yes

    Fully customizable frontend

    Yes

    Yes

    Yes

    Yes

    Suitable for complex use cases

    Sometimes

    Yes

    Yes

    Yes

    Customized and managed only in Nosto dashboard

    Yes (templates are in Nosto backend)

    Depends on (can build templates in Nosto or own code base, injection needs to be triggered manually via JS)

    Unlikely (you can build templates in Nosto but will likely build them in the own code base, injection needs to be triggered manually via JS)

    No (can build templates in own code base via server-side rendering)

    A/B Testing

    Yes (via placements)

    Yes (via placements)

    Yes (via placements)

    No (Nosto built-in A/B testing is not available, needs full page testing like OmniConvert)

    Drawbacks

    Not suitable for SPAs or Headless.

    Not suitable for SPAs or Headless.

    Requires a bit more planning because page tracking and campaign injection are combined

    No Nosto A/B testing. No Dynamic Filtering. No OCP (HTML content/banners).

    Nosto response
    recommendation campaigns
    number of pages visited
    HTML
    JSONResult
    JSONProduct
    Nosto-variables
    simplify DOM injection and click attribution
    Nosto Open Source packages
    ev1 request
    examples here
    API reference
    "Visitor" tab in the Nosto Debug Toolbar
    ev1 request
    Nosto Debug Toolbar
    Sending an additional event when a specific SKU has been selected (either on a PDP or via a "quick view" modal.
    example
    Dynamic filtering
    viewCustomField
    JS API
    Personalization via variant/SKU affinity
    multi currency or customer group pricing
    a GraphQL mutation that returns the session ID
    example response above
    Nosto response
    attributeProductClicksInCampaign
    example here
    attributeProductClicksInCampaign

    undefined (No tracking)

    Track the search query as coming from the provided page type

    isKeyword

    true false

    false (Not a keyword)

    Indicates that the search is triggered by a keyword click in autocomplete

    autocomplete input (type = autocomplete)
  • search results product click (type = serp), (type = autocomplete) or (type = category)

  • autocomplete keyword click (type = autocomplete)

  • category merchandising results (type = category)

  • autocomplete input (type = autocomplete) - user sees partial results while typing in search input
  • category merchandising results (type = category) - user sees specific category results when category is selected (category merchandising must be implemented)

  • options (optional)

    Record search options. Currently is accepts:

    isKeyword: boolean - should be set when keyword in autocomplete is clicked (search is submitted via keyword)

    If present, include products.sort to track sorting behavior.
  • If applicable, incorporate products.filter.

  • searchResults parameter:

    • products.hits array containing objects with a productId is mandatory.

    • products.total number to identify if the search has results.

    • For accurate pagination tracking, products.from and product.size must be included.

    • For identifying if the request was autocorrected include products.fuzzy.

    • For category requests, either products.categoryId or products.categoryPathis mandatory.

  • redirect

    true

    false

    false (Ignore redirects)

    Automatically follow page redirects if instructed by the backend response

    track

    maxWait

    2000

    Maximum execution time in MS

    cacheRefreshInterval

    60000

    Maximum cache time

    type

    Search type: serp, autocomplete, category

    request

    Full search API request

    result

    Full search API result

    type

    Search type: serp, autocomplete, category

    hit

    Object containing productId and url, collected from clicked product

    type

    Search type: serp, autocomplete, category

    hit

    Object containing productId and url, collected from product

    Search page

    Redirects

    Autocomplete

    Category page

    Session parameters

    The search function of the JS API already includes session state automatically.

    Analytics

    Error Handling

    Search

    You don't need to execute api.recordSearch()if you call api.search(query, { track: 'serp'|'autocomplete'|'category'}) function from JS API, becauseapi.search()already calls api.recordSearch() when track option is provided.

    Keyword click tracking

    Category results tracking

    Search form submit

    Organic search - is a search query submitted through search input and which lead to SERP (search engine results page). Following faceting, paginating, sorting queries on organic query is also counted as organic.

    Search product click

    Search add to cart additions

    ⚠️ Event Tracking Requirements ⚠️

    🚩 Search Form Submit

    🔍 Accurate Click Tracking

    categoryId
    categoryPath
    sessionParams
    form data
    HTTP response status codes
    full request
    full result

    "autocomplete"

    "category"

    "serp"

    undefined

    nostojs(api => {
      api.defaultSession()
        .viewProduct("product-123")
        .setPlacements(api.placements.getPlacements())
        .load()
    });
    // ...
            products: { fields: ["name", "customFields.key", "customFields.value"] },
    // ...
    nostojs(api => {
        api.search({
            query: 'my search',
            products: {
                facets: ['*'],
                fields: ['name'],
                size: 10
            }
        }, {
            redirect: true,
            track: 'serp',
            isKeyword: true
        }).then(response => {
            console.log(response);
        });
    });
    window.location.href = response.redirect
    nostojs(api => {
        api.search({
            query: 'my search',
            products: {
                fields: ['name'],
                size: 10
            },
            keywords: {
                fields: ['keyword'],
                size: 5
            }
        }, {
            track: 'autocomplete'
        }).then(response => {
            console.log(response);
        });
    });
    nostojs(api => {
        api.search({
            products: {
                categoryId: '12345',
                categoryPath: 'Pants',
                fields: ['name'],
                size: 10
            }
        }, {
            track: 'category',
            redirect: true
        }).then(response => {
            console.log(response);
        });
    });
    nostojs(api => {
        api.getSearchSessionParams(options).then(response => {
            console.log(response);
        });
    });
    nostojs(async (api) => {
    
        const searchRequest = {
          query: "shoes",
          products: {
            sort: [{ field: "price", order: "asc" }],
            filter: [{ "field": "brand", "value": "Nike" }]
          }
        }
        const response = await sendGraphQlRequest(searchRequest)
        const searchResult = response.data.search
    
        api.recordSearch(
            type,
            searchRequest,
            searchResult,
            options
          )
    })
    api.recordSearch(
        "serp",
        {
            query: "shoes",
            products: {
                sort: [{ field: "price", order: "asc" }],
                filter: [{ "field": "brand", "value": "Nike" }]
            }
        },
        {
            products: {
                hits: [{ productId: "123" }, { productId: "124" }],
                fuzzy: true,
                total: 2,
                size: 2,
                from: 0
            }
        },
        {
            isKeyword: false
        }
    )
    api.recordSearch(
        'autocomplete', 
        searchRequest,
        searchResult,
        { 
            isKeyword: true
        } 
    )
    api.recordSearch(
        'category',
        {
            products: {
                categoryId: "123456",
                categoryPath: "Pants",
                sort: [{ field: "price", order: "asc" }]
                size: 24,
                from: 0,
            }
        },
        {
            products: {
                categoryId: "123456",
                categoryPath: "Pants",
                hits: [{ productId: "123" }, { productId: "124" }],
                fuzzy: true,
                total: 18,
                size: 24,
                from: 0
            }
        },
        {
            isKeyword: false
        }
    )
    nostojs(api => {
        const query = 'shoes'
        api.recordSearchSubmit(query)
    })
    nostojs(api => {
        api.recordSearchClick(type, hit)
    })
    api.recordSearchClick(
        "autocomplete", 
        { productId: "123", url: "https://myshop.com/product123" }
    )
    nostojs(api => {
        api.recordSearchAddToCart(type, hit)
    })
    api.recordSearchAddToCart(
        "autocomplete", 
        { productId: "123", url: "https://myshop.com/product123" }
    )
    query {
      search(
        accountId: "YOUR_ACCOUNT_ID"
        query: "green"
        products: { size: 10, from: 10 }
      ) {
        products {
          hits {
            productId
          }
          total
          size
          from
          fuzzy
          categoryId
          categoryPath
        }
      }
    }

    Implement Search results page

    API Requests

    Searching

    For a basic search, it’s enough to provide accountId, query, and select fields that should be returned. You can control which product attributes to select through products.hits field. See all available fields.

    Query

    For example, if you want to return only productId and name, the query would be:

    query {
      search(accountId: "YOUR_ACCOUNT_ID", query: "green") {
        products {
          hits {
            productId
            name
          }
          total
          size
          from
        }
      }
    }

    Playground example

    Query parameters:

    See all .

    Products offset parameter from is used for pagination functionality.

    The default count of documents returned per page is size = 5, you can change it with products.size, and offset of products is controlled with products.from field:

    The total value in the response is useful for pagination as well:

    • total / products.size is the number of available pages with the current page size.

    • products.from + products.size >= total is true when the last page has been reached. This is particularly useful for infinite scrolling/load more solutions.

    By default results are sorted by products relevance score.

    To change the sorting, use the sort parameter, where you would specify any indexed field which should be sorted by, and order: asc for ascending and desc for descending. Top-level product fields can be used for sorting using . To sort by a custom field, prefix the custom field name with customFields.. Likewise, extracted fields need to be prefixed with extra. to use them for sorting.

    By default, you should always sort by relevance and merchandising rules, which is achieved by not specifying any sort parameter. Only if the user selects a different sort method, a sorting rule should be used.

    Facets help the user to find products more easily. Faceted navigation is normally found in the sidebar of a website and contains filters only relevant to the current search query. Facets are configured in the Nosto dashboard.

    One of the facet types is type = terms. It returns list if common terms from found documents.

    Assume that we have configured facets for customFields.brandname and categories:

    Stats facet returns minimum and maximum number field value from found documents. The most common usage is to render slider filter (e.g. price)/

    Filtering by terms facet, for example by Adidas, Converse brands:

    When filtering by multiple same field items, filters will be joined with OR operator and different fields with AND.

    If you wish to have more facets, you should configure it in the Nosto dashboard first.

    Filtering by stats field, for example by price:

    You can sort using these arguments: lt (less than), gt (greater than), lte (less than or equal to), gte (greater than or equal to).

    Redirects can be used to forward users to special pages depending on their search keywords. For example, users searching for shipping could be forwarded to https://example.com/shipping.html.

    You can request specific currency formatting settings for prices returned in the search results. This is done by specifying the currencyFormat parameter within the products input. The actual formatting details (like currency symbol, placement, decimal places) are then returned in the priceFormat field within the products object of the response.

    To select which pre-configured currency settings to retrieve, include the currencyFormat parameter within the products input. Additionally, ensure you request the priceFormat field in your query to receive these details.

    Variables Example:

    • If currencyFormat is not provided in the products input, the default currency format configured for the account will be used for the priceFormat field.

    • If currencyFormat is provided but corresponds to a currency for which no settings are configured, an error will be returned.

    These parameters describe how the prices should be formatted on the frontend based on the selected currencyFormat.

    Name
    Description

    For features like personalized results and user segments to function effectively, the search function needs access to the user's session information. Session information can be .

    Alternatively, it's possible to get search session data using the :

    The results of this function should be passed to search query parameter. In case search is called from backend, it should pass this data to backend (e.g. using ).

    To analyze user behavior you need to implement tracking. This can be achieved in two different ways, depending on the integration environment:

    • (recommended - more convenient, but requires a JavaScript environment).

    • (works anywhere).

    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,

    data.value

    original facet value, it should be displayed in the user interface

    data.count

    shows how many products will be returned if you select this facet, it should be displayed in the user interface

    data.selected

    indicates if there is an active filter on this value

    min

    minimum field value for documents that match provided query

    max

    maximum field value for documents that match provided query

    If currencyFormat is not provided and no default currency format exists for the account, an error will be returned.

    decimalSeparator

    The character used to separate the decimal part of the price (e.g., ".", ",").

    thousandSeparator

    The character used to separate thousands in the price (e.g., ",", " ").

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

    accountId

    Nosto account ID

    query

    search query text

    id

    internal facet ID, used to select specific facets in query

    field

    facet field, should be used for filtering

    type

    facet type, in this case terms

    name

    user friendly facet name configured in the dashboard

    name

    user friendly facet name configured in the dashboard

    terms

    facet type, in this case stats

    field

    facet field, should be used for filtering

    id

    internal facet ID, used to select specific facets in query

    currencySymbol

    The symbol for the currency (e.g., "$", "€").

    placement

    Indicates where the currency symbol is placed relative to the price ("before" or "after").

    decimalPlaces

    The number of decimal places to display for the price.

    Pagination and size

    Up to 250 products can be retrieved in a single page, corresponding to size = 250.

    Query

    Sorting

    When sorting by one or more fields, only the field(s) dictate the order of products. Merchandising rules have no effect.

    Query

    Some search implementations are more straightforward if sorting is defined in all search queries, even ones that use default sorting (by relevance and rules). Sorting by field _score with order desc is equivalent to omitting the sort parameter entirely.

    Faceting

    To use facet for a specific field you need to configure it in the Nosto dashboard first.

    Terms facet

    Query

    Response

    Response parameters:

    Stats facet

    Query

    Response

    Response parameters:

    Filter

    Query

    Filters in requests take precedence over merchandising rules. Filtered products can't be brought back using pinning.

    Redirects

    For API integrations GraphQL can only return the target URL. The actual browser redirect must be implemented by the merchant.

    Query

    Response

    Price Formatting and Currency Display

    Query

    Behavior and Error Handling:

    Response Example:

    priceFormat Response Parameters:

    Session params

    Analytics

    Nosto Analytics

    Search engine configuration

    query parameters
    Playground example
    documented field names
    Playground example
    Playground example
    GraphQL playground example
    GraphQL playground example
    GraphQL playground example
    GraphQL playground example
    queried from the session API
    JS API
    sessionParams
    form data
    JavaScript library
    GraphQL API
    Searchable Fields
    Facets
    Ranking and Personalization
    Terms facet
    Stats facet
    query {
      search(
        accountId: "YOUR_ACCOUNT_ID"
        query: "green"
        products: { size: 10, from: 10 }
      ) {
        products {
          hits {
            name
          }
          total
          size
          from
        }
      }
    }
    query {
        search(
          accountId: "YOUR_ACCOUNT_ID"
          query: "green"
          products: {
            sort: [
              {
                field: "price"
                order: asc
              }
            ]
          }   
      ) {
          products {
            hits {
              productId
              name
              price
            }
          }
        }
      }
    query {
      search(
        accountId: "YOUR_ACCOUNT_ID"
        query: "green"
      ) {
        products {
          facets {
            ... on SearchTermsFacet {
              id
              field
              type
              name
              data {
                value
                count
                selected
              }
            }
          }
        }
      }
    }
    {
      "data": {
        "search": {
          "products": {
            "facets": [
              {
                "id": "345678901abc",
                "field": "categories",
                "type": "terms",
                "name": "Categories",
                "data": [
                  {
                    "value": "/Shoes",
                    "count": 30,
                    "selected": false
                  },
                  {
                    "value": "/Shoes/Sportswear",
                    "count": 6,
                    "selected": false
                  }
                ]
              }
            ]
          }
        }
      }
    }
    
    query {
        search(
          accountId: "YOUR_ACCOUNT_ID"
          query: "green"
      ) {
          products {
            facets {
              ... on SearchStatsFacet {
                id
                field
                type
                name
                min
                max
              }
            }
          }
        }
      }
    {
      "data": {
        "search": {
          "products": {
            "facets": [
                {
                    "id": "123456789abc",
                    "field": "price",
                    "type": "stats",
                    "name": "Price",
                    "min": 0.60,
                    "max": 70.99
                }
            ],
          }
        }
      }
    }
    query {
      search(
        accountId: "YOUR_ACCOUNT_ID"
        query: "green"
        products: {
          size: 10
          filter: [{ field: "brand", value: ["Adidas", "Converse"] }]
        }
      ) {
        products {
          hits {
            productId
            name
          }
          facets {
            ... on SearchTermsFacet {
              id
              field
              type
              name
              data {
                value
                count
                selected
              }
            }
          }
        }
      }
    }
    query {
       search(
        accountId: "YOUR_ACCOUNT_ID"
        query: "green"
        products: {
          filter: [
            {
              field: "price",
              range: {lt: "60", gt: "50"}
            }
          ]
        }
      ) {
        products {
          hits {
            productId
            name
          }
          facets {
            ... on SearchStatsFacet {
              id
              field
              type
              name
              min
              max
            }
          }
        }
      }
    }
    query {
      search(
        accountId: "YOUR_MERCHANT_ID",
        query: "shipping"
      ) {
        redirect
        products {
          hits {
            name
          }
        }
        keywords {
          hits {
            keyword
          }
        }
      }
    }
    {
      "data": {
        "search": {
          "redirect": "https://example.com/shipping.html",
          "products": {
            "hits": []
          },
          "keywords": null
        }
      }
    }
    query (
      $accountId: String,
      $products: InputSearchProducts,
    ) {
       search(
        accountId: $accountId
        products: $products
      ) {
         products {
          # This field will contain the details of the selected currency format
          priceFormat {
            currencySymbol
            placement
            decimalPlaces
            decimalSeparator
            thousandSeparator
          }
        }
      }
    }
    {
      "accountId": "shopify-55872454679-538837015-fi",
      "products": {
        "currencyFormat": "EUR"
      }
    }
    {
      "data": {
        "search": {
          "products": {
            "priceFormat": {
              "currencySymbol": "€",
              "placement": "after",
              "decimalPlaces": 2,
              "decimalSeparator": ",",
              "thousandSeparator": " "
            }
          }
        }
      }
    }
    nostojs(api => {
        api.getSearchSessionParams().then(response => {
            console.log(response);
        });
    });

    Analytics, Personalization and A/B testing

    Template and JavaScript integrations come with tracking- and A/B testing support out of the box. For pure API integrations, some extra steps need to be performed on the integration side to ensure that user interactions are tracked and attributed appropriately.

    • An API_APPS authentication token is necessary to implement API requests related to session management and tracking.

    The search request lifecycle looks like this:

    The key points are:

    Track impression event including found products and A/B variations (if applicable) when displaying search results.

  • Track click event including clicked product and A/B variations (if applicable) when clicking on a search result.

  • Store A/B variations received from the search API and include them in all following search requests for the duration of the session.

  • Storing the session ID for the duration of the session (30 minutes) is essential to ensure that the experience is personalized using the segments and affinities associated with the session.

    When requesting search results subject to an A/B test without supplying any A/B testing parameters, the search API assigns a random A/B variation and includes it in the search result. It is vital to include the returned A/B variations in following search requests within the same session to ensure a consistent experience. Failing to do so results in the user being assigned a new A/B variation for equivalent search requests, potentially leading to the user seeing different results, corresponding to the different A/B variations, for the same search.

    The following graphic uses a fictional scenario to illustrate which A/B testing information needs to be stored, sent to search, and tracked.

    • In search 1, the session starts without any A/B tests, so no A/B testing information is included in the search request. The request is affected by an A/B test, so the test ID and affected variation are returned. It must be tracked and stored.

    • In search 2, all known A/B assignments are included in the search request. This request is affected by a different A/B test, so the response contains information about this A/B test. Now, both of these A/B test's information should be stored for future searches within the session, but only the A/B test(s) affecting the latest search request should be included in the corresponding tracking requests.

    • In search 3, all known A/B assignments (now two) are included in the search request. The search isn't affected by any A/B tests, so the response doesn't contain any, and none should be tracked.

    • In search 4, the same known A/B tests are included in the search request. The request is affected by Test 1, and includes the known assignment for Test 1 in the request, ensuring that the assignment remains the same as before in the same session.

    • The session ends after search 4. Search 5 represents a search in a new session, which starts with fresh A/B variation assignments and fresh storage.

    Search is handled by the Nosto search GraphQL API. Its use is documented elsewhere in great detail.

    This is a minimal example search request that contains all fields relevant for tracking and A/B testing purposes:

    Session creation, segment retrieval, and analytics tracking is handled by the Nosto platform GraphQL API.

    Examples on this page use explicit session creation using the newSession mutation, and other API requests reference this session using the session ID and the parameter by: BY_CID.

    If some form of session ID is already available, creating a new session with the newSession mutation can be skipped. Use the already available session ID and replace by: BY_CID with by: BY_REF.

    Example for what segment retrieval looks like with an externally provided session ID:

    Creates a new session and returns that session's ID, which should be used in further interactions with this API. This step can be skipped if externally provided session IDs are used.

    Store the value of the newSession property for 30 minutes and include it in the following API interactions for the duration of the session.

    Learn more about session handling here.

    Retrieves segments and personalization affinities that have been assigned to this session. Segments must be included in search requests to leverage segmentation in merchandising rules.

    Request segments and affinities before searching. Note that segments and affinities can change during the course of the session based on user interactions.

    Learn more about session handling here. Note that updating the session as described in the linked article is necessary for new segments and affinities to be applied based on user behavior.

    Tracks search impressions (immediately upon displaying search results) and search clicks (upon clicking a product). The exact structure varies between impressions and clicks, but search metadata is the same for both.

    The specific structure of metadata depends on whether the user is searching or visiting a category.

    Here is an example of what metadata looks like for search requests:

    Properties:

    • hasResults: true if the search response total is greater than zero.

    • autoComplete: false for regular search, true for search requests used for providing autocomplete-style functionality. This distinguishes interactions in the Nosto search analytics dashboard.

    • autoCorrect: Set to the fuzzy value returned in the search response to indicate searches that required error-tolerant search.

    • keyword: Set to true if search keywords were requested (typically for autocomplete purposes).

    • organic: Set to true if the search was caused by a user action within the store. Searches caused by links to the store (e.g. from ads) are indicated by false.

    • refined: Set to true if the user searched before in this session, and searched again now with a different query.

    • refinedQuery: In case of refined search (see above), include the previously searched for query. Otherwise, this can be null or omitted entirely.

    • sorted: true when sorting by anything other than _score. Sorting by _score is default behavior if no sort parameter is supplied in the search request.

    • query: The current search query entered by the user into the search field.

    • resultId: Unique ID for this interaction. UUID4 is particularly useful for this.

    • searchType: Type of search logic being used, as returned by the API's searchType field. This is required for merchants using Hybrid Vector Search, but otherwise optional.

    Here is an example of what the (much simpler) category tracking metadata looks like:

    Properties:

    • category: Human-readable category name. This should be the same as the categoryPath parameter in category requests sent to the search API.

    • categoryId: Machine-readable category ID. This should be the same as the categoryId parameter in category requests sent to the search API.

    At least one of these parameter is required. Provide the same one(s) that are included in category requests sent to the search API.

    A/B test properties are the same for both impression and click tracking for both search and categories. They should contain all A/B variations that applied to the search request this tracking request is associated with.

    If the search API returns A/B test data like this:

    The corresponding tracking properties should look like:

    The object above is referred to in the following examples as $properties.

    This request must be sent immediately upon displaying search or category results.

    Example for search using previous examples for search metadata as $metadata and A/B test properties as $properties:

    Example for categories using previous examples for category metadata as $metadata and A/B test properties as $properties:

    Properties:

    • type: SEARCH for search events, CATEGORY for category events.

    • timestamp: Time of event must be formatted as ISO 8601 date.

    • page: 1-based page number.

    • productIds: The product IDs (productId property in search response) that are shown on this result page.

    The response contains a generic success message that is not necessary for further processing.

    This request must be sent when a search result is clicked. The request uses the same search metadata and A/B testing properties as impression tracking, so make sure to store them.

    Search example using previous examples for search metadata as $metadata and A/B test properties as $properties.

    Category example using previous examples for category metadata as $metadata and A/B test properties as $properties.

    Properties:

    • type: SEARCH for search events, CATEGORY for category events.

    • timestamp: Time of event must be formatted as ISO 8601 date.

    • productId: The product ID (productId property in search response) of the product that was clicked.

    The response contains a generic success message that is not necessary for further processing.

    The JavaScript program below implements the complete workflow of session maintenance, segment retrieval, search, and tracking with support for A/B testing. The general flow and data structures can be translated to any language.

    Error handling is largely omitted to focus on the more interesting bits.

    The workflow for search and categories is generally the same. This article describes the necessary steps mostly from a search perspective but provides additional information where deviation for category support is necessary.

    Limitations

    General workflow

    Implementation steps of a search request lifecycle
    query {
      search(
        accountId: "your merchant ID"
        query: "what the user typed"
        # In case of a category request, include categoryPath or categoryId *instead* of query, like so:
        # products: {
        #  categoryPath: "Tops and Shirts"
        #  categoryId: "AB1337"
        #}
        sessionParams: {
          segments: ["array", "of", "segment", "IDs", "from", "API"]
          products: {
            personalizationBoost: [
              # List each affinity belonging to the session using mapped names.
              #  - topBrands -> affinities.brand
              #  - topCategories -> affinities.categories
              #  - topProductTypes -> affinities.productType
              # SKU attributes are flattened.
              # "score" from the session API becomes "weight".
              # Example:
              {
                field: "affinities.brand"
                value: ["Amazing Brand"]
                weight: 0.6
              }
              {
                field: "affinities.categories"
                value: ["/accessories"]
                weight: 0.4
              }
              {
                field: "affinities.productType"
                value: ["accessory"]
                weight: 0.53
              }
              {
                field: "affinities.size"
                value: ["36"]
                weight: 0.5
              }
              {
                field: "affinities.color"
                value: ["green"]
                weight: 0.8
              }
            ]
          }
        }
        # For the first search in a session, this can be an empty array. All following searches should contain an array
        # of all A/B variation assignments returned by search within the same session.
        abTests: []
      ) {
        products {
          total
          fuzzy
          searchType
          hits {
            productId
          }
        }
        abTests {
          id
          activeVariation {
            id
          }
        }
      }
    }
    query {
      session(by: BY_REF, id: "1b3fed4c-8c0b-4445-9d7d-8809412b26db") {
        segments {
          id
        }
      }
    }
    mutation {
      newSession
    }
    {
      "data": {
        "newSession": "68b6f028a49067459453e89b"
      }
    }
    query {
      session(by: BY_CID, id: "68b6f028a49067459453e89b") {
        segments {
          id
        }
        affinities {
          topBrands {
            name
            score
          }
          topCategories {
            name
            score
          }
          topProductTypes {
            name
            score
          }
          topSkus {
            attribute
            values {
              name
              score
            }
          }
        }
      }
    }
    {
      "data": {
        "session": {
          "segments": ["5a497a000000000000000001", "5b71f1500000000000000006"],
          "topBrands": [
            {
              "name": "Amazing Brand",
              "score": 0.6
            }
          ],
          "topCategories": [
            {
              "name": "/accessories",
              "score": 0.4
            }
          ],
          "topProductTypes": [
            {
              "name": "accessory",
              "score": 0.53
            }
          ],
          "topSkus": [
            {
              "attribute": "size",
              "values": [
                {
                  "name": "36",
                  "score": 0.5
                }
              ]
            },
            {
              "attribute": "color",
              "values": [
                {
                  "name": "green",
                  "score": 0.8
                }
              ]
            }
          ]
        }
      }
    }
    {
      "hasResults": true,
      "autoComplete": false,
      "autoCorrect": false,
      "keyword": false,
      "organic": true,
      "refined": false,
      "refinedQuery": null,
      "sorted": false,
      "query":  "t-shirt",
      "resultId": "d65b040c-56ae-4c6d-a038-fe908e140855",
      "searchType": "keyword"
    }
    {
      "category": "Tops and Shirts",
      "categoryId": "AB1337"
    }
    // ...
    "abTests": [
      {
        "id": "65ca1ee5d05d1f5159f0ac7e",
        "activeVariation": {
          "id": "A"
        }
      }
    ]
    // ...
    {
      "abTestAttribution": [
        {
          "key": "65ca1ee5d05d1f5159f0ac7e",
          "value": "A"
        }
      ]
    }
    mutation ($metadata: InputSearchEventMetadataInputEntity, $properties: InputAnalyticEventPropertiesInputEntity) {
      recordAnalyticsEvent(
        id: "68b6f028a49067459453e89b"
        by: BY_CID
        params: {
          type: SEARCH
          timestamp: "2025-09-02T13:56:08.890Z"
          searchImpression: {
            metadata: $metadata
            page: 1
            productIds: ["0", "1", "2"],
            properties: $properties
          }
        }
      ) {
        errors {
          message
        }
        message
      }
    }
    mutation ($metadata: InputCategoryEventMetadataInputEntity, $properties: InputAnalyticEventPropertiesInputEntity) {
      recordAnalyticsEvent(
        id: "68b6f028a49067459453e89b"
        by: BY_CID
        params: {
          type: CATEGORY
          timestamp: "2025-09-02T13:56:08.890Z"
            categoryImpression: {
              metadata: $metadata
              page: 1
              productIds: ["0", "1", "2"],
              properties: $properties
          }
        }
      ) {
        errors {
          message
        }
        message
      }
    }
    mutation ($metadata: InputSearchEventMetadataInputEntity, $properties: InputAnalyticEventPropertiesInputEntity) {
      recordAnalyticsEvent(
        id: "68b6f028a49067459453e89b"
        by: BY_CID
        params: {
          type: SEARCH
          timestamp: "2025-09-02T13:56:08.890Z"
          searchClick: {
            metadata: $metadata
            productId: "<ID of the clicked product>",
            properties: $properties
          }
        }
      ) {
        errors {
          message
        }
        message
      }
    }
    mutation ($metadata: InputCategoryEventMetadataInputEntity, $properties: InputAnalyticEventPropertiesInputEntity) {
      recordAnalyticsEvent(
        id: "68b6f028a49067459453e89b"
        by: BY_CID
        params: {
          type: CATEGORY
          timestamp: "2025-09-02T13:56:08.890Z"
          categoryClick: {
            metadata: $metadata
            productId: "<ID of the clicked product>",
            properties: $properties
          }
        }
      ) {
        errors {
          message
        }
        message
      }
    }
    // This object stores values that are invariant for a given integration.
    const config = {
      // This is your merchant ID.
      merchantId: "<your merchant ID>",
      // Nosto API key with scope API_APPS - ask support if you don't have one.
      platformGraphqlApiKey: "<your token>",
      // API URLs and session time are constant.
      platformGraphqlUrl: "https://api.nosto.com/v1/graphql",
      searchGraphqlUrl: "https://search.nosto.com/v1/graphql",
      sessionTimeToLiveMilliseconds: 30 * 60 * 1000, // 30 minutes
    }
    
    // Simplistic placeholder for some sort of storage that survives individual
    // page views and lasts for the duration of the session.
    // In the backend, this could be a key-value store or database.
    // In the frontend, this could be localStorage, or a cookie.
    const store = {
      // Session ID to attribute user actions to the same session.
      sessionId: null,
      // Remember the session start time to be able to invalidate it after 30 minutes.
      sessionStart: null,
      // Accumulate and store all A/B variation assignments received in search
      // responses during the session, so they can be included in follow-up
      // searches. This ensures that users receive a consistent experience for
      // the duration of the session.
      abTests: [],
      // Store the most recent search metadata for tracking purposes.
      mostRecentSearchMetadata: null,
      // Store the most recent search request's A/B variations for tracking purposes.
      mostRecentABVariations: []
    }
    
    async function graphql(url, query, variables) {
      const authHeader = {
        "Authorization": "Basic " + btoa(`:${config.platformGraphqlApiKey}`)
      }
    
      const response = await fetch(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          // API key is only needed for platform API.
          ...(url === config.platformGraphqlUrl ? authHeader : {}),
        },
        body: JSON.stringify({ query, variables })
      });
      const json = await response.json();
      // Don't fail on soft errors.
      if (json.errors) {
        console.warn("GraphQL errors:", json.errors);
        if (!json.data) {
            throw new Error(JSON.stringify(json.errors, null, 2));
        }
      }
      // Definitely fail when the request fails completely.
      if (response.status !== 200) {
        throw new Error(JSON.stringify(response.status, null, 2));
      }
      return json.data;
    }
    
    async function createSessionIfMissingOrExpired() {
      const now = new Date()
      if (store.sessionStart && (now - store.sessionStart) < config.sessionTimeToLiveMilliseconds) {
        // Session still valid - keep using it.
        return
      } else {
        // Forget previous session assignments and metadata from past searches.
        // This allows the new session to be assigned to new A/B variations.
        clearSession()
      }
    
      // No active session or session expired - create a new one.
      const response = await graphql(config.platformGraphqlUrl, `
        mutation {
          newSession
        }`, {})
    
      // Store session ID for tracking.
      store.sessionId = response.newSession
    
      // Store start of session to know when to invalidate the session ID.
      store.sessionStart = now
    }
    
    function clearSession() {
      store.sessionId = null
      store.sessionStart = null
      store.abTests = []
      store.mostRecentSearchMetadata = null
      store.mostRecentABVariations = []
    }
    
    async function track(event) {
      await graphql(config.platformGraphqlUrl, `
        mutation ($sessionId: String!, $eventParams: InputRecordAnalyticsEventParams!) {
          recordAnalyticsEvent(id: $sessionId, by: BY_CID, params: $eventParams)
        }`,
        {
          sessionId: store.sessionId,
          eventParams: {
            timestamp: new Date().toISOString(),
            // Event type is the same for all search tracking.
            type: SEARCH,
            ...event
          }
        })
    }
    
    // Map the session's affinities to search "personalizationBoost" entries.
    // The session API and search API use different names, so the affinities need
    // to be translated:
    //   - topBrands       -> affinities.brand
    //   - topCategories   -> affinities.categories
    //   - topProductTypes -> affinities.productType
    //   - topSkus         -> affinities.<attribute> (SKU attributes are flattened)
    // In all cases, the affinity "name" becomes the boost "value" and the affinity
    // "score" becomes the boost "weight".
    function mapAffinitiesToPersonalizationBoost(affinities) {
      if (!affinities) {
        return []
      }
    
      const boost = []
    
      const directMappings = [
        { source: affinities.topBrands, field: "affinities.brand" },
        { source: affinities.topCategories, field: "affinities.categories" },
        { source: affinities.topProductTypes, field: "affinities.productType" },
      ]
      for (const { source, field } of directMappings) {
        for (const { name, score } of source ?? []) {
          boost.push({ field, value: [name], weight: score })
        }
      }
    
      // SKU affinities are flattened: each attribute becomes its own field, e.g.
      // the "size" attribute becomes the field "affinities.size".
      for (const sku of affinities.topSkus ?? []) {
        for (const { name, score } of sku.values ?? []) {
          boost.push({ field: `affinities.${sku.attribute}`, value: [name], weight: score })
        }
      }
    
      return boost
    }
    
    async function fetchPersonalization() {
      const result = await graphql(config.platformGraphqlUrl, `
        query ($sessionId: String!) {
          session(by: BY_CID, id: $sessionId) {
            segments {
              id
            }
            affinities {
              topBrands {
                name
                score
              }
              topCategories {
                name
                score
              }
              topProductTypes {
                name
                score
              }
              topSkus {
                attribute
                values {
                  name
                  score
                }
              }
            }
          }
        }`, { sessionId: store.sessionId })
    
      return {
        segments: result.session.segments.map(segment => segment.id),
        products: {
          personalizationBoost: mapAffinitiesToPersonalizationBoost(result.session.affinities)
        }
      }
    }
    
    function transformSearchResultsToTrackingMetadata(query, searchResults, isAutoComplete, isOrganic) {
      return {
        hasResults: searchResults.search.products.total > 0,
        autoComplete: isAutoComplete,
        autoCorrect: searchResults.search.products.fuzzy,
        keyword: false, // Use true if keywords were requested.
        organic: isOrganic,
        refined: !!store.mostRecentSearchMetadata?.query &&
          store.mostRecentSearchMetadata?.query !== query,
        sorted: false,
        query,
        refinedQuery: store.mostRecentSearchMetadata?.query ?? null,
        resultId: crypto.randomUUID(),
        searchType: searchResults.search.products.searchType
      }
    }
    
    async function search(query, isAutoComplete = false, isOrganic = true) {
      // Before doing anything, ensure that the session is current. Create a new
      // one if not.
      await createSessionIfMissingOrExpired()
    
      // Retrieve an up-to-date list of personalization parameters for the user in this session.
      // Segments are important to support segment-aware merchandising rules that might be
      // associated with A/B tests.
      // Keep in mind that user behavior changes segments during the session!
      // If caching is used, use short lifetimes.
      const sessionParams = await fetchPersonalization()
    
      const searchResults = await graphql(config.searchGraphqlUrl, `
        query (
          $accountId: String!,
          $query: String!,
          $abTests: [InputSearchABTest!]
          $sessionParams: InputSearchQuery
        ) {
          search(accountId: $accountId, query: $query, abTests: $abTests, sessionParams: $sessionParams) {
            products {
              total
              fuzzy
              searchType
              hits {
                productId
                name
              }
            }
            abTests {
              id
              activeVariation {
                id
              }
            }
          }
        }`,
        {
          accountId: config.merchantId,
          query,
          // Segments and personalization affinities both travel in sessionParams.
          sessionParams,
          // Include previously stored A/B variation assignments in search
          // requests to ensure consistent results within the session.
          abTests: store.abTests
        })
    
      // Add returned A/B variations to memory for use in the next search request.
      // Note that the search API only returns A/B variation assignments relevant
      // to the current request, so the stored assignments need to be accumulated
      // rather than being overwritten.
      store.abTests.push(...searchResults.search.abTests)
    
      // Store response metadata for impression- and click tracking.
      store.mostRecentSearchMetadata = transformSearchResultsToTrackingMetadata(
        query, searchResults, isAutoComplete, isOrganic)
    
      // The most recent search request's A/B variations are used to attribute
      // interactions with the result to that A/B test.
      store.mostRecentABVariations = searchResults.search.abTests.map(
        abTest => ({ key: abTest.id, value: abTest.activeVariation.id })
      )
    
      return searchResults.search
    }
    
    async function trackSearchImpression(productIds, page) {
      await track({
        searchImpression: {
          metadata: store.mostRecentSearchMetadata,
          page,
          productIds,
          properties: {
            // This part ensures that the search impression is attributed
            // to the currently active A/B test(s) for this session.
            abTestAttribution: store.mostRecentABVariations
          }
        }
      })
    }
    
    async function trackSearchClick(productId) {
      await track({
        searchClick: {
          metadata: store.mostRecentSearchMetadata,
          productId, // Clicked product's ID.
          properties: {
            // This part ensures that the search impression is attributed
            // to the currently active A/B test(s) for this session.
            abTestAttribution: store.mostRecentABVariations
          }
        }
      })
    }
    
    // Session takes place in the following block.
    (async () => {
      // 1. Search for something (implicitly creates a new session if needed).
      const searchResults = await search("<your search query>")
      // 2. Display results.
      console.log(searchResults.products.hits.map(hit => hit.name))
      // 3. Track impression with current 1-based pagination information.
      await trackSearchImpression(
        searchResults.products.hits.map(hit => hit.productId),
        1
      )
    
      // 4. User now looks at search results and may or may not interact.
      // If a result is clicked, track the ID of the clicked product:
      await trackSearchClick(searchResults.products.hits[0].productId)
      await trackSearchClick(searchResults.products.hits[2].productId)
    })()

    Keeping track of assigned A/B test variations

    Search API

    Session management and tracking API

    This API requires authentication using an API token with scope API_APPS. Learn more about the authentication workflow .

    Using external session IDs instead of Nosto-generated session IDs

    When using non-Nosto session IDs, it is no less important to maintain limited 30-minute session durations. This includes deleting stored A/B test variation assignments at the end of the session.

    Mutation newSession

    Request example

    Response example

    Query session

    Request example

    Response example

    Mutation recordAnalyticsEvent

    Search tracking metadata

    Category tracking metadata

    A/B testing properties

    Impression tracking request example

    Click tracking request example

    Putting it all together

    here
    Diagram of which A/B variations to store and include in search requests.

    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: true,
        formCssSelector: '#search-form',
        formUnbindDelay: 1000, // 1 second
        serpUrlMapping: {
            query: 'q',
        },
        serpQuery: {
            products: {
                size: 20,
                from: 0,
            },
        },
    })

    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

    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.

    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.

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

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

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

    Description
    Example

    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

    The supported sizes are

    Code
    Description

    The same mapping will also be attempted for SKU level data

    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:

    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

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

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

    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.

    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)

    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

    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.

    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.

    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.

    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 .

    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.

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

    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:

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

    Component parameters:

    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.

    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.

    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.

    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.

    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.

    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.

    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,

    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

    alternateImageHashes for alternateImageUrls thumbnails
  • sku.imageHash for sku.imageUrl thumbnails

  • 4

    50x50 px

    5

    30x30 px

    6

    100x140 px

    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

    1

    170x170 px

    2

    100x100 px

    3

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

    Search page redirect

    Checkout our API documentation on serpPathRedirect

    Unbinding existing search input

    Serp component

    Automatic URL Parameter Compression

    Checkout our API documentation on compressUrlParameters

    Product thumbnails

    Checkout our API documentation on thumbnailDecorator

    Currency formatting

    Checkout our API documentation on priceDecorator

    Multi-Currency

    Price variations example

    Exchange rates example

    Query parameter mapping

    Features

    Faceted navigation

    Stats facet

    Checkout our API documentation for useRangeSlider hook

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

    Checkout our API documentation on useRangeSelector

    Terms facet

    Checkout our API documentation on useActions hook

    Pagination

    Checkout our API documentation on usePagination hook

    Infinite Scroll

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

    Checkout our API documentation on InfiniteScroll

    Persistent Search Cache

    Checkout our API documentation on persistentSearchCache

    Product actions

    Handling native results

    Analytics

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

    Checkout our API documentation on SerpElement

    Fallback Functionality

    Enabling Fallback

    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: Customizing Fallback Location

    Alternative Fallback Behavior

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

    Customizing Fallback Location

    SERP Fallback

    Category Fallback

    Search engine configuration

    price variations
    here
    Enabling multi-currency from the admin
    stats facet
    terms facet
    Intersection Observer API
    options
    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}`);
        },
    });