# Smart Sorting and Reranking with the SDK

> Current Froomle documentation · [View canonical page](https://docs.froomle.com/froomle-doc-main/v2/js_lib/smart-sorting-reranking.html)

Smart sorting lets you pass an existing candidate set, such as search results or an editorial list, and ask Froomle to return those candidates in personalized order. The SDK uses the recommendation API `list_content` field for this.

Use this SDK page when you want browser-side integration details. For the product/API concept, see [Smart Sorting & Reranking](https://docs.froomle.com/froomle-doc-main/v2/guide/topics/smart-sorting-reranking.html).

<a id="SmartSortWhenToUse"></a>

## When to Use It

Use smart sorting when:

-   your application already owns the candidate set,
    
-   Froomle should rank that set for the current visitor,
    
-   you still want normal recommendation attribution where the SDK owns or can stamp rendered recommendation metadata.
    

Typical examples:

-   reranking search results that are already returned by a commerce/search backend,
    
-   reranking editorially selected article lists,
    
-   pinning sponsored or editorial items while personalizing the remaining positions.
    

> **IMPORTANT:**
> 
> Smart sorting is list-based. It is not the same as slot-based `proxyReco(…​)`, `useCreateReco(…​)`, or repeated `data-froomle-reco` placeholders. A smart-sort request sends one list with candidate items in `list_content` and receives one sorted list back.

<a id="SmartSortSharedModel"></a>

## Shared SDK Model

All SDK smart-sorting integrations send a recommendation list with `list_content`. The behavior depends on whether candidates have `rank`.

| Shape | SDK input | Expected behavior |
| --- | --- | --- |
| Smart sorting request | Candidates without `rank`. | Froomle reranks the supplied candidates. It does not fill missing positions with unrelated recommendations. |
| Request with fixed-rank items | Candidates where every item has `rank` / `data-froomle-rank` / `getRank(…​)`. | Froomle pins those items and may fill the remaining positions with normal recommendations. |
| Combined request | A mix of ranked and unranked candidates in the same list. | Ranked candidates are pinned. Unranked candidates are reranked into open positions. Do not rely on unrelated fill when free candidates are present. |

Plain smart sorting request

```json
{
  "list_name": "search_results_rerank",
  "list_size": 3,
  "limit": 3,
  "list_content": [
    { "id": "sku-1", "item_type": "product" },
    { "id": "sku-2", "item_type": "product" },
    { "id": "sku-3", "item_type": "product" }
  ]
}
```

Fixed-rank request

```json
{
  "list_name": "curated_feed",
  "list_size": 6,
  "limit": 6,
  "list_content": [
    { "id": "promo-article", "item_type": "article", "rank": 1 },
    { "id": "sponsored-item", "item_type": "article", "rank": 4 }
  ]
}
```

Combined request

```json
{
  "list_name": "search_results_rerank",
  "list_size": 4,
  "limit": 4,
  "list_content": [
    { "id": "sku-sponsored", "item_type": "product", "rank": 1 },
    { "id": "sku-1", "item_type": "product" },
    { "id": "sku-2", "item_type": "product" },
    { "id": "sku-3", "item_type": "product" }
  ]
}
```

Candidate fields:

-   `id`: required candidate item ID.
    
-   `item_type`: item type. The SDK defaults to `article` when no type is provided.
    
-   `rank`: optional 1-based fixed-rank position.
    

The returned recommendation items follow the normal recommendation response model. The SDK matches returned items back to candidates by `item_type` + `id`.

<a id="SmartSortIntegrationOrder"></a>

## Integration Methods

The SDK docs use the same ordering throughout:

1.  Declarative DOM / script-tag.
    
2.  Module/import setup with declarative DOM.
    
3.  Programmatic JS/TS.
    
4.  React bindings.
    

Choose the first method that matches your rendering ownership.

<a id="SmartSortDeclarative"></a>

## Declarative DOM / Script-Tag

Use this when the final browser DOM already contains the candidate elements and the SDK may reorder those elements directly. This works for plain HTML, SSR templates, JSP, Twig, ArcXP static output types, and similar DOM-first pages.

Markup contract:

-   Put `data-froomle-smart-sort="list_name"` on the direct container.
    
-   Mark direct child candidates with `data-froomle-smart-sort-item`.
    
-   Put the candidate ID on each child with `data-froomle-id`.
    
-   Set `data-froomle-item-type` on the container or child when the item type is not `article`.
    
-   Optionally set `data-froomle-rank` on a candidate child for fixed-rank placement.
    
-   Optionally set `data-froomle-limit` and `data-froomle-list-size` on the container.
    

```html
<script
  src="https://cdn.example.com/froomle.global.js"
  data-froomle-env="demo_shop"
  data-froomle-page-visit="search"
  data-froomle-consent="2"
  defer
></script>

<ul
  data-froomle-smart-sort="search_results_rerank"
  data-froomle-item-type="product"
  data-froomle-list-size="3"
  data-froomle-limit="3"
>
  <li data-froomle-smart-sort-item data-froomle-id="sku-1">...</li>
  <li data-froomle-smart-sort-item data-froomle-id="sku-2" data-froomle-rank="1">...</li>
  <li data-froomle-smart-sort-item data-froomle-id="sku-3">...</li>
</ul>
```

Runtime behavior:

-   The SDK reads only direct child candidates.
    
-   The SDK sends those candidates as `list_content`.
    
-   A child with `data-froomle-rank` becomes a fixed-rank item; children without it are free candidates for reranking.
    
-   Matched returned items are moved into returned order.
    
-   Unmatched candidates are kept after matched candidates in original order.
    
-   Matched candidates are stamped with `data-froomle-reco`, `data-froomle-request-id`, `data-froomle-item-type`, and returned user-group metadata where available.
    
-   Automatic `impression` and `click_on_recommendation` tracking can work on stamped candidates, following the same rules as normal SDK-owned DOM recommendation elements.
    

Benchmarking and exclusions:

-   In a Froomle/treatment branch, matched candidates move into the returned Froomle order.
    
-   In a customer/control branch, the SDK keeps the original customer order and stamps matched candidates with the benchmark `request_id` and control `user_group`.
    
-   Global auto-exclusions and manual SDK exclusions are sent with the smart-sort request. The current smart-sort placement is excluded from automatic page collection so the candidate list does not exclude itself.
    
-   Unmatched fallback candidates remain visible but are not Froomle-attributed unless they receive valid smart-sort metadata.
    

Customer-managed smart sorting with page exclusions

```html
<ul
  data-froomle-smart-sort="search_results_rerank"
  data-froomle-item-type="product"
  data-froomle-benchmark-mode="customer-managed"
  data-froomle-benchmark-current-group="customer"
  data-froomle-benchmark-control-group="customer"
  data-froomle-benchmark-treatment-group="froomle"
>
  <li data-froomle-smart-sort-item data-froomle-id="sku-1">...</li>
  <li data-froomle-smart-sort-item data-froomle-id="sku-2">...</li>
</ul>
```

In the example above, the `customer` group keeps the input order and still receives request attribution on matched items. Change `data-froomle-benchmark-current-group` to the configured treatment group when Froomle should own the returned order.

Failure behavior:

-   If required page context is missing, the SDK leaves the customer DOM untouched.
    
-   If a request fails, the SDK restores the candidate DOM and fails open.
    
-   Candidates without usable `data-froomle-id` are skipped and reported in smart-sorting diagnostics warnings.
    

<a id="SmartSortModuleDom"></a>

## Module / JavaScript Setup with Declarative DOM

Use this when your app imports `@froomle/frontend-sdk` but still renders declarative smart-sort DOM. This is the same DOM contract as script-tag mode, but module/import integrations must start DOM processing explicitly with `init()`.

```ts
import {
  init,
  setConsent,
  setEnvironment,
  setPageVisit,
} from '@froomle/frontend-sdk'

setEnvironment('demo_shop')
setPageVisit('search')
setConsent(2)

await init()
```

```html
<ul data-froomle-smart-sort="search_results_rerank" data-froomle-item-type="product">
  <li data-froomle-smart-sort-item data-froomle-id="sku-1">...</li>
  <li data-froomle-smart-sort-item data-froomle-id="sku-2">...</li>
</ul>
```

Use this path when:

-   your build system owns SDK loading,
    
-   templates or server output still own the candidate markup,
    
-   you want the SDK to reorder/stamp that existing DOM.
    

Do not use this path inside a React-rendered subtree unless you intentionally run declarative DOM processing there. For React-owned UI, prefer [React bindings](https://docs.froomle.com/froomle-doc-main/v2/js_lib/smart-sorting-reranking.html#SmartSortReact).

<a id="SmartSortProgrammatic"></a>

## Programmatic JS/TS

Use `smartSort(…​)` when your code owns candidates and rendering. The helper normalizes candidates, sends `list_content`, and maps returned Froomle items back to the original candidates.

```ts
import {
  setEnvironment,
  setPageVisit,
  setConsent,
  smartSort,
} from '@froomle/frontend-sdk'

setEnvironment('demo_shop')
setPageVisit('search')
setConsent(2)

const result = await smartSort(
  {
    list_name: 'search_results_rerank',
    list_size: products.length,
    limit: products.length,
    candidates: products,
    getId: (product) => product.sku,
    getItemType: () => 'product',
    getRank: (product) => product.pinnedRank,
  },
  {
    benchmark: {
      mode: 'customer-managed',
      currentGroup: 'froomle',
      groups: { control: 'customer', treatment: 'froomle' },
    },
    exclusions: [
      { id: 'sku-already-visible', item_type: 'product' },
    ],
  },
)

for (const entry of result.entries) {
  renderProduct(entry.candidate, entry.item)
}
```

Candidate ID resolution order:

-   `getId(candidate, index)` when provided,
    
-   `candidate.id`,
    
-   `candidate.item_id`,
    
-   `candidate.itemId`,
    
-   string/number candidate values directly.
    

Candidate item type resolution order:

-   `getItemType(candidate, index)` when provided,
    
-   `candidate.item_type`,
    
-   `candidate.itemType`,
    
-   request-level `item_type` / `itemType`,
    
-   `article` fallback.
    

Fixed-rank resolution order:

-   `getRank(candidate, index)` when provided,
    
-   `candidate.rank`.
    

If no candidate has a rank, `smartSort(…​)` is a plain smart-sorting request. If every candidate has a rank, it is a fixed-rank request. If only some candidates have a rank, it is a combined request.

Return value:

-   `entries`: matched original candidate + returned Froomle item pairs.
    
-   `items`: returned Froomle recommendation items.
    
-   `sortedCandidates`: original candidates in Froomle response order.
    
-   `unmatchedCandidates`: original candidates not returned by Froomle.
    
-   `response` and `list`: raw response/list objects for inspection.
    

<a id="_paginate_a_programmatic_smart_sort"></a>

### Paginate a programmatic smart sort

When your application supplies the full candidate set, request only the first page with `limit` and enable page controls with `pagination: true`. Froomle ranks the full set before your application renders each page. The first result has `items`, `entries`, and `sortedCandidates` at the top level, with page controls in `result.pagination`. See [Programmatic Pagination with the SDK](https://docs.froomle.com/froomle-doc-main/v2/js_lib/programmatic-pagination.html) for a comparison with recommendation lists and rank-based slices.

```ts
const firstPage = await smartSort({
  list_name: 'search_results_rerank',
  candidates: allMatchingProducts,
  getId: (product) => product.sku,
  getItemType: () => 'product',
  limit: 24,
  pagination: true,
})

renderProducts(firstPage.sortedCandidates)

const secondPage = await firstPage.pagination.getPage(2)
renderProducts(secondPage.sortedCandidates)
```

The SDK sends all candidates as `list_content`, defaults `list_size` to the candidate count, and requests the first `limit` items. The initial result is page 1. `getPage(2)` uses the returned `list_key` to start at rank 25, and later pages keep the initial `limit` of 24. Each page result has its own `items`, `entries`, `sortedCandidates`, and `pagination`. Paginated results omit `unmatchedCandidates`, because candidates absent from one page may appear later.

`getListSlice({ listKey, offset, limit })` can also fetch a rank-based slice from this `smartSort` result, but it returns `Recommendations` rather than `entries` and `sortedCandidates` mapped to your candidates. Use `firstPage.pagination.getPage(pageNumber)` for the mapped result.

For a later request or a page reload, save `firstPage.pagination.listKey` while it is non-null and use `getListSlice` with the saved key:

```ts
import { getListSlice } from '@froomle/frontend-sdk'

const secondPage = await getListSlice({
  listKey: savedListKey,
  offset: 25,
  limit: 24,
})
const rankedProductIds = secondPage.lists()[0].items.map((item) => item.Id)
```

These IDs are in Froomle rank order. Match them to your product data before rendering. If the key has expired, call `smartSort` again with the full candidate set.

If the initial response has no `list_key`, `firstPage.pagination.listKey` is `null` and `getPage(2)` rejects; contact Froomle if your integration needs pagination. An unknown or expired non-empty key returns HTTP 404. Your application renders each page and tracks recommendations. Customer-managed benchmark control order is not supported with this pagination option.

Request options:

-   `benchmark`: applies the same benchmark request contract as `getRecommendations(…​)`. Customer/control keeps input order; treatment/Froomle uses returned order.
    
-   `exclusions`: sends request-local `histories.exclude` entries with `reconsumable: false`.
    
-   `autoExclusions`: scans configured page sources at request time.
    

> **NOTE:**
> 
> Programmatic rendering is customer-owned. If you render arbitrary custom DOM without SDK metadata, automatic recommendation impression/click tracking is not guaranteed. Either render equivalent attribution metadata into DOM, use manual `sendEvent(…​)`, or use React FroomleReco entry={entry} when rendering in React.
> 
> When `smartSort(…​)` is called with a customer-managed benchmark control group, the returned `sortedCandidates` and `entries` preserve the original customer order while still carrying matched response attribution. Use the returned `request_id` and `list_name` when sending manual impression/click events. Include `user_group` only for the externally assigned customer/control branch; omit it for customer-managed treatment/Froomle and for Froomle-managed assignment.

<a id="SmartSortRawRecommendations"></a>

### Raw `getRecommendations(…​)` with `list_content`

Use raw `getRecommendations(…​)` only when you deliberately want to own response mapping yourself.

```ts
const response = await getRecommendations([
  {
    list_name: 'search_results_rerank',
    list_size: products.length,
    limit: products.length,
    list_content: products.map((product) => ({
      id: product.sku,
      item_type: 'product',
      rank: product.pinnedRank,
    })),
  },
])
```

This keeps the request shape explicit, but unlike `smartSort(…​)`, it does not return `sortedCandidates`, `unmatchedCandidates`, or matched candidate entries.

<a id="SmartSortReact"></a>

## React Bindings

Use `useSmartSort(…​)` when a React component owns candidate data and rendering. Render returned entries through FroomleReco entry={entry} when you want automatic recommendation `impression` and `click_on_recommendation` tracking.

```tsx
import { FroomleReco, useSmartSort } from '@froomle/frontend-sdk/react'

function SearchResults({ products }) {
  const { entries, loading, unmatchedCandidates } = useSmartSort({
    listName: 'search_results_rerank',
    list_size: products.length,
    candidates: products,
    getId: (product) => product.sku,
    getItemType: () => 'product',
    getRank: (product) => product.pinnedRank,
  })

  if (loading) return null

  return (
    <>
      {entries.map((entry) => (
        <FroomleReco key={entry.itemId} entry={entry} asChild>
          <ProductCard product={entry.candidate} />
        </FroomleReco>
      ))}
      {unmatchedCandidates.map((product) => (
        <ProductCard key={product.sku} product={product} />
      ))}
    </>
  )
}
```

Return value:

-   `status`: `loading`, `success`, or `error`.
    
-   `loading`: boolean loading flag.
    
-   `error`: request error when status is `error`.
    
-   `entries`: renderable entries for FroomleReco entry={entry}.
    
-   `items`: returned Froomle recommendation items.
    
-   `sortedCandidates`: original candidates in Froomle response order.
    
-   `unmatchedCandidates`: original candidates not returned by Froomle.
    
-   `response` and `list`: raw response/list objects for inspection.
    

Request-shaping options:

-   `benchmark` follows the same benchmark config rules as `useRecoList(…​)`. In a customer/control branch, `useSmartSort(…​)` preserves the original candidate order and FroomleReco entry={entry} stamps the control `user_group`.
    
-   `exclusions`, `excludeItems`, and `autoExclusions` follow the same in-page deduplication rules as `useRecoList(…​)`. Use `excludeItems` when React already has the visible CMS/search-result objects in memory.
    

> **IMPORTANT:**
> 
> If you render `entries` without `FroomleReco`, the SDK no longer owns recommendation attribution metadata for those React nodes. Treat recommendation impressions and clicks as manual in that case.

<a id="SmartSortBackend"></a>

## Backend-Fetched or Server-Side Reranking

The browser frontend SDK does not run on the backend. For backend-side smart sorting, call the recommendation API directly with `list_content` from your server and then decide how the browser should track rendered recommendations.

Supported browser follow-up options:

-   Render returned metadata into DOM with `data-froomle-reco`, `data-froomle-id`, `data-froomle-request-id`, and optional `data-froomle-user-group`; then the DOM event layer can auto-track.
    
-   Render custom UI and send recommendation events manually with `sendEvent(…​)` including `action_item`, `action_item_type`, `list_name`, `request_id`, and `user_group` only for an externally assigned customer/control branch.
    
-   If backend benchmarking puts the visitor in customer/control, render the customer order but keep the returned benchmark attribution metadata for events.
    
-   In React, prefer browser-side `useSmartSort(…​)` if the candidate list is available client-side and automatic tracking is desired.
    

See also [Advanced Hybrid Pattern: Backend-Fetched Recommendations + SDK Event Tracking](https://docs.froomle.com/froomle-doc-main/v2/js_lib/jslib.html#HybridBackendTracking).

<a id="SmartSortDiagnostics"></a>

## Diagnostics

The browser runtime exposes the latest smart-sorting resolution under:

```javascript
window.FroomleFrontendSdkRuntime.diagnostics.smartSorting
```

Fields:

-   `lastResolvedAt`: timestamp of the last smart-sorting resolution.
    
-   `lastSurface`: `dom`, `programmatic`, or `react`.
    
-   `lastListName`: last smart-sorted list name.
    
-   `lastCandidateCount`: number of candidates sent.
    
-   `lastFixedRankCount`: number of candidates with fixed `rank`.
    
-   `lastResponseCount`: number of returned recommendation items.
    
-   `lastUnmatchedCount`: number of input candidates not returned by Froomle.
    
-   `warnings`: recent smart-sorting warnings, for example missing DOM candidate IDs.
    

<a id="SmartSortTroubleshooting"></a>

## Troubleshooting

| Symptom | Check |
| --- | --- |
| No request is sent | Verify shared SDK context: environment, page type, consent, and initialization. Module/import DOM integrations must call `init()`. |
| DOM candidates do not reorder | Verify the smart-sort container has `data-froomle-smart-sort`, and candidate elements are direct children with `data-froomle-smart-sort-item` and `data-froomle-id`. |
| Some candidates are missing from the sorted result | Check `unmatchedCandidates` or `diagnostics.smartSorting.lastUnmatchedCount`. Froomle can only return candidates it recognizes for that list/environment. |
| Recommendation impressions/clicks are missing | Verify the rendered item is SDK-stamped/SDK-rendered. In React, render through FroomleReco entry={entry}. In custom programmatic UI, either render equivalent DOM metadata or send events manually. |
| Product IDs do not match returned items | Check the ID and item-type adapters. Matching is done by `item_type` + `id`. |
