Кейс реального проекта: 354 посадочные страницы для каталога сэндвич-панелей на 1С-Битрикс + Аспро ЛайтШоп, созданные полностью через SSH-доступ ИИ-агентом, минуя графический интерфейс модуля.
Почему эта статья важна
Модуль «SEO умного фильтра» от Аспро (aspro.smartseo) — мощный инструмент для создания посадочных страниц под низкочастотные запросы умного фильтра. Но его графический интерфейс рассчитан на ручное создание правил и условий по одному. Когда вам нужно развернуть сотни посадочных страниц с разными свойствами, разделами и SEO-тегами — ручной путь становится невыносимым:
- 8 разделов × 5 свойств × десятки значений = 300+ посадочных
- Каждую нужно заполнить: Title, Description, H1, Keywords, SEO-текст на 3000 символов
- Каждое правило в UI требует 15-20 кликов и аккуратного заполнения формы
- Любая опечатка в CONDITION_TREE ломает генерацию URL без понятной ошибки
В этом гайде — как делегировать эту работу ИИ-агенту, который работает с БД напрямую через ORM и SQL.
Архитектура решения
┌─────────────────────────────────────────────────────────┐
│ ИИ-агент (ZCode/CLI) │
│ - Читает ТЗ (XLSX/CSV/DOCX) │
│ - Формирует JSON-дампы │
│ - Генерирует PHP-скрипты │
└────────────────────┬────────────────────────────────────┘
│ SSH (ключи)
▼
┌─────────────────────────────────────────────────────────┐
│ Bitrix-сервер (PHP 8.2 CLI) │
│ - prolog_before.php (bootstrap) │
│ - CModule::IncludeModule('aspro.smartseo') │
│ - Прямой $DB->Query для low-level операций │
│ - ORM Aspro\Smartseo\Models\* для бизнес-логики │
└─────────────────────────────────────────────────────────┘
Ключевой принцип: никакого браузера, никакого графического интерфейса. Всё через CLI.
Что нужно знать об устройстве aspro.smartseo
Модуль хранит данные в 18 таблицах с префиксом b_aspro_smartseo_*. Главные из них:
| Таблица | Назначение |
|---|---|
b_aspro_smartseo_filter_rule | Правила (rule = связка сайт+инфоблок+разделы) |
b_aspro_smartseo_filter_iblock_sections | Привязка разделов к правилу (ManyToMany) |
b_aspro_smartseo_filter_condition | Условия (condition = фильтр по свойствам) |
b_aspro_smartseo_filter_url | Сгенерированные URL (NEW_URL ↔ REAL_URL) |
b_aspro_smartseo_seo_template | SEO-шаблоны (Title/Description/…) |
Иерархия сущностей
FilterRule (FR)
├── IBLOCK_SECTIONS (разделы, к которым применяется)
└── FilterCondition (FC) — условия генерации
└── FilterUrl (FU) — сгенерированные URL
└── SeoTemplate (FU/FC/FR) — SEO-теги и тексты
SEO-шаблоны можно навешивать на любой уровень: правило целиком, условие, конкретный URL. Чем ниже уровень — тем выше приоритет.
8 типов SEO-полей
| CODE | Что это |
|---|---|
META_TITLE | Мета-тег <title> |
META_DESCRIPTION | Мета-тег description |
META_KEYWORDS | Мета-тег keywords |
PAGE_TITLE | H1 страницы |
BREADCRUMB_PAGE | Хлебные крошки |
TOP_DESCRIPTION | Текст над каталогом |
BOTTOM_DESCRIPTION | Текст под каталогом (основной SEO-текст) |
ADDITIONAL_DESCRIPTION | Дополнительный блок |
Типы сущностей для SEO-шаблонов
| Код | Тип | Применение |
|---|---|---|
FR | FilterRule | Все URL правила (общий шаблон) |
FC | FilterCondition | Все URL условия |
FU | FilterUrl | Конкретный URL (точечная настройка) |
Подготовка каталога
1. Свойства — только список (L), не число (N)
Критический момент: умный фильтр требует свойства типа «Список» (L) с pre-определёнными значениями в b_iblock_property_enum. Свойства типа «Число» (N) не работают для генерации посадочных — движок просто не увидит значения.
// ПРАВИЛЬНО
$ibp = new CIBlockProperty;
$ibp->Add([
'NAME' => 'Толщина',
'CODE' => 'TOLSHCHINA_MM',
'IBLOCK_ID' => 85,
'PROPERTY_TYPE' => 'L', // ← список
'LIST_TYPE' => 'L',
'MULTIPLE' => 'N',
'FILTRABLE' => 'Y',
'VALUES' => [
['VALUE' => '40', 'XML_ID' => md5('40'), 'SORT' => 40],
['VALUE' => '50', 'XML_ID' => md5('50'), 'SORT' => 50],
// ... 13 значений от 40 до 250 мм
],
]);
2. Включение умного фильтра для разделов
Aspro.smartseo v1.0.8 читает свойства из старой таблицы b_iblock_section_property (с колонкой SMART_FILTER), а НЕ из новой b_iblock_property_feature. Это важно: даже если вы включили умный фильтр в админке, нужно убедиться, что запись есть в старой таблице.
// SECTION_ID=0 = "для всех разделов инфоблока"
\Bitrix\Iblock\SectionPropertyTable::update(
['IBLOCK_ID' => 85, 'SECTION_ID' => 0, 'PROPERTY_ID' => $propId],
['SMART_FILTER' => 'Y', 'DISPLAY_TYPE' => 'F', 'DISPLAY_EXPANDED' => 'Y']
);
DISPLAY_TYPE управляет визуализацией:
F— флажки/чекбоксы (для перечислений)G— с картинкой/цветом (для RAL)K— радио-кнопкиP— числовой диапазон
3. Товары-болванки
Aspro НЕ генерирует URL для значений фильтра, под которыми нет товаров. Если вы добавили в enum значение «120 мм», но в каталоге нет ни одного товара с этим значением — посадочная не создастся.
Решение: создайте матрицу товаров-болванок, покрывающую все значения всех свойств. Минимум — max(количество значений по свойствам) товаров на раздел.
// 22 товара на раздел (по количеству RAL-цветов) — покрывает все 13 толщин,
// 2 ширины, 22 RAL, 2 цвета, 6 типов профилирования
// Каждому товару даём циклически значения всех свойств
for ($i = 0; $i < 22; $i++) {
$ralIdx = $i; // 0..21
$tolIdx = $i % 13; // циклически 0..12
$shirIdx = $i % 2;
// ... создаём товар с этими значениями
}
CONDITION_TREE — главный формат
Это сердце модуля. Дерево условий хранится в b_aspro_smartseo_filter_condition.CONDITION_TREE как PHP-serialized массив.
Рабочий шаблон
$tree = [
'CLASS_ID' => 'CondGroup',
'DATA' => ['All' => 'AND'], // AND или OR
'CHILDREN' => [
[
'CLASS_ID' => 'CondIBProp:85:1950', // CondIBProp:<IBLOCK>:<PROPERTY>
'DATA' => [
'logic' => 'Equal', // Equal, Not, Contain, ...
'value' => 748, // ← ID enum-значения (НЕ само значение!)
],
],
[
'CLASS_ID' => 'CondIBProp:85:1950',
'DATA' => ['logic' => 'Equal', 'value' => 749], // ещё значение
],
// ... сколько угодно детей = ИЛИ внутри одного свойства
],
];
$conditionTree = serialize($tree);
Ключевые нюансы
value— это enum ID (изb_iblock_property_enum.ID), а не само значение «40»- CLASS_ID имеет формат
CondIBProp:<IBLOCK_ID>:<PROPERTY_ID>— обе цифры обязательны - Несколько детей одного свойства = логика ИЛИ (комбинирование)
- Разные свойства в детях = логика И (пересечение)
Создание условия через ORM
use Aspro\Smartseo\Models\SmartseoFilterConditionTable;
$result = SmartseoFilterConditionTable::add([
'FILTER_RULE_ID' => $ruleId,
'ACTIVE' => 'Y',
'NAME' => 'Толщина',
'URL_TEMPLATE' => '#SITE_DIR#catalog/#SECTION_CODE_PATH#-{#PROPERTY_VALUE#}-mm/',
'URL_TYPE_GENERATE' => 'CM', // CM = комбинирование, MR = слияние
'URL_STRICT_COMPLIANCE'=> 'N',
'CONDITION_TREE' => serialize($tree),
]);
$conditionId = $result->getId();
URL_TEMPLATE — токены
| Токен | Что подставляется |
|---|---|
#SITE_DIR# | Корень сайта (/) |
#SECTION_CODE_PATH# | Путь из кодов разделов (catalog/sendvich-paneli/stenovye-sendvich-paneli) |
#SECTION_CODE# | Код текущего раздела |
{#PROPERTY_VALUE#} | Значение свойства (в фигурных скобках!) |
{#PROPERTY_CODE#} | Код свойства |
#SMART_FILTER_PATH# | Стандартный путь умного фильтра |
Важно: {#PROPERTY_VALUE#} берётся в фигурные скобки, а остальные токены — в решётки. Это не опечатка, так задумано.
URL_TYPE_GENERATE
CM(Комбинирование) — для каждого значения свойства отдельный URLMR(Слияние) — все значения в один URL
Для посадочных под НЧ-запросы почти всегда нужен CM.
Генерация URL через UrlEngine
Самый приятный момент — после создания условий не нужно вручную что-либо генерировать. Движок сам обходит товары, находит совпадения с CONDITION_TREE и создаёт записи в b_aspro_smartseo_filter_url.
use Aspro\Smartseo\Engines\UrlEngine;
$engine = new UrlEngine($conditionId);
$engine->update();
$result = $engine->getResult();
echo "Сгенерировано URL: " . $result['COUNT'];
Движок сам:
- Обходит разделы правила
- Применяет CONDITION_TREE как фильтр к товарам
- Для каждой комбинации «раздел + значение свойства» создаёт URL
- Записывает в БД пару
NEW_URL(ЧПУ) ↔REAL_URL(стандартный путь умного фильтра)
Заливка SEO-тегов и текстов
Структура записей
// INSERT в b_aspro_smartseo_seo_template
// для каждого URL и каждого SEO-полей
foreach ($urls as $urlId => $tags) {
foreach (['META_TITLE', 'META_DESCRIPTION', 'META_KEYWORDS', 'PAGE_TITLE'] as $code) {
$DB->Query("INSERT INTO b_aspro_smartseo_seo_template
(CODE, TEMPLATE, ENTITY_TYPE, ENTITY_ID)
VALUES ('$code', '" . $DB->ForSql($tags[$code]) . "', 'FU', $urlId)");
}
}
Теги региональности
Aspro LiteShop поддерживает теги для подстановки названия города:
| Тег | Падеж | Пример (Москва) |
|---|---|---|
#REGION_NAME# | Именительный | Москва |
#REGION_NAME_DECLINE_RP# | Родительный | Москвы |
#REGION_NAME_DECLINE_PP# | Предложный | Москве |
#REGION_NAME_DECLINE_TP# | Творительный | Москвой |
Эти теги работают в любом SEO-поле, включая BOTTOM_DESCRIPTION. Aspro заменяет их на лету при отдаче страницы — но только если в модуле «Аспро → Региональность» настроены города со склонениями.
Умная замена плейсхолдеров
Если SEO-тексты приходят от копирайтера с универсальным плейсхолдером {город} — нужно автоматически распределить теги по контексту:
import re
def replace_gorod(text):
# "в {город}" / "В {город}" → предложный (в Москве)
text = re.sub(r'\b([вВ])\s+\{город\}', r'\1 #REGION_NAME_DECLINE_PP#', text)
# "для {город}" → родительный (для Москвы)
text = re.sub(r'\b(для|Для)\s+\{город\}', r'\1 #REGION_NAME_DECLINE_RP#', text)
# "с {город}" / "над {город}" → творительный (с Москвой)
text = re.sub(r'\b(с|С|над|под|перед)\s+\{город\}', r'\1 #REGION_NAME_DECLINE_TP#', text)
# Оставшиеся без предлога → именительный (Москва)
text = text.replace('{город}', '#REGION_NAME#')
return text
В нашем проекте 98% случаев были «в/В {город}» → предложный падеж. Это дало корректные фразы «купить в Москве», «доставка в Москве», «специалисты в Москве помогут».
Ошибки, с которыми мы столкнулись
1. Bitrix prolog ругается на output
Симптом: скрипт обрывается сообщением «При выполнении скрипта возникла ошибка. Включить расширенный вывод ошибок…»
Причина: prolog_after.php пытается отправить HTTP-заголовки, а вы уже вывели echo. На CLI это фатально.
Решение: используйте файловый лог вместо echo.
$LOG = __DIR__ . '/script.log';
file_put_contents($LOG, ""); // очищаем в начале
function log_s($m) {
global $LOG;
file_put_contents($LOG, date("H:i:s") . " " . $m . "\n", FILE_APPEND);
}
// Вместо echo:
log_s("Создано записей: $count");
2. Свойство не находится в ConditionResult
Симптом: ConditionResult::getPropertyFields() возвращает пустой массив для свойства, хотя оно явно существует с типом L.
Причина: после конвертации свойства из N в L (UPDATE b_iblock_property SET PROPERTY_TYPE='L' WHERE ID=...) aspro кэширует старое состояние свойства. Свойство видно в БД, но движок его игнорирует.
Решение: полное пересоздание свойства через CIBlockProperty::Add().
// 1) Сохраняем enum и значения товаров
$enums = $DB->Query("SELECT * FROM b_iblock_property_enum WHERE PROPERTY_ID=$oldId")->fetchAll();
$values = $DB->Query("SELECT * FROM b_iblock_element_property WHERE IBLOCK_PROPERTY_ID=$oldId")->fetchAll();
// 2) Удаляем полностью
$DB->Query("DELETE FROM b_iblock_property WHERE ID=$oldId");
$DB->Query("DELETE FROM b_iblock_property_enum WHERE PROPERTY_ID=$oldId");
$DB->Query("DELETE FROM b_iblock_element_property WHERE IBLOCK_PROPERTY_ID=$oldId");
$DB->Query("DELETE FROM b_iblock_section_property WHERE PROPERTY_ID=$oldId");
// 3) Создаём заново
$ibp = new CIBlockProperty;
$newId = $ibp->Add([
'NAME' => 'Ширина',
'CODE' => 'SHIRINA_MM',
'IBLOCK_ID' => 85,
'PROPERTY_TYPE' => 'L',
'LIST_TYPE' => 'L',
'VALUES' => [...],
]);
// 4) Возвращаем значения товарам с новыми enum ID
// 5) Включаем SMART_FILTER заново
После этого aspro видит свойство и генерирует URL. Это самый неочевидный баг, на который мы потратили часы.
3. 0 URL сгенерировано, но товары есть
Симптом: UrlEngine->update() возвращает COUNT=0, хотя CONDITION_TREE корректный и товары со значениями есть.
Причина: либо aspro не видит свойство (см. пункт 2), либо нет товаров, удовлетворяющих условию в конкретном разделе.
Диагностика:
// Проверяем, видит ли ConditionResult свойство
$cr = new \Aspro\Smartseo\Condition\ConditionResult(85, $conditionTreeSerialized);
$fields = $cr->getPropertyFields();
// Если пусто — aspro не нашёл свойство, проблема в кэше типов
4. Двойные префиксы в URL
Симптом: вместо /catalog/...-ral-1014/ получается /catalog/...-ral-ral-1014-slonovaya-kost/.
Причина: aspro берёт VALUE enum и транслитерирует его. Если VALUE = «RAL 1014 (слоновая кость)» — в URL попадёт весь текст.
Решение: храните в VALUE короткое значение, а расшифровку — в отдельном поле или в описании.
-- Было:
UPDATE b_iblock_property_enum SET VALUE='RAL 1014 (слоновая кость)' WHERE ID=718;
-- Стало:
UPDATE b_iblock_property_enum SET VALUE='1014' WHERE ID=718;
И в URL_TEMPLATE оставьте префикс:
#SITE_DIR#catalog/#SECTION_CODE_PATH#-ral-{#PROPERTY_VALUE#}/
5. Опечатки в enum ломают URL
Aspro использует стандартный битриксовый транслит, который не прощает опечаток. «Трапецевидная» → trapets**evi**dnaya, а в ТЗ от SEO-специалиста могло быть trapecevidnaya (без ts).
Решение: либо менять enum VALUE под битриксовый транслит, либо настраивать правила транслитерации. Проще первое.
6. ORM cache ломает данные
После любого INSERT в b_aspro_smartseo_seo_template обязательно сбрасывайте кэш:
$BX_CACHE = \Bitrix\Main\Application::getInstance()->getManagedCache();
$BX_CACHE->cleanDir('aspro_smartseo');
$BX_CACHE->cleanDir('b_iblock');
// И физическое удаление файлов кэша
shell_exec('rm -rf ' . $_SERVER['DOCUMENT_ROOT'] . '/bitrix/managed_cache/MYSQL/*');
shell_exec('rm -rf ' . $_SERVER['DOCUMENT_ROOT'] . '/bitrix/cache/*');
Иначе вы увидите старые SEO-теги на странице даже после заливки новых.
7. SITE_ID в правиле решает
Правило привязывается к SITE_ID. Если у вас многосайтовость — правило для s1 НЕ будет работать на s2. Нужно создавать отдельное правило для каждого сайта или убедиться, что целевой домен использует нужный SITE_ID.
В нашем случае dev.all4stroika.ru использует SITE_ID=s2 — и это боевой сайт, несмотря на название.
Фишки, которые теперь доступны через ИИ-агента
1. Массовое добавление значений свойств
Нужно добавить новые толщины (300, 350 мм)? Раньше — кликать в админке. Теперь:
foreach ([300, 350] as $v) {
$DB->Query("INSERT INTO b_iblock_property_enum
(PROPERTY_ID, VALUE, XML_ID, SORT, DEF)
VALUES (1950, '$v', '" . md5($v) . "', $v, 'N')");
}
// Перегенерация URL
$engine = new UrlEngine(5);
$engine->update();
30 секунд вместо 10 минут в UI.
2. Массовое обновление SEO-тегов
Получили от копирайтера обновлённые Title для 100 страниц? Заливаем одной транзакцией:
$DB->Query("DELETE FROM b_aspro_smartseo_seo_template
WHERE ENTITY_TYPE='FU' AND CODE='META_TITLE'
AND ENTITY_ID IN (" . implode(',', $urlIds) . ")");
foreach ($newTitles as $urlId => $title) {
$DB->Query("INSERT INTO b_aspro_smartseo_seo_template
(CODE, TEMPLATE, ENTITY_TYPE, ENTITY_ID)
VALUES ('META_TITLE', '" . $DB->ForSql($title) . "', 'FU', $urlId)");
}
3. Автоматическая генерация SEO-текстов под новые посадочные
Если вы добавили новое значение свойства — ИИ-агент может сгенерировать SEO-текст по шаблону существующих текстов и сразу залить:
# Агент читает существующие тексты, изучает структуру,
# генерирует новый по аналогии и подставляет теги регионов
template_text = generate_text_for_new_thickness(300) # AI-generated
text = replace_gorod(template_text) # подставляем #REGION_NAME_*#
4. Тестирование изменений без риска
Все скрипты идемпотентны: их можно запускать многократно. Перед запуском на проде — сухой прогон с echo SQL-запросов вместо $DB->Query(). После проверки — боевой запуск.
5. Дамп конфигурации для аудита
Любой момент времени можно задампить всю конфигурацию в JSON:
$config = [
'rules' => [...],
'conditions' => [...],
'urls' => [...],
'seo_templates' => [...],
];
file_put_contents('audit_' . date('Y-m-d') . '.json', json_encode($config, JSON_PRETTY_PRINT));
И сравнить с предыдущим дампом через diff — увидите все изменения.
6. Автоматический матчинг ТЗ и БД
ТЗ от SEO-специалиста приходит в Excel/CSV. ИИ-агент:
- Парсит файл
- Сравнивает URL из ТЗ с URL в БД
- Находит расхождения (новые посадочные, удалённые, переименованные)
- Догенерирует недостающее
- Обновляет изменённые теги
Всё без ручной сверки таблиц.
Безопасная работа на проде
1. Бэкап перед изменениями
# Дамп ключевых таблиц
mysqldump -u user -p db \
b_aspro_smartseo_filter_rule \
b_aspro_smartseo_filter_condition \
b_aspro_smartseo_filter_url \
b_aspro_smartseo_seo_template \
b_iblock_property b_iblock_property_enum \
b_iblock_section_property \
> backup_$(date +%Y%m%d_%H%M).sql
2. Доступ только через SSH-ключи
Никаких паролей в скриптах. SSH-ключи на сервере, права chmod 600.
3. Идемпотентные скрипты
Каждый скрипт должен:
- Сначала проверять, существует ли уже запись
- Если да — обновлять, если нет — создавать
- Не падать на повторном запуске
$exist = CIBlockElement::GetList(
[],
['IBLOCK_ID' => 85, 'CODE' => $code],
false, false, ['ID']
)->Fetch();
if ($exist) {
CIBlockElement::SetPropertyValuesEx($exist['ID'], 85, $props);
// обновляем
} else {
$el->Add($fields); // создаём
}
4. Логирование каждого шага
Все скрипты пишут в /home/.../scripts_seo/stepN.log. Если что-то пошло не так — у вас полный аудит-лог.
Чеклист для запуска на новом проекте
- Настроен SSH-доступ по ключам
- Найден PHP для CLI (для Битрикс 26 нужен 8.2+)
- Определён IBLOCK_ID целевого каталога
- Определён SITE_ID целевого домена
- Свойства созданы с типом L (не N, не S)
- SMART_FILTER=Y для всех нужных разделов (в старой таблице
b_iblock_section_property) - Созданы товары-болванки, покрывающие все значения свойств
- Сброшен кэш after каждого шага
- Сделан бэкап таблиц aspro.smartseo перед началом
- Настроен модуль aspro.lite regions с городами и склонениями
Итог
ИИ-агент через SSH — это не «обход админки». Это продуктивный интерфейс для массовых операций с aspro.smartseo.
Что мы получили:
- 354 посадочные страницы за 1 день работы (вместо недели ручного кликанья)
- 1705 SEO-тегов + 317 SEO-текстов (3000+ символов каждый)
- Корректные теги региональности во всех полях
- Полная история изменений в git
- Идемпотентные скрипты, которые можно переиспользовать
Что стало возможным:
- Добавить новое значение свойства → 30 секунд на заливку и перегенерацию
- Обновить 100 Title → один SQL-запрос
- Провести аудит → один SELECT
- Откатить изменения → один mysqldump-restore
Графический интерфейс остаётся для единичных правок и визуализации. Для всего остального — есть SSH.
Приложение: полезные SQL-запросы
Сводка по правилам и условиям
SELECT
r.ID as rule_id, r.NAME as rule_name, r.SITE_ID,
COUNT(DISTINCT c.ID) as conditions,
COUNT(DISTINCT u.ID) as urls
FROM b_aspro_smartseo_filter_rule r
LEFT JOIN b_aspro_smartseo_filter_condition c ON c.FILTER_RULE_ID = r.ID
LEFT JOIN b_aspro_smartseo_filter_url u ON u.FILTER_CONDITION_ID = c.ID
GROUP BY r.ID;
Найти URL без SEO-тегов
SELECT u.NEW_URL
FROM b_aspro_smartseo_filter_url u
LEFT JOIN b_aspro_smartseo_seo_template t
ON t.ENTITY_TYPE='FU' AND t.ENTITY_ID=u.ID
WHERE t.ID IS NULL;
Сравнить SITE_ID правила с фактическим SITE_ID домена
SELECT r.ID, r.NAME, r.SITE_ID, s.NAME as site_name, s.SERVER_NAME
FROM b_aspro_smartseo_filter_rule r
LEFT JOIN b_lang s ON s.LID = r.SITE_ID;
Все URL с их SEO-тегами
SELECT
u.NEW_URL,
MAX(CASE WHEN t.CODE='META_TITLE' THEN t.TEMPLATE END) as title,
MAX(CASE WHEN t.CODE='PAGE_TITLE' THEN t.TEMPLATE END) as h1,
MAX(CASE WHEN t.CODE='META_DESCRIPTION' THEN t.TEMPLATE END) as description
FROM b_aspro_smartseo_filter_url u
LEFT JOIN b_aspro_smartseo_seo_template t
ON t.ENTITY_TYPE='FU' AND t.ENTITY_ID=u.ID
GROUP BY u.ID;
Кейс по проекту all4stroika.ru, июль 2026. Автор — Евгений Урядов, automata.sale: 20+ лет опыта с 1С-Битрикс, партнёр Аспро. Полный набор идемпотентных PHP-скриптов и ТЗ доступен в git-репозитории проекта.