# Widget Embed Guide

BasketBooster ships three storefront widgets as standard web components:
`<product-recs>` (recommendations), `<product-search>` (instant search with
image search), and `<shop-the-look>` (shoppable images). They work on any
platform — plain HTML, WooCommerce templates, headless frontends.

All widgets need your **publishable key** (`pk_...`, from the dashboard's
*Recommender → Widget snippet* page). It is safe in public HTML.

## Loading the scripts

Each widget family has its own script, served per store domain (the domain in
the URL is for caching only — your identity comes from the key):

```html
<!-- Recommendations -->
<script src="https://api.basketbooster.eu/widget/v1/your-store.com.js" async></script>
<!-- Search -->
<script src="https://api.basketbooster.eu/widget/v1/search-your-store.com.js" async></script>
<!-- Shop the Look -->
<script src="https://api.basketbooster.eu/widget/v1/look-your-store.com.js" async></script>
```

Load only the ones you use. The scripts are small, dependency-free, and safe to
load `async`.

## `<product-recs>` — recommendations

### Product page ("You may also like")

```html
<product-recs data-key="pk_YOUR_KEY" data-item-id="123"></product-recs>
```

### Cart page ("Complete the cart")

Pass everything already in the basket — the engine suggests items that complete
the set:

```html
<product-recs data-key="pk_YOUR_KEY" data-item-ids="123,456,789"></product-recs>
```

### Frequently Bought Together

Trained on real orders only — this widget stays short or empty rather than
padding with generic suggestions:

```html
<product-recs data-key="pk_YOUR_KEY" data-item-id="123" data-source="buy"
              data-title="Frequently bought together"></product-recs>
```

### Visually similar products

Matches products by how their photos look (image embeddings of your catalog
photos — no behavioural data needed, so it works from day one). Requires
product images and an indexed search catalog; backfills with same-category
items otherwise. Can be turned off on the dashboard Merchandising page:

```html
<product-recs data-key="pk_YOUR_KEY" data-item-id="123" data-source="similar"
              data-title="Similar products"></product-recs>
```

### Trending now (homepage, empty cart, collections)

```html
<product-recs data-key="pk_YOUR_KEY" data-source="trending"></product-recs>
```

### All attributes

| Attribute | Meaning |
|---|---|
| `data-key` | **Required.** Your publishable key. |
| `data-item-id` | Anchor product id (product page). |
| `data-item-ids` | Comma-separated basket ids (cart page). |
| `data-user-id` / `data-customer-id` | Optional shopper identity for personalised results. Anonymous shoppers are tracked by an automatic session id. |
| `data-source` | `mix` (default), `buy` (frequently-bought-together), `browse` (personal history), `similar` (visually similar by product photo), `trending`. |
| `data-num` | How many products (default from your dashboard config, max 20). |
| `data-title` | Heading text (defaults to "You may also like" / "Trending now"). |
| `data-layout` | `grid` or `carousel` (overrides the dashboard setting per placement). |
| `data-currency` | Currency symbol for prices. |
| `data-api` | Override the API base URL (rarely needed — it is baked into the script). |

## `<product-search>` — instant search

Typo-tolerant, as-you-type search with an optional camera button for
search-by-photo:

```html
<product-search data-key="pk_YOUR_KEY" data-num="12"
                data-placeholder="Search products…"
                data-image="true"></product-search>
```

`data-image="true"` enables the photo-upload button (visually similar product
matching). Photos are analyzed on the fly and never stored.

Ranking and matching are configurable from the dashboard's
**Search → Settings** page — field weights, synonyms, Latin↔Cyrillic matching,
and AI (semantic) search. See [search-settings](search-settings.md).

### In-page results (grid instead of dropdown)

By default results appear in a dropdown under the input. For a search results
*page*, render them as an in-flow product grid instead:

```html
<product-search data-key="pk_YOUR_KEY" data-image="true"
                data-display="page" data-num="24"></product-search>
```

Or render the grid into any element of your page — a plain `<div>` or the
provided `<product-search-results>` container:

```html
<product-search data-key="pk_YOUR_KEY" data-image="true"
                data-results-target="#bb-results"></product-search>
...
<product-search-results id="bb-results"></product-search-results>
```

The grid shows results in batches of 12 with a **Show more** button (up to 50
per search). Column width is the "Grid column width" setting in the widget
designer.

### Add image search to your existing search bar

Already have a search you're happy with? Embed only the camera button inside
(or next to) your own search bar and point it at a results container anywhere
in the page:

```html
<form class="my-search">
  <input type="search" name="q" placeholder="Search…">
  <product-search data-key="pk_YOUR_KEY" data-icon-only="true"
                  data-results-target="#bb-image-results"></product-search>
</form>

<product-search-results id="bb-image-results"></product-search-results>
```

`data-icon-only="true"` renders just the 📷 button — no input, no styling around
it — so it drops into any existing markup. Image search is always on in this
mode. If the results target is missing, matches fall back to a small dropdown
anchored to the icon.

### All attributes

| Attribute | Meaning |
|---|---|
| `data-key` | **Required.** Your publishable key. |
| `data-num` | How many results (default 12 in dropdown mode, 24 in page mode, max 50). |
| `data-placeholder` | Input placeholder text (default "Search products…"). |
| `data-image` | `"true"` adds the search-by-photo camera button. |
| `data-display` | `"page"` renders results as an in-flow grid below the input instead of a dropdown. |
| `data-results-target` | CSS selector of the element to render the results grid into. |
| `data-icon-only` | `"true"` renders only the camera button, for embedding in your own search bar. |
| `data-currency` | Currency symbol for prices (dashboard setting wins when configured). |
| `data-api` | Override the API base URL (rarely needed — it is baked into the script). |

## `<shop-the-look>` — shoppable images

Shows the published Look for a product (created and published from your
dashboard's Shop-the-Look page):

```html
<shop-the-look data-key="pk_YOUR_KEY" data-item-id="123"></shop-the-look>
```

Renders nothing if no published look exists for that product, so it is safe to
place on every product page.

## Conversion tracking

Views and clicks on recommendations are tracked by the widgets automatically.
**Add-to-carts and purchases are not** — the widgets can't see your cart button
or your checkout, so your storefront must report those two events itself.
(The WooCommerce plugin does both for you; this section is for custom
storefronts.)

**Add-to-cart** — call from your add-to-cart handler, after the add succeeds:

```html
<script>
  ProductRecs.track('add-to-cart', {
    item_id: '123',
    user_id: 'CUSTOMER_ID',  // logged-in customer id — omit for guests
    value: 49.9,             // line value (unit price × quantity)
    currency: 'EUR'
  });
</script>
```

Alternatively, configure a custom DOM event name under the dashboard's widget
"Storefront events" and dispatch it instead:
`document.dispatchEvent(new CustomEvent('my:add-to-cart', {detail: {item_id: '123', value: 49.9}}))`.

**Purchase** — fire from the order-confirmation page, once per line item:

```html
<script>
  ProductRecs.track('buy', {
    item_id: '123',
    order_id: 'ORDER_1234',  // groups items bought together — feeds the FBT model
    user_id: 'CUSTOMER_ID',  // optional
    value: 49.9,
    currency: 'EUR'
  });
</script>
```

…or sync the whole order server-side with `POST /v1/orders` (see the
[API reference](v1-api-reference.md)). `ProductRecs` is available after the
recommendations script loads.

### Attribution: keep the shopper id consistent

The Impact dashboard credits an add-to-cart or sale to the recommender only when
the commerce event and an earlier rec click/impression carry the **same shopper
identity and product id** (30-day window). Identity is `user_id` when present,
otherwise the widget's automatic session id — and `user_id` always wins when both
are sent. So:

- **Logged-in shoppers**: pass the customer id everywhere — `data-user-id` on
  every widget embed, `user_id` on your `add-to-cart`/`buy` calls, and `user_id`
  on `POST /v1/orders`. If the widgets run anonymous while orders carry a
  customer id, nothing matches and attribution stays at zero.
- **Anonymous shoppers**: the widget keeps its session id in
  `localStorage["pr_sid"]`. Browser-side `ProductRecs.track` calls pick it up
  automatically. For server-side order sync, mirror it into a first-party cookie
  and forward it as `session_id`:

```html
<script>
  try {
    var sid = localStorage.getItem('pr_sid');
    if (sid) document.cookie = 'pr_sid=' + encodeURIComponent(sid) +
      '; path=/; max-age=15552000; SameSite=Lax';
  } catch (e) {}
</script>
```

## Appearance

Fonts, colors, buttons, and layout come from your dashboard's widget designer
and are fetched live by the widget — one source of truth, changes are instant.
Custom CSS can be added in the designer, or override the widget's classes in
your own stylesheet.

### Google Analytics

If your store runs GA4 (`gtag`), you can enable the analytics passthrough in
the dashboard: widget interactions are mirrored as standard GA4 events tagged
`item_list_id="basketbooster"`, so results show up in your own reports.

## Gotchas

- **HTTPS stores need the HTTPS API base** — browsers block `http://` calls
  from `https://` pages.
- **Origin allowlist** — a publishable key with an empty allowlist accepts any
  origin. If you set an allowlist in the dashboard, include every domain the
  widget runs on. See [publishable-keys-and-cors](publishable-keys-and-cors.md).
- The widget renders nothing (not an error) when the key is missing or no
  anchor is provided — check the browser console and attributes first.
