Skip to main content
On a shop platform? Check the Shopify and Magento, Shopware, PrestaShop, BigCommerce and WooCommerce adapters first. Their platform-specific pages explain installation availability and which page, cart, customer and order context they supply automatically.
The embed is a JavaScript widget you add to your site with a single script tag. Wherever you put a placeholder element, it renders a strip of recommendations drawn from your catalogue. The same widget measures what it renders. Once a card has been on screen long enough to have been seen, it records an impression; when a visitor clicks one, it records that too. Those signals are what your models learn from, so rendering and measurement arrive together rather than as two separate pieces of work.
Nothing else is required to render a strip. The sections below cover styling, telling the widget about the basket and the customer, reporting sales, and configuring what each strip serves from the console.

Choosing between the embed and the SDK

Both serve the same recommendations from the same contexts. The embed suits a website where you want recommendations rendered for you. The SDK suits cases where you need full control of the markup, or where you are serving somewhere a browser script cannot run — a mobile app, an email, a server-rendered page you assemble yourself.

Get a publishable key

1

Open Website embed

In the console, go to Developers → Website embed.
2

List your origins

Add every origin that will render the widget, including staging. A key with no origins does nothing.
3

Copy the snippet

The console gives you the script tag with your key already in it. The key is shown once, when you create it.
4

Check it is live

The same page shows the origins we have received requests from, including any we turned away because they were not on your list.
A publishable key (nsl_pk_…) is readable by anyone who views your page source, and is built for that. Its permissions are limited to reading recommendations and reporting impressions: it cannot administer your workspace, read your catalogue, or create other credentials.
Never put a Client Secret or an nsl_ secret key in browser code. Those grant full access to your workspace. The publishable key is the only NeuronSearchLab credential intended for a browser.

Placeholders

A placeholder is an empty element that marks where a strip should render. auto is what removes the need for integration code: your catalogue is keyed on canonical URLs, so the page already carries the identifier for the item it is about. Placeholders added to the DOM after load are picked up automatically, so single-page applications work without re-initialising anything.

Styling

The strip renders inside a shadow root. Your stylesheet cannot reach into it, and its styles cannot leak out into your page. CSS custom properties cross that boundary, and are how you restyle it:

Choosing a script URL

Two URLs are published. A versioned URL supports Subresource Integrity, because its contents never change; a rolling URL updates itself, which is incompatible with a fixed integrity hash. Per-version integrity hashes are published with each release.

What the widget sends

  • A recommendation request carrying the page’s canonical URL and a random first-party visitor id. Your key travels as a header, never in the URL.
  • Impressions, once a card has been at least 50% visible for two seconds. Scrolling past quickly is not an impression, and scrolling back does not count a second one.
  • Clicks, delivered with sendBeacon so they survive the navigation. The widget does not intercept or delay the click itself.
It sets no cookies and uses no third-party storage. Visitor and session ids are random values held in first-party localStorage and sessionStorage. Where that storage is unavailable — private browsing, blocked site data — the widget still renders and simply does not recognise the visitor on the next page.

How an item is resolved

A related strip needs to know which catalogue item the page is about. The widget reads two things and sends both:
  • the canonical URL, from data-nsl-item-url="auto"
  • an identifier from the page’s JSON-LD: sku, mpn, productID or isbn
Whichever resolves against your catalogue wins, in order of how much the identifier is trusted: one your feed asserted, then one the crawler scraped, then the URL. Reading the identifier matters on sites where the canonical URL a visitor’s browser sees differs from the one we crawled — locale prefixes, per-market canonicalisation and AMP copies all cause this. Where the URL does not match, the page’s own sku still resolves the item, with no change to your markup. You never send NeuronSearchLab’s identifiers. To override what the page says, set data-nsl-item-sku or a literal data-nsl-item-url.

Debugging an empty strip

Add ?nsl_debug=1 to any page carrying the widget. A panel lists every placeholder, what it resolved to, the request id, how many items came back and how many events were sent, and names the cause when something is wrong:
  • Request failed — the key is not valid for this origin. Check the origin list in the console.
  • This page is not in your catalogue — neither the URL nor the sku matched an item, so there is nothing for the strip to be related to.
  • No request_id — the strip rendered, but views and clicks are not being recorded.
  • No context to serve from — the workspace has no contexts. Create one under Contexts.
data-nsl-debug on the script tag enables the panel permanently. The query parameter is usually more practical: the script tag is normally baked into a theme, and the parameter needs no deploy.

Page context

The crawler reads your product pages, but it cannot see a basket, it does not know whether anyone is signed in, and it never reaches a thank-you page — those are noindex and are skipped. Your page can supply that context through a command queue:
Each command replaces the previous value for that slot rather than adding to it, so calling nsl('cart', …) twice describes one basket. Commands and JSON-LD can be mixed freely — there is no mode to select between them. Where you cannot run script, one element does the same job:
Context resolves against your catalogue or it is dropped and counted. It can never create an item, which is what keeps a key published in page source from being a route into your catalogue.

Category and search pages

A listing page says what it is listing, and a results page says what was searched:
category filters on the category field of your item metadata. Catalogues disagree about what that field is called, so a value may carry its own field name: topic:Health filters on topic, while a bare Health filters on category. query turns the request into query-driven retrieval. If the workspace has no query embedder, the request is retried without it, so the strip still renders.
These scope a single strip from the page. To scope every strip on a surface, put the filter on the context the placement points at — one context can serve many placements.

Conversions

Sales reported this way arrive as the order_reported signal, never as purchase. A browser cannot prove that a sale happened, so the event type the embed creates for it has weight 0: it is for attribution and revenue reporting, not training. You can change that weight like any other, but a publishable key is readable from your page source, so anyone could report sales against it. Order lines are keyed on the order id, so a reloaded thank-you page cannot count a sale twice. An order id is therefore required — without one there is nothing to deduplicate on. If you also connect Shopify, the verified order replaces the browser’s report in place rather than adding a second record.

Placements

A placement is a name you give a strip. What that strip serves then becomes a console setting rather than a markup change:
In Developers → Website embed, a placement sets the context, the item count, the layout (grid or list), which fields appear and an optional heading. Click-through for each placement is then reported without further instrumentation. A placement the console does not recognise still renders, using your workspace defaults, so renaming one does not blank a live page.

Which context serves a strip

Every strip is served by a context, which is what decides the recipe, the pipeline, the rules and the filters behind it. The context is chosen in this order:
  1. The placement’s context, if the strip names a placement that has one.
  2. Your workspace’s default context.
So a strip with no placement is served by the default context — which means that until you create placements, every strip on your site behaves the same way. Giving two surfaces different behaviour is what placements are for. Set the default under Contexts; it starts as the Homepage context created with your workspace. A workspace with no contexts at all cannot serve the embed, and the debug panel says so rather than rendering an empty strip.

Events

The embed never sends an event name. It reports signals, and your workspace decides which event ID each one lands on: Creating an embed key creates an event type for each of these — an integer ID, a label and a weight — and binds the signal to it. view uses the View event your workspace already has. The event types count toward your plan’s event-type limit like any other; the Free plan has room for the defaults plus the embed. These are ordinary event types. Rename them, change their weights, and choose which ones a training template uses on the Events page. To send a signal to an event you already track — say your server already records product views — open Events → Integration signals and pick that event. Nothing is ever matched by name, so a workspace whose events are called something else entirely works the same way. Recommendation impressions are kept apart from view on purpose. An item the model chose to show is much weaker evidence of interest than someone opening it, and counting the two as one event teaches the model to agree with itself.

What a browser cannot send

The embed cannot send purchase or any event ID directly. Send verified conversions from your server with the Events API, or import them with a backfill. Strip events are checked against the recommendations actually served, so a key copied from your page source cannot report engagement for arbitrary items. view and add_to_cart must come from an allowlisted origin, name an item in your catalogue, and count once per visitor, item and session. Neither is sent while nsl('consent', false) is in effect. Embed events go through the same pipeline as the Events API, so they are metered, stitched to identities and reflected in the next recommendation the same way.

Quota

Embed traffic is metered separately from API requests, because a strip makes one request per pageview. When the allowance is exhausted the widget continues to render, serving cached results, and the response carries X-NSL-Quota: exceeded. It does not begin failing on your live site.

Failure behaviour

Every failure path renders nothing and leaves the page as it was. The script sits in your critical render path, so it does not throw, does not block, and does not surface errors to your visitors. It is under 4 kB gzipped.