Оформление
Событие onProductRecommendations
Когда размещение товарных рекомендаций работает в режиме Передавать данные через JavaScript, скрипт Sales Ninja не рисует карточки сам. Он передаёт товары и состояние в обработчик. Витриной владеет сайт.
Этот метод нужен только для такого размещения. Если блок вставляет Sales Ninja, подписка не требуется — см. размещение.
Сначала очередь ninja(...) должна получить init и start. Код проекта копируйте из Настройки проекта → Установка на сайт.
html
<script>
ninja('init', '<код проекта>');
ninja('start');
</script>onProductRecommendations можно вызвать до start: вызов встанет в очередь и выполнится после загрузки скрипта. Если вызвать его до init, вернётся ошибка Run init before onProductRecommendations.
Подписка
js
const unsubscribe = await ninja('onProductRecommendations', {
placementId: '11111111-1111-1111-1111-111111111111',
handler: ({ status, items, trackClick }) => {
}
})| Поле | Обязательно | Назначение |
|---|---|---|
handler | да | Функция, которая получает каждый результат |
placementId | нет | Идентификатор размещения из кабинета. Без него обработчик получает все JavaScript-размещения |
timeout | нет | Миллисекунды ожидания первого результата, если задан onTimeoutExceeded. По умолчанию 5000 |
onTimeoutExceeded | нет | Вызывается один раз, если за отведённое время не пришёл ни один результат |
Метод возвращает Promise с функцией отписки. Вызовите её, когда компонент окончательно размонтирован.
Идентификатор размещения появляется после первого сохранения. До сохранения в кабинете показан пример без конкретного идентификатора.
Если подписка создана уже после ответа, обработчик сразу получает последний актуальный результат этого размещения.
Состояния
handler получает один объект.
status | Когда приходит | Что делать |
|---|---|---|
available | Есть товары для этого размещения | Нарисовать карточки из items |
cleared | Страница больше не подходит, сменился маршрут SPA или размещение исчезло из ответа | Удалить прежний блок. items будет пустым |
При переходах внутри SPA событие приходит повторно. Не копируйте карточки поверх старых: заменяйте содержимое контейнера целиком.
Пример реализации
Подставьте свой placementId и контейнер. Стили карточек задаёт сайт.
js
const root = document.querySelector('#recommendations')
const unsubscribe = await ninja('onProductRecommendations', {
placementId: '11111111-1111-1111-1111-111111111111',
handler: ({ status, items, trackClick }) => {
if (!root) return
if (status === 'cleared') {
root.replaceChildren()
return
}
root.replaceChildren(
...items.map((item, itemIndex) => {
const card = document.createElement('a')
card.href = item.linkUrl || '#'
card.addEventListener('click', () => trackClick({ itemIndex }))
if (item.imageUrl) {
const image = document.createElement('img')
image.src = item.imageUrl
image.alt = item.title || ''
card.append(image)
}
const title = document.createElement('span')
title.textContent = item.title || ''
card.append(title)
if (item.priceAmount != null) {
const price = document.createElement('span')
price.textContent = `${item.priceAmount} ${item.priceCurrency || ''}`
card.append(price)
}
return card
})
)
}
})Данные для карточки
Эти поля нужны в первую очередь, чтобы показать товар.
json
{
"projectProductId": "22222222-2222-2222-2222-222222222222",
"title": "Кроссовки беговые Flow Runner",
"linkUrl": "https://shop.example/catalog/flow-runner",
"imageUrl": "https://cdn.example/images/flow-runner.jpg",
"priceAmount": 7490,
"priceCurrency": "RUB"
}| Поле | Смысл |
|---|---|
projectProductId | Идентификатор товара для передачи клика через trackClick |
title | Название |
linkUrl | Ссылка на карточку товара |
imageUrl | Изображение |
priceAmount | Цена |
priceCurrency | Валюта, например RUB |
Поле может отсутствовать, если его нет в каталоге. Не опирайтесь на то, что каждое значение всегда заполнено.
Что ещё можно взять
При необходимости в товаре есть бренд, старая цена и наличие. В самом ответе — статус, идентификатор размещения и настройки внешнего вида, если рисуете блок по ним. Sales Ninja эти настройки сам не применяет.
| Поле | Смысл |
|---|---|
status | available — есть товары, cleared — блок нужно убрать |
brand | Бренд |
oldPriceAmount | Старая цена, если есть скидка |
availability | Нормализованное наличие, например inStock |
externalItemId | Идентификатор товара в каталоге магазина |
presentation.settings | Настройки внешнего вида из кабинета, если они нужны сайту |
Служебные идентификаторы (feedItemId, sourceRecommendationId, dynamicModelVersionId, score) нужны только если связываете карточку со своей аналитикой.
Таймаут
Если витрина должна скрыть запасной блок, когда рекомендации не пришли, задайте timeout и onTimeoutExceeded. Таймаут относится к вашей подписке и не доказывает, что размещение сломано: страница могла не подойти под условия или фильтр исключил все товары.
js
await ninja('onProductRecommendations', {
placementId: '11111111-1111-1111-1111-111111111111',
handler: (payload) => { /* ... */ },
timeout: 2000,
onTimeoutExceeded: () => {
document.querySelector('#recommendations-fallback')?.removeAttribute('hidden')
},
})Показы и клики
Выдача непустого списка уже учитывается как показ размещения в сессии — в том числе в режиме JavaScript. Это не подтверждение видимости отдельных карточек. Повторная выдача того же размещения в одной сессии не добавляет ещё один такой показ. Отдельно передавать показ не нужно.
Для учёта кликов handler получает функцию trackClick. Вызывайте её в обработчике реального клика одним из способов:
js
trackClick({ itemIndex: 0 })
// Альтернатива для того же товара, а не второй вызов на один клик:
trackClick({ projectProductId: items[0].projectProductId })Передавайте ровно одно поле. itemIndex — индекс от нуля в исходном items. Если фильтруете или переставляете карточки, сохраняйте исходный индекс либо передавайте projectProductId из товара этой выдачи. Это не externalItemId из каталога магазина. Неизвестный или неоднозначный идентификатор, неверный индекс и оба поля одновременно не создают событие.
trackClick возвращает undefined, не требует await и не выполняет переход. Каждый вызов учитывается как отдельный клик. Ошибка отправки не прерывает работу сайта; доставка события при закрытии страницы или сетевом сбое не гарантируется. Клик не подтверждает загрузку страницы назначения.
Используйте функцию из последнего результата: после замены или очистки выдачи, смены страницы или сессии и отписки прежняя функция ничего не отправляет. В состоянии cleared вызов также ничего не отправляет.
Если установленная версия скрипта ещё не передаёт trackClick, обновите её перед подключением учёта кликов. Прежние подписки без этого метода продолжают работать. В готовом блоке клики и видимость карточек учитываются автоматически. Цели, корзина и покупки передаются через обычную интеграцию сайта.
Ошибки
Аргументы проверяются до подписки. Неверные типы дают Invalid onProductRecommendations arguments. Вызов до init даёт Run init before onProductRecommendations. Ошибки внутри handler не обрывают остальные подписки: они пишутся в консоль.