diff --git a/cspell.json b/cspell.json index 5757e960..be6d94e6 100644 --- a/cspell.json +++ b/cspell.json @@ -297,6 +297,55 @@ "securepay", "usn_income", "msp3tbank", + "bannerpro", + "bannerclick", + "bannerimpression", + "bannerproImpressionQueue", + "clickout", + "CrawlerDetect", + "dayparting", + "FunnelChart", + "httpbin", + "stats_compare", + "webhook", + "SSRF", + "HMAC", + "seed_bannerpro_demo", + "tagsMode", + "htmlent", + "formit", + "сидер", + "стабы", + "плейсхолдере", + "плейсхолдера", + "чекбоксы", + "взаимоисключимо", + "thismonth", + "lastmonth", + "thisyear", + "алиасы", + "эндпоинты", + "adposition", + "adpositions", + "byad", + "byhtml", + "sortby", + "sortdir", + "getclicks", + "getreferrers", + "numberfield", + "notempty", + "pagetitle", + "matomo", + "Matomo", + "Referer", + "антифрода", + "рефереры", + "Рефереры", + "чанком", + "таймингами", + "селл", + "кликает", "msmc", "nbrb", "NBRB", @@ -314,12 +363,15 @@ "плейсхолдеры", "Плейсхолдер", "Плейсхолдеры", + "плейсхолдером", + "плейсхолдерами", "плейсхолдеров", "чанк", "чанки", "Чанк", "чанка", "чанках", + "чанков", "рендер", "рендере", "рендера", diff --git a/docs/components/bannerpro/analytics.md b/docs/components/bannerpro/analytics.md new file mode 100644 index 00000000..eeb6648c --- /dev/null +++ b/docs/components/bannerpro/analytics.md @@ -0,0 +1,128 @@ +--- +title: Внешняя аналитика +description: "BannerProAnalytics, GA4, Matomo, Яндекс Метрика и события bannerpro:click / bannerpro:impression" +--- + +# Внешняя аналитика + +BannerPro вызывает серверные события MODX и браузерные события для кликов и показов. Плагин **BannerProAnalytics** отправляет эти события в GA4, Matomo и Яндекс Метрику. + +## Что включено в ядро + +| Уровень | Событие | Когда срабатывает | +| --- | --- | --- | +| MODX | `OnBannerProClick` | Перед redirect после клика | +| MODX | `OnBannerProImpression` | После pixel-запроса показа | +| Browser | `bannerpro:click` | При клике по ссылке баннера, если загружен `analytics.js` | +| Browser | `bannerpro:impression` | При показе баннера, если включён `impression.js` | + +Серверные события работают без `BannerProAnalytics`. + +## Плагин BannerProAnalytics + +Плагин выключен по умолчанию. Для включения: + +1. Откройте **Элементы → Плагины**. +2. Включите **BannerProAnalytics**. +3. В системных настройках включите `bannerpro_analytics_enabled`. +4. Убедитесь, что на странице уже есть GA4/GTM, Matomo или Яндекс Метрика. + +Плагин подключает `assets/components/bannerpro/js/analytics.js` на событии `OnLoadWebDocument`. + +## Настройки + +| Ключ | По умолчанию | Роль | +| --- | --- | --- | +| `bannerpro_analytics_enabled` | `false` | Подключает клиентский мост | +| `bannerpro_analytics_ga4` | `true` | Включает `dataLayer.push` | +| `bannerpro_analytics_ga4_click` | `bannerpro_click` | Имя события клика GA4 | +| `bannerpro_analytics_ga4_impression` | `bannerpro_impression` | Имя события показа GA4 | +| `bannerpro_analytics_matomo` | `true` | Включает `_paq.push(['trackEvent', ...])` | +| `bannerpro_analytics_ym_counter` | пусто | ID счётчика Метрики | +| `bannerpro_analytics_ym_click_goal` | `bannerpro_click` | Цель клика в Метрике | +| `bannerpro_analytics_ym_impression_goal` | `bannerpro_impression` | Цель показа в Метрике | + +## Что отправляется + +| Система | Клик | Показ | +| --- | --- | --- | +| GA4 / GTM | `dataLayer.push({ event: 'bannerpro_click', ... })` | `event: 'bannerpro_impression'` | +| Matomo | `_paq.push(['trackEvent', 'BannerPro', 'click', ...])` | `action = impression` | +| Яндекс Метрика | `ym(ID, 'reachGoal', 'bannerpro_click', params)` | `bannerpro_impression` | + +Для GA4 через GTM создайте триггеры на события `bannerpro_click` и `bannerpro_impression`. + +## Параметры события + +| Ключ | Что содержит | +| --- | --- | +| `ad_id` | ID баннера | +| `ad_name` | Название | +| `ad_url` | URL перехода | +| `ad_type` | `image` или `html` | +| `product_id` | ID товара MiniShop3 или `0` | +| `position` | ID позиции | +| `position_name` | Имя позиции | +| `adposition` | ID связи баннер + позиция | +| `ip` | IP посетителя, только сервер | +| `recorded` | Запись создана в БД | +| `duplicate` | Дубликат за сутки | +| `referrer` | HTTP Referer, только клик | +| `redirect_url` | URL после обработки чанка, только клик | + +## Свой браузерный обработчик + +```javascript +document.addEventListener('bannerpro:impression', function (event) { + console.log(event.detail) +}) +``` + +Если `analytics.js` загрузился после `impression.js`, показы попадают в `window.bannerproImpressionQueue` и обрабатываются при инициализации моста без двойного учёта в GA4/Matomo/Метрике. + +## QA без реальных счётчиков + +Демо-страница: ресурс `bannerpro-test.html`, шаблон `core/elements/templates/demo/bannerpro_test.tpl`, секция **19**. + +Подготовка из корня MODX: + +```bash +php core/elements/demo/seed_bannerpro_demo.php +php clear_cache.php +``` + +Сидер включает `bannerpro_analytics_enabled=1`, плагин **BannerProAnalytics**, демо-позиции и баннеры. + +На стенде подключаются `analytics-mock.js` (стабы `dataLayer`, `_paq`, `ym()`) и `analytics.js`. Кнопки **Симулировать показ/клик** вызывают `bannerproAnalytics.trackImpression/Click`. + +::: warning +На демо-странице шаблон может подключать `analytics.js` даже при выключенном плагине (пометка «стенд»). На продакшене используйте только плагин + `bannerpro_analytics_enabled`. +::: + +### Ловушка Fenom `{extends}` + +Если демо-шаблон наследует layout через `{extends 'layout.tpl'}`, убедитесь, что блок с баннерами не перекрывается родительским шаблоном. Иначе события аналитики не сработают на видимых баннерах. + +## Свой серверный плагин + +Создайте плагин MODX и подпишите его на `OnBannerProClick` или `OnBannerProImpression`. + +```php +if ($modx->event->name === 'OnBannerProClick') { + $modx->log(modX::LOG_LEVEL_INFO, 'Banner click: ' . $scriptProperties['ad_name']); +} +``` + +## Webhook как альтернатива + +Для внешней аналитики без клиентских счётчиков настройте [webhook при клике и показе](settings#webhook-при-клике). Сервер отправит POST JSON на ваш endpoint. + +## Диагностика + +| Симптом | Что проверить | +| --- | --- | +| `analytics.js` не подключился | Плагин `BannerProAnalytics`, `bannerpro_analytics_enabled`, контекст `web` | +| GA4 не получает событие | На странице есть `dataLayer`, имя события совпадает с GTM-триггером | +| Matomo не получает событие | На странице есть `_paq` | +| Метрика не получает цель | `bannerpro_analytics_ym_counter` и цель JavaScript-события | +| Показы не приходят | `bannerpro_track_impressions`, `BannerProImpression`, видимость баннера в viewport | diff --git a/docs/components/bannerpro/development/events.md b/docs/components/bannerpro/development/events.md new file mode 100644 index 00000000..c71f335a --- /dev/null +++ b/docs/components/bannerpro/development/events.md @@ -0,0 +1,123 @@ +--- +title: События MODX +description: "События BannerPro для разработчиков: OnBannerProClick, OnBannerProImpression и connector actions" +--- + +# События MODX + +BannerPro вызывает события после клика и после фиксации показа. Используйте их для своей аналитики, CRM, антифрода или логирования. + +## События + +| Событие | Где вызывается | Когда | +| --- | --- | --- | +| `OnBannerProClick` | `bannerpro_invoke_click_event()` | После обработки клика, перед redirect | +| `OnBannerProImpression` | `bannerpro_invoke_impression_event()` | После pixel-запроса показа | + +Код событий лежит в `core/components/bannerpro/include/events.php`. + +## Payload + +| Ключ | Тип | Описание | +| --- | --- | --- | +| `ad` | `byAd\|null` | Объект баннера | +| `ad_id` | int | ID баннера | +| `ad_name` | string | Название | +| `ad_url` | string | URL перехода | +| `ad_type` | string | `image` или `html` | +| `product_id` | int | ID товара MiniShop3 или `0` | +| `position` | int | ID позиции | +| `position_name` | string | Имя позиции | +| `adposition` | int | ID связи `bannerpro_ads_positions` | +| `ip` | string | IP посетителя | +| `recorded` | bool | Запись создана в таблице статистики | +| `duplicate` | bool | Событие повторилось в тот же день | +| `referrer` | string | HTTP Referer, только для клика | +| `redirect_url` | string | URL после обработки чанка, только для клика | + +## Пример плагина + +```php +event->name !== 'OnBannerProClick') { + return; +} + +$adName = (string) ($scriptProperties['ad_name'] ?? ''); +$positionName = (string) ($scriptProperties['position_name'] ?? ''); + +$modx->log( + modX::LOG_LEVEL_INFO, + 'BannerPro click: ' . $adName . ' / ' . $positionName +); +``` + +Подпишите плагин на `OnBannerProClick`. + +## HTTP webhook + +Параллельно с MODX-событиями компонент может отправлять POST JSON на внешний URL. Настройки: [Системные настройки](../settings#webhook-при-клике). + +| Тип | Настройка URL | Поле `event` | +| --- | --- | --- | +| Клик | `bannerpro_webhook_url` | `click` | +| Показ | `bannerpro_webhook_impression_url` | `impression` | + +Подпись: заголовок `X-BannerPro-Signature` (HMAC-SHA256 от тела), если задан `bannerpro_webhook_secret`. + +Компонент шлёт webhook клика после `OnBannerProClick`, до редиректа. Timeout 2 с, ошибки не блокируют переход. Webhook показа: fire-and-forget после `OnBannerProImpression`, ответ `204` не ждёт завершения запроса. + +URL проверяется на SSRF: запрещены `localhost`, loopback и private/reserved IP в hostname. + +### Payload клика + +`event`, `ad_id`, `position_id`, `adposition`, `referrer`, `ip`, `click_id`, `timestamp` (ISO8601), `redirect_url`, `recorded`, `duplicate`. + +### Payload показа + +`event`, `ad_id`, `position_id`, `adposition`, `ip`, `timestamp` (ISO8601), `recorded`, `duplicate`. + +## Клиентские события + +`impression.js` отправляет `bannerpro:impression`. `analytics.js` перехватывает клики и может отправлять `bannerpro:click`. + +```javascript +document.addEventListener('bannerpro:impression', function (event) { + console.log(event.detail.adposition) +}) +``` + +## Connector actions + +Админка обращается к `assets/components/bannerpro/connector.php` методом `POST`. Параметр `action` выбирает обработчик. + +| Группа | Actions | +| --- | --- | +| Баннеры | `ads_getlist`, `ads_get`, `ads_create`, `ads_update`, `ads_remove`, `ads_enable`, `ads_disable`, `ads_getclicks`, `ads_duplicate`, `ads_bulk_enable`, `ads_bulk_disable`, `ads_bulk_remove`, `ads_bulk_assign_positions`, `ads_create_from_template` | +| Шаблоны | `ad_templates_getlist` | +| Позиции | `positions_getlist`, `positions_create`, `positions_update`, `positions_remove`, `positions_duplicate` | +| Связи | `adpositions_getlist`, `adpositions_add`, `adpositions_remove`, `adpositions_sort`, `adpositions_update_weight` | +| Статистика | `stats_summary`, `stats_by_day`, `stats_by_weekday`, `stats_top_ads`, `stats_export`, `stats_purge`, `stats_compare`, `clicks_getreferrers` | +| Журнал | `audit_getlist` | +| Метки | `tags_suggest` | +| Настройки | `settings_integrations_get`, `settings_integrations_update` | +| MiniShop3 | `resource_getlist` | + +Connector требует сессию `mgr`. Мутации проверяют `bannerpro_save`, `bannerpro_remove` или `bannerpro_stats`. + +## Модель данных + +| Класс xPDO | Таблица | +| --- | --- | +| `byAd` | `bannerpro_ads` | +| `byPosition` | `bannerpro_positions` | +| `byAdPosition` | `bannerpro_ads_positions` | +| `byClick` | `bannerpro_clicks` | +| `byImpression` | `bannerpro_impressions` | +| `byAdTemplate` | `bannerpro_ad_templates` | +| `byAudit` | `bannerpro_audit` | + +Поле `byAdPosition.id` попадает в плейсхолдер `adposition` и используется в ссылке клика. + +Поля баннера `byAd`: `category_id`, `max_clicks`, `max_impressions`, `show_hours` (JSON), `target_resource_id`, `target_parent_id`, `tags` (JSON). Позиция `byPosition`: `context_key`. diff --git a/docs/components/bannerpro/development/rest-api.md b/docs/components/bannerpro/development/rest-api.md new file mode 100644 index 00000000..3ffc5c07 --- /dev/null +++ b/docs/components/bannerpro/development/rest-api.md @@ -0,0 +1,156 @@ +--- +title: REST API +description: "Read-only REST API BannerPro: включение, Bearer-токен, баннеры, позиции и статистика" +--- + +# REST API + +BannerPro предоставляет read-only HTTP API для внешних систем. API отдаёт баннеры, позиции и статистику, но не создаёт и не меняет записи. + +Точка входа: + +```text +assets/components/bannerpro/api.php +``` + +## Включение + +1. Откройте **Система → Настройки системы** и отфильтруйте namespace **`bannerpro`**. +2. Установите **`bannerpro_api_enabled`** = **Да**. +3. Скопируйте значение **`bannerpro_api_key`** (см. [Ключ API](#ключ-api)). +4. Отправляйте только **GET**-запросы с заголовком **`Authorization: Bearer …`**. + +## Ключ API + +В примерах `curl` плейсхолдер **`YOUR_API_KEY`**: значение **`bannerpro_api_key`** из системных настроек. + +### Где посмотреть + +| Способ | Путь | +| --- | --- | +| Менеджер | **Система → Настройки системы** → namespace `bannerpro` → **REST API ключ** | +| База | Таблица `modx_system_settings` (префикс может отличаться), ключ `bannerpro_api_key` | + +### Как появляется ключ + +При установке или обновлении пакета resolver записывает случайную строку из **32 hex-символов**, если поле было пустым. Можно задать свой токен: сохраните новое значение в настройке и используйте его в заголовке. + +После смены ключа очистите кэш MODX (`php clear_cache.php` или **Управление → Очистить кэш**), если старое значение ещё отдаётся из кэша настроек. + +### Пример запроса + +Подставьте ключ из настройки **без** префикса `Bearer` в переменную. Префикс `Bearer` указывают только в HTTP-заголовке: + +```bash +API_KEY="вставьте_значение_bannerpro_api_key" + +curl -s -H "Authorization: Bearer ${API_KEY}" \ + "https://example.com/assets/components/bannerpro/api.php?path=/ads&limit=10" +``` + +Ответ `401 unauthorized`: неверный или пустой ключ. `503 api disabled`: выключен `bannerpro_api_enabled`. + +## Аутентификация + +```http +Authorization: Bearer YOUR_API_KEY +``` + +| Код | Причина | +| --- | --- | +| `401` | Ключ отсутствует или неверен | +| `503` | API выключен через `bannerpro_api_enabled` | +| `404` | Маршрут не найден | + +## Формат ответа + +Успешный ответ: + +```json +{ + "success": true, + "data": [], + "total": 0 +} +``` + +Ошибка: + +```json +{ + "success": false, + "message": "not found" +} +``` + +## GET /ads + +Возвращает список баннеров. + +| Параметр | Тип | Описание | +| --- | --- | --- | +| `position` | int | ID позиции | +| `active` | `0` или `1` | Фильтр активности | +| `limit` | int | Лимит, по умолчанию 20, максимум 500 | +| `offset` | int | Смещение | + +```bash +curl -s -H "Authorization: Bearer YOUR_API_KEY" \ + "https://example.com/assets/components/bannerpro/api.php?path=/ads&limit=10" +``` + +## GET /ads/{id} + +Возвращает один баннер со счётчиками кликов, показов и заказов. + +```bash +curl -s -H "Authorization: Bearer YOUR_API_KEY" \ + "https://example.com/assets/components/bannerpro/api.php?path=/ads/42" +``` + +## GET /positions + +Возвращает список позиций. + +```bash +curl -s -H "Authorization: Bearer YOUR_API_KEY" \ + "https://example.com/assets/components/bannerpro/api.php?path=/positions" +``` + +## GET /stats + +Возвращает сводку, динамику по дням и топ баннеров. + +| Параметр | Тип | Описание | +| --- | --- | --- | +| `period` | string | `all`, `today`, `this_month`, `last_month`, `this_year`. Алиасы: `overall`, `thismonth`, `lastmonth`, `thisyear` | +| `position` | int | ID позиции, `0` означает все | +| `limit` | int | Лимит для `top_ads`, по умолчанию 10 | + +```bash +curl -s -H "Authorization: Bearer YOUR_API_KEY" \ + "https://example.com/assets/components/bannerpro/api.php?path=/stats&period=this_month&position=0" +``` + +В ответе `stats_by_day` и сводке есть поле `conversions` (заказы MS3 с атрибуцией клика). Для одного баннера `GET /ads/{id}` тоже возвращает счётчик `conversions`. + +## PATH_INFO + +API принимает путь через `?path=...`. Если веб-сервер корректно передаёт `PATH_INFO`, можно использовать путь после `api.php`. + +```bash +curl -s -H "Authorization: Bearer YOUR_API_KEY" \ + "https://example.com/assets/components/bannerpro/api.php/ads" +``` + +## Безопасность + +- Храните `bannerpro_api_key` в секретах backend-сервиса. +- Не отправляйте ключ в публичный frontend. +- Ограничьте доступ к `api.php` по IP на веб-сервере, если API нужен только внутренним сервисам. +- Используйте HTTPS. + +## См. также + +- [Системные настройки](../settings): `bannerpro_api_enabled`, `bannerpro_api_key`. +- [События MODX](events): серверные события и connector actions. diff --git a/docs/components/bannerpro/faq.md b/docs/components/bannerpro/faq.md new file mode 100644 index 00000000..9f1b2e68 --- /dev/null +++ b/docs/components/bannerpro/faq.md @@ -0,0 +1,272 @@ +--- +title: FAQ +description: "Частые вопросы по BannerPro: пустой вывод, клики, показы, VueTools, MiniShop3 и REST API" +--- + +# FAQ + +## Баннеры не выводятся на сайте + +Проверьте pdoTools, имя позиции, активность баннера, даты показа и связь баннера с позицией. Затем контекст, таргетинг и A/B: + +- **Контекст позиции**: при заполненном `context_key` баннеры видны только в этом контексте MODX. Пустое поле: все контексты. В сниппете передайте `&context=` для явной проверки. +- **Таргетинг**: `show_hours`, `target_resource_id`, `target_parent_id`, метки (`&tags=`), лимиты `max_clicks` / `max_impressions`. +- **A/B**: при `sortby=ab` в слоте нужны ровно 2 баннера. Иначе выводятся все подходящие без split. + +Минимальный debug-вызов: + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'sidebar', + 'tpl' => 'byAd', + 'showLog' => true +]} +``` + +```modx +[[!BannerPro? + &positionName=`sidebar` + &tpl=`byAd` + &showLog=`1` +]] +``` + +::: + +`showLog` работает только для пользователя с активной сессией `mgr`. + +## Сниппет пишет, что нужен pdoTools + +Установите **pdoTools** и очистите кэш MODX. + +Текст ошибки: + +```text +You need to install pdoTools to use this snippet! +``` + +## Картинки не отображаются + +Проверьте `bannerpro_media_source`, источник файлов у конкретного баннера и путь в поле `image`. + +Если баннер использует свой Media Source, настройка `bannerpro_media_source` не влияет на этот баннер. + +## Клики не считаются + +Проверьте пять пунктов: + +1. Плагин **BannerProClickout** включён на `OnPageNotFound`. +2. Чанк ведёт на `[[+click_url]]` / `{$click_url}`, не на прямой URL баннера. +3. `adposition` означает ID связи `bannerpro_ads_positions`. +4. Friendly URL не перехватывает путь раньше MODX. +5. **Фильтр ботов**: при `bannerpro_filter_bots = Да` клики с User-Agent ботов не записываются. Нужен пакет CrawlerDetect; без него фильтр не срабатывает. + +Повторный клик с того же IP за сутки на ту же пару баннер + позиция не увеличивает счётчик. Redirect всё равно работает. + +## Статистика занижена из-за ботов + +Настройка **Фильтр ботов** (`bannerpro_filter_bots`) исключает из статистики запросы, определённые как боты. Установите [CrawlerDetect](https://modstore.pro/packages/other/crawlerdetect) и включите настройку. Исторические данные не пересчитываются. + +## Баннер активен, но не виден на сайте + +1. Проверьте **start** / **end** (календарный период). +2. **Расписание** `show_hours`: дни недели и часы; вне слота баннер скрыт. +3. **Таргетинг**: `target_resource_id` / `target_parent_id` ограничивают страницы. +4. **Метки**: при `&tags=` в сниппете баннер без нужных меток не попадёт в выборку. +5. Лимиты `max_clicks` / `max_impressions` скрывают баннер при достижении порога. + +## Контекст MODX и позиция + +Если у позиции заполнен `context_key`, баннеры этой позиции выводятся только в указанном контексте. Параметр сниппета `&context=` сверяет активный контекст с полем позиции. Подробнее: [Интеграция](integration#контекст-позиции). + +## Показы не считаются + +Проверьте `bannerpro_track_impressions`, плагин **BannerProImpression** и наличие `impression.js` на странице. + +Компонент фиксирует показ, когда баннер попадает в viewport. Повтор с того же IP за сутки не создаёт новую запись. + +При `bannerpro_impression_lazy = Да` pixel отправляется после задержки в viewport, а не сразу при появлении. + +## A/B split не работает + +Параметр `sortby=ab` делит трафик 50/50 только если в позиции **ровно два** активных баннера. Иначе выводятся все подходящие без cookie split. + +Проверьте: + +1. В слоте два баннера, оба проходят фильтры (даты, таргетинг, лимиты). +2. Кэш сниппета: ключ кэша включает вариант A/B. Не кэшируйте вывод с `sortby=ab` глобально на всех пользователей. +3. TTL cookie: `bannerpro_ab_ttl` (дней, по умолчанию 30). + +Подробнее: [Интеграция](integration#ab-split-sortbyab). + +## Webhook на клик не срабатывает + +1. `bannerpro_webhook_url` задан и начинается с `http://` или `https://`. +2. URL не указывает на `localhost`, `127.0.0.1` или private IP. +3. Проверьте лог MODX на `[bannerpro] Webhook failed`. +4. Редирект не ждёт ответа webhook; timeout 2 с. + +Для отладки можно временно указать URL [httpbin.org](https://httpbin.org/post) и проверить входящий JSON. + +Подробнее: [Системные настройки](settings#webhook-при-клике). + +## Webhook на показ не срабатывает + +1. Заполните `bannerpro_webhook_impression_url`. +2. Включён учёт показов: `bannerpro_track_impressions`. +3. Те же SSRF-ограничения, что и для клика. +4. Payload содержит `event: impression`. Подпись в заголовке `X-BannerPro-Signature`, если задан secret. + +Альтернатива webhook: [внешняя аналитика](analytics) (GTM, Метрика) или событие `OnBannerProImpression`. + +## Lead-форма через FormIt + +BannerPro не ставит FormIt. Схема: HTML-баннер с `click_url` → landing с FormIt. Пошагово: [FormIt](formit). + +## Админка пустая + +Проверьте **VueTools**, право `view`, консоль браузера и файл: + +```text +assets/components/bannerpro/js/mgr/vue-dist/bannerpro-admin.min.js +``` + +Если вы собираете transport из исходников, перед сборкой выполните `npm run build`. + +## Вкладка «Статистика» не видна + +Пользователю нужно право `bannerpro_stats`. Назначьте его в политике доступа MODX и перезайдите в менеджер. + +## Кнопки создания и редактирования скрыты + +Пользователю нужно право `bannerpro_save`. Для удаления нужно `bannerpro_remove`. + +## HTML-баннер идёт без ссылки + +У HTML-баннера должен быть заполнен URL. Если URL пустой, компонент выводит сырой HTML без обёртки ссылки. + +## Как вывести баннеры по имени позиции + +Используйте `positionName`. + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'sidebar', + 'tpl' => 'byAd' +]} +``` + +```modx +[[!BannerPro? + &positionName=`sidebar` + &tpl=`byAd` +]] +``` + +::: + +Если заданы `position` и `positionName`, приоритет у `position`. + +## Как вывести произвольный HTML + +В форме баннера выберите тип **HTML** и заполните поле HTML. Сниппет выведет его через `byHtml` или через ваш `tplHtml`. + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'sidebar', + 'tplHtml' => 'byHtml' +]} +``` + +```modx +[[!BannerPro? + &positionName=`sidebar` + &tplHtml=`byHtml` +]] +``` + +::: + +Если URL заполнен, BannerPro обернёт HTML в ссылку клика. + +## Баннеры MiniShop3 не показываются на карточке товара + +Передайте `productId` в сниппет и проверьте `product_id` у баннера. + +На карточке товара: + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'shop-product-sidebar', + 'productId' => $_modx->resource.id, + 'tpl' => 'byAdProduct' +]} +``` + +```modx +[[!BannerPro? + &positionName=`shop-product-sidebar` + &productId=`[[*id]]` + &tpl=`byAdProduct` +]] +``` + +::: + +На каталоге и главной не передавайте `productId`. + +## Плейсхолдеры `product_*` пустые + +Проверьте, что баннер привязан к товару, товар опубликован, а TV `price`, `old_price`, `thumb` или `image` заполнены. + +## Пикер товара показывает мало товаров + +Пикер загружает до 200 товаров за запрос. Введите часть названия в фильтр. + +## GA4, Matomo или Метрика не получают события + +Проверьте плагин **BannerProAnalytics**, настройку `bannerpro_analytics_enabled` и наличие счётчика на странице. + +Для Метрики укажите `bannerpro_analytics_ym_counter`. Для GA4 через GTM создайте триггер на событие `bannerpro_click` или `bannerpro_impression`. + +На демо-странице запустите `php core/elements/demo/seed_bannerpro_demo.php`. Если статус «плагин не установлен», а настройки верные, проверьте шаблон: при Fenom `{extends}` переменные `{set}` должны быть внутри `{block}`. Подробнее: [Внешняя аналитика](analytics#demo-qa-без-реальных-счётчиков). + +## Где взять ключ REST API? + +1. **Система → Настройки системы** → namespace `bannerpro` → **`bannerpro_api_key`** (подпись «REST API ключ»). +2. Включите **`bannerpro_api_enabled`** = Да. +3. В `curl` подставьте значение настройки: `-H "Authorization: Bearer <ключ>"`. + +Ключ не связан с логином менеджера. При пустом поле обновите пакет или задайте токен вручную. Подробнее: [REST API](development/rest-api#ключ-api). + +## REST API возвращает 401 + +Передайте ключ в заголовке: + +```http +Authorization: Bearer YOUR_API_KEY +``` + +Сверьте значение с `bannerpro_api_key`. См. [Где взять ключ REST API?](#где-взять-ключ-rest-api). + +## REST API возвращает 503 + +Включите `bannerpro_api_enabled`. + +## Как очистить старую статистику + +Запустите cron: + +```bash +php core/components/bannerpro/cron/purge.php +``` + +Срок хранения задают `bannerpro_clicks_retention_days` и `bannerpro_impressions_retention_days`. diff --git a/docs/components/bannerpro/formit.md b/docs/components/bannerpro/formit.md new file mode 100644 index 00000000..3a259c13 --- /dev/null +++ b/docs/components/bannerpro/formit.md @@ -0,0 +1,123 @@ +--- +title: FormIt +description: "Lead-баннер BannerPro с FormIt: HTML-баннер, click_url и landing с формой" +--- + +# FormIt + +BannerPro не включает FormIt в ядро. Связка «баннер → landing с формой» строится на HTML-баннере, плейсхолдере `click_url` и странице с FormIt. + +**Требования:** BannerPro, FormIt, ресурс-лендинг с формой. + +## Схема + +```text +Баннер (HTML) → клик → clickout → landing с FormIt + ↳ UTM в URL (опционально, см. [настройки](settings#utm-при-клике)) +``` + +## Шаг 1. Landing с FormIt + +Создайте ресурс, например «Заявка с баннера»: + +::: code-group + +```fenom +{'!FormIt' | snippet : [ + 'hooks' => 'email', + 'emailSubject' => 'Заявка с баннера', + 'emailTo' => 'manager@example.com', + 'validate' => 'name:required,email:email:required' +]} +
+ + + + +
+{if $_modx->getPlaceholder('fi.error.name')} +

{$_modx->getPlaceholder('fi.error.name')}

+{/if} +{if $_modx->getPlaceholder('fi.error.email')} +

{$_modx->getPlaceholder('fi.error.email')}

+{/if} +``` + +```modx +[[!FormIt? + &hooks=`email` + &emailSubject=`Заявка с баннера` + &emailTo=`manager@example.com` + &validate=`name:required,email:email:required` +]] +
+ + + + +
+[[+fi.error.name:notempty=`

[[+fi.error.name]]

`]] +[[+fi.error.email:notempty=`

[[+fi.error.email]]

`]] +``` + +::: + +## Шаг 2. HTML-баннер в BannerPro + +Тип баннера: **HTML**. Чанк `tplHtml`: + +::: code-group + +```fenom +
+

{$name|escape}

+ Оставить заявку +
+``` + +```modx +
+

[[+name:htmlent]]

+ Оставить заявку +
+``` + +::: + +URL баннера в админке укажите на ресурс с FormIt. Компонент обернёт HTML в ссылку clickout через `click_url`. + +## Шаг 3. UTM (опционально) + +Включите UTM в [системных настройках](settings#utm-при-клике) или во вкладке **Настройки** админки. Параметры добавятся к `redirect_url` при клике. + +## Шаг 4. Вызов сниппета + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'lead-sidebar', + 'tplHtml' => '@FILE chunks/byad-lead.fenom.tpl', + 'limit' => 1 +]} +``` + +```modx +[[!BannerPro? + &positionName=`lead-sidebar` + &tplHtml=`@FILE chunks/byad-lead.fenom.tpl` + &limit=`1` +]] +``` + +::: + +## Отличие от MS3 attribution + +FormIt не создаёт заказ MiniShop3. Для e-commerce смотрите [MiniShop3](minishop3). + +## Связанные разделы + +- [Интеграция](integration): `click_url`, `sortby`, кэш +- [Системные настройки](settings): UTM, webhook +- [Сниппет BannerPro](snippets/BannerPro): параметры `tplHtml` diff --git a/docs/components/bannerpro/index.md b/docs/components/bannerpro/index.md new file mode 100644 index 00000000..b615dfe0 --- /dev/null +++ b/docs/components/bannerpro/index.md @@ -0,0 +1,140 @@ +--- +title: BannerPro +description: "Управление баннерами в MODX 3: позиции, ротация, UTM, webhook, A/B split, клики и показы, Vue-админка" +author: ibochkarev +dependencies: [pdoTools, VueTools] + +items: [ + { + text: 'Начало работы', + link: 'quick-start', + items: [ + { text: 'Быстрый старт', link: 'quick-start' }, + { text: 'Системные настройки', link: 'settings' }, + ], + }, + { + text: 'Интеграция на сайте', + link: 'integration', + items: [ + { text: 'Интеграция и сценарии', link: 'integration' }, + { text: 'Сниппеты (обзор)', link: 'snippets/index' }, + { text: 'Сниппет BannerPro', link: 'snippets/BannerPro' }, + ], + }, + { + text: 'Админка', + link: 'manager', + items: [ + { text: 'Баннеры, позиции, статистика', link: 'manager' }, + ], + }, + { + text: 'Интеграции', + link: 'minishop3', + items: [ + { text: 'MiniShop3', link: 'minishop3' }, + { text: 'Внешняя аналитика', link: 'analytics' }, + { text: 'FormIt', link: 'formit' }, + ], + }, + { + text: 'Для разработчика', + link: 'development/events', + items: [ + { text: 'События MODX', link: 'development/events' }, + { text: 'REST API', link: 'development/rest-api' }, + ], + }, + { text: 'FAQ', link: 'faq' }, +] +--- + +# BannerPro + +**BannerPro** выводит баннеры по позициям через сниппет `BannerPro`, считает клики по URL `/{bannerpro_click}/{adposition}` и фиксирует показы через `impression.js`, если включён учёт. Админка на Vue 3 и PrimeVue через **VueTools**. + +Namespace настроек: **`bannerpro`**. Основные таблицы: `bannerpro_ads`, `bannerpro_positions`, `bannerpro_ads_positions`, `bannerpro_clicks`, `bannerpro_impressions`. + +С чего начать: [Быстрый старт](quick-start). + +## Требования + +| Компонент | Версия | Роль | +| --- | --- | --- | +| MODX Revolution | 3.0+ | Платформа | +| PHP | 8.2+ | Runtime | +| pdoTools | 2.1+ | Выборка и рендер сниппета `BannerPro` | +| VueTools | актуальная | Vue 3 и PrimeVue в админке | +| MiniShop3 | опционально | Привязка баннера к товару и атрибуция заказа | + +## Возможности + +- **Позиции**: слоты `sidebar`, `header`, `shop-product-sidebar`; вывод по имени или ID. +- **Типы баннеров**: изображение или HTML. При заполненном URL HTML оборачивается в ссылку клика. +- **Ротация**: `RAND()`, `idx`, `weighted`, A/B split (`sortby=ab`). +- **Таргетинг**: расписание `show_hours`, метки, привязка к ресурсу или родителю, лимиты `max_clicks` / `max_impressions`. +- **Контекст**: поле `context_key` у позиции и параметр `&context=` в сниппете. +- **UTM**: query-параметры при клике из системных настроек или вкладки «Настройки» админки. +- **Webhook**: POST JSON на внешний URL при клике и при показе, подпись HMAC. +- **Фильтр ботов**: исключение ботов из статистики через CrawlerDetect. +- **Учёт кликов**: плагин `BannerProClickout` перехватывает `OnPageNotFound`, пишет клик и делает redirect на URL баннера. +- **Учёт показов**: настройка `bannerpro_track_impressions` подключает `impression.js` и pixel URL. +- **Статистика**: вкладка админки показывает клики, показы, CTR, конверсии MS3, воронку и сравнение периодов. +- **REST API**: read-only endpoint `assets/components/bannerpro/api.php` отдаёт баннеры, позиции и статистику с `conversions`. + +## Минимальный путь + +1. Установите **BannerPro**, **pdoTools** и **VueTools**. +1. Откройте **Компоненты → BannerPro → Позиции** и создайте позицию `sidebar`. +1. Создайте баннер, задайте URL, изображение или HTML и привяжите его к позиции. +1. В шаблоне сайта вызовите сниппет: + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'sidebar', + 'tpl' => 'byAd', + 'limit' => 1 +]} +``` + +```modx +[[!BannerPro? + &positionName=`sidebar` + &tpl=`byAd` + &limit=`1` +]] +``` + +::: + +1. Откройте страницу и проверьте ссылку `bannerclick/{adposition}`. + +## Поток клика + +```mermaid +flowchart LR + Page[Страница сайта] --> Link["/{bannerpro_click}/{adposition}"] + Link --> NotFound[OnPageNotFound] + NotFound --> Clickout[BannerProClickout] + Clickout --> ClickRow[bannerpro_clicks] + Clickout --> Event[OnBannerProClick] + Clickout --> Redirect[Redirect на URL баннера] +``` + +## Быстрые ссылки + +| Нужно | Документ | +| --- | --- | +| Установить и вывести первый баннер | [Быстрый старт](quick-start) | +| Проверить все ключи `bannerpro_*` | [Системные настройки](settings) | +| Настроить баннеры и позиции | [Админка](manager) | +| Разобрать кэш, ротацию, клики и показы | [Интеграция](integration) | +| Посмотреть параметры сниппета | [Сниппет BannerPro](snippets/BannerPro) | +| Выводить баннеры в MiniShop3 | [MiniShop3](minishop3) | +| Отправлять события в GA4, Matomo или Метрику | [Внешняя аналитика](analytics) | +| Связать баннер с формой FormIt | [FormIt](formit) | +| Подключить REST API | [REST API](development/rest-api) | +| Найти причину пустого вывода | [FAQ](faq) | diff --git a/docs/components/bannerpro/integration.md b/docs/components/bannerpro/integration.md new file mode 100644 index 00000000..b8fbceea --- /dev/null +++ b/docs/components/bannerpro/integration.md @@ -0,0 +1,441 @@ +--- +title: Интеграция и сценарии +description: "Вывод BannerPro на сайте: чанки, ротация, кэш, клики, показы, лимиты и очистка статистики" +--- + +# Интеграция и сценарии + +Сниппет `BannerPro` выбирает активные баннеры, рендерит их через pdoTools и отдаёт HTML для позиции. Клики обрабатывает плагин `BannerProClickout`, показы обрабатывает `BannerProImpression`. + +## Базовый вывод + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'sidebar', + 'tpl' => 'byAd', + 'limit' => 3 +]} +``` + +```modx +[[!BannerPro? + &positionName=`sidebar` + &tpl=`byAd` + &limit=`3` +]] +``` + +::: + +`positionName` принимает одно имя или список через запятую: `sidebar,footer`. Если вы передали `position`, сниппет игнорирует `positionName`. + +## Выборка + +Сниппет всегда добавляет базовые условия: + +| Условие | Что делает | +| --- | --- | +| `start` / `end` | Показывает баннер только в активный период | +| `active = 1` | Скрывает выключенные баннеры, если не задан `showInactive` | +| `byAdPosition` | Берёт только баннеры, привязанные к позиции | +| `position` / `positions` / `positionName` | Фильтрует позиции | +| `context_key` | Фильтрует позиции по контексту MODX (текущий или `&context=`) | +| `max_clicks` / `max_impressions` | Скрывает баннер при достижении лимита | + +Лимит кликов и показов не меняет поле `active`. Баннер просто перестаёт попадать в выборку. + +## Ротация и порядок + +| `sortby` | Поведение | +| --- | --- | +| `RAND()` | Случайный порядок, значение по умолчанию | +| `idx` | Порядок связи баннер + позиция | +| `weighted` | Взвешенная ротация через `RAND() * weight` | +| `ab` | Sticky A/B 50/50 при ровно двух баннерах в слоте | +| поле `byAd` | Сортировка по полю баннера | + +Фиксированный порядок: + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'sidebar', + 'tpl' => 'byAd', + 'sortby' => 'idx', + 'sortdir' => 'ASC' +]} +``` + +```modx +[[!BannerPro? + &positionName=`sidebar` + &tpl=`byAd` + &sortby=`idx` + &sortdir=`ASC` +]] +``` + +::: + +Взвешенная ротация: + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'sidebar', + 'tpl' => 'byAd', + 'sortby' => 'weighted', + 'limit' => 1 +]} +``` + +```modx +[[!BannerPro? + &positionName=`sidebar` + &tpl=`byAd` + &sortby=`weighted` + &limit=`1` +]] +``` + +::: + +Вес задайте в админке на связи баннер + позиция. + +## A/B split + +При `sortby=ab` компонент делает sticky split 50/50 между **ровно двумя** баннерами в одной позиции (после фильтров расписания, лимитов и таргетинга): + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'hero', + 'sortby' => 'ab', + 'limit' => 1 +]} +``` + +```modx +[[!BannerPro? + &positionName=`hero` + &sortby=`ab` + &limit=`1` +]] +``` + +::: + +| Поведение | Описание | +| --- | --- | +| Первый визит | Случайный выбор одного из двух баннеров | +| Повторные визиты | Тот же баннер по cookie `bannerpro_ab_{positionId}` | +| Не 2 баннера | A/B не применяется, выводятся все подходящие | +| Кэш сниппета | Отключён (как для `RAND()` и `weighted`) | +| TTL cookie | `bannerpro_ab_ttl` (дней, по умолчанию 30) | + +## Контекст MODX + +Позиция может иметь поле `context_key` (`web`, `mgr`, …). Пустое значение означает все контексты. + +Сниппет фильтрует позиции по текущему контексту. Явный override: + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'sidebar', + 'context' => 'web', + 'tpl' => 'byAd' +]} +``` + +```modx +[[!BannerPro? + &positionName=`sidebar` + &context=`web` + &tpl=`byAd` +]] +``` + +::: + +Ключ кэша учитывает effective context. Два вызова с разным `context` на одной странице не делят один кэш. + +## Таргетинг + +В админке на баннере задают ограничения показа. Сниппет применяет их автоматически: + +| Механизм | Поле / параметр | Что делает | +| --- | --- | --- | +| Расписание | `show_hours` | Дни недели и часы показа в рамках `start` / `end` | +| Страница | `target_resource_id` | Баннер только на указанном ресурсе | +| Раздел | `target_parent_id` | Баннер на дочерних страницах раздела | +| Метки | `&tags=`, `tagsMode` | Фильтр по JSON-меткам баннера | + +Таргетинг по странице/разделу в админке: либо конкретный ресурс, либо дочерние страницы раздела, не оба сразу. + +Фильтр по меткам в вызове: + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'sidebar', + 'tags' => 'sale,promo', + 'tpl' => 'byAd' +]} +``` + +```modx +[[!BannerPro? + &positionName=`sidebar` + &tags=`sale,promo` + &tpl=`byAd` +]] +``` + +::: + +При `&tags=` баннер без меток не попадёт в выборку. + +## Кэш HTML + +`BannerPro` кэширует готовый HTML, если включена настройка `bannerpro_cache` и вызов подходит для кэша. + +Сниппет не использует кэш при таких условиях: + +- `sortby=RAND()` +- `sortby=weighted` +- `sortby=ab` +- `&cache=0` +- `&toSeparatePlaceholders` +- `&showLog=1` и активная сессия `mgr` + +Принудительно отключить кэш для одного вызова: + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'sidebar', + 'tpl' => 'byAd', + 'sortby' => 'idx', + 'cache' => false +]} +``` + +```modx +[[!BannerPro? + &positionName=`sidebar` + &tpl=`byAd` + &sortby=`idx` + &cache=`0` +]] +``` + +::: + +Ключ кэша учитывает контекст, культуру, сортировку, параметры вызова и часовой bucket. Кэш сбрасывается при смене периода показа (`start` / `end`). + +## Чанки + +В transport входят два базовых чанка: + +| Чанк | Для чего | +| --- | --- | +| `byAd` | Баннер-изображение со ссылкой клика | +| `byHtml` | HTML-баннер | + +В чанке `byAd` и `byHtml` ссылку клика берите из `click_url` (clickout + UTM при включённых настройках): + +::: code-group + +```fenom + +``` + +```modx + +``` + +::: + +Вызов с файловым чанком: + +::: code-group + +```fenom +{'!BannerPro' | snippet : [ + 'positionName' => 'sidebar', + 'tpl' => '@FILE chunks/banner.fenom.tpl', + 'fastMode' => true +]} +``` + +```modx +[[!BannerPro? + &positionName=`sidebar` + &tpl=`@FILE chunks/banner.fenom.tpl` + &fastMode=`1` +]] +``` + +::: + +## Клики + +Поток клика: + +1. Посетитель открывает ссылку из `click_url` → `/{bannerpro_click}/{adposition}`. +2. MODX не находит ресурс и вызывает `OnPageNotFound`. +3. `BannerProClickout` ищет связь `byAdPosition` по `adposition`. +4. Компонент пишет клик в `bannerpro_clicks`. +5. Компонент вызывает `OnBannerProClick`. +6. MODX перенаправляет посетителя на URL баннера. + +Повторный клик с того же IP за сутки на ту же пару баннер + позиция не создаёт новую запись. Redirect и событие всё равно выполняются, в payload будет `duplicate: true`. + +URL баннера поддерживает плейсхолдеры из query string: + +::: code-group + +```fenom + + {$name|escape} + +``` + +```modx + + [[+name]] + +``` + +::: + +### UTM на redirect + +Настройки `bannerpro_utm_*` добавляют UTM к URL редиректа при клике. См. [Системные настройки](settings#utm-при-клике). UTM из настроек не перезаписывают уже существующие query-параметры в URL баннера. + +Если в админке URL равен `https://shop.example/sale?utm_campaign=[[+utm_source]]`, компонент подставит `sidebar`. + +## Показы + +Для показов включите настройку `bannerpro_track_impressions`. + +При `bannerpro_impression_lazy = 1` (по умолчанию) `impression.js` подключается через IntersectionObserver, а не блокирующим `