
Когда производитель с дилерской сетью из 85+ партнёров решает стандартизировать их сайты — первый инстинкт понятен: сделать JSON-фид с основного домена, настроить синхронизацию, и пусть у всех будет одинаковый актуальный каталог.
Проблема в том, что «одинаковый» и «хорошо индексируемый» — противоречащие друг другу требования, если реализовано наивно. Яндекс видит 85 сайтов с идентичным контентом и поступает предсказуемо.
Под катом — как мы решали эту задачу для крупнейшего производителя сельхозтехники в России (Ростов-на-Дону, 150+ моделей комбайнов и тракторов, 50 000 SKU запчастей): архитектура Lock/Edit на уровне инфоблоков Битрикс, Manticore Search для артикулов со спецсимволами, OpenAI Batch API для обогащения каталога и Python-автоматизация развёртывания 85 поддоменов.
Постановка задачи
Требования производителя:
Единый каталог техники у всех дилеров (актуальные ТТХ, официальные фото, единая терминология)
Централизованное обновление — изменил на основном домене, разошлось по всем
Brand Safety — дилер не может испортить продуктовый контент
Требования дилеров:
Региональное SEO — позиции по запросам «купить комбайн [город]»
Локальный коммерческий контент — цены, остатки, условия лизинга, местный сервис
Уникальность в глазах поисковика — не быть дублём основного домена
Техническое ограничение: объект КИИ → только российские платформы и сертифицированное ПО → 1С-Битрикс как CMS.
Проблема Full Overwrite
Стандарт, разработанный для дилерской сети, требовал полного дублирования каталога через JSON-фид. Фид обновляется раз в сутки.
Первоначальная реализация — Full Overwrite: при каждом обновлении фид перезаписывает все поля элемента инфоблока. В Битрикс событие OnBeforeIBlockElementUpdate не блокирует перезапись из внешнего источника по умолчанию.
Последствия аудита одного из дилеров (декабрь 2025):
Органический трафик по гео-запросам: −73% — Яндекс склеил поддомен с основным доменом как дубль
Конверсия в заявку: 0.05% — сайт работает как справочник
Любые SEO-правки дилера (Title, Description) аннулировались через 24 часа
Архитектура Lock/Edit
Решение — разделить поля инфоблока на два типа с разными правилами синхронизации.
Классификация полей
Lock-поля (перезаписываются фидом, управляет производитель):
NAME— название моделиARTICLE— артикулPREVIEW_TEXT/DETAIL_TEXT— базовое описаниеPREVIEW_PICTURE/DETAIL_PICTURE— фотографииPROPERTY_SPECS_*— технические характеристики
Edit-поля (защищены от перезаписи, управляет дилер):
PROPERTY_LOCAL_SEO_TITLE— региональный заголовок страницыPROPERTY_LOCAL_DESCRIPTION_APPEND— региональное дополнение к описаниюPROPERTY_DEALER_COMMERCIAL_BLOCK— цены, условия, акцииPROPERTY_DEALER_SERVICES— услуги дилера (сервис, лизинг, трейд-ин)
Реализация через обработчик событий
php
<?php // /local/php_interface/init.php AddEventHandler( "iblock", "OnBeforeIBlockElementUpdate", ["FederatedSync", "ProtectEditFields"] ); class FederatedSync { // Список Edit-полей, защищённых от перезаписи фидом private static array $editFields = [ 'LOCAL_SEO_TITLE', 'LOCAL_DESCRIPTION_APPEND', 'DEALER_COMMERCIAL_BLOCK', 'DEALER_SERVICES', ]; public static function ProtectEditFields(array &$arFields): void { // Срабатываем только на обновления из фида if (empty($arFields['FROM_FEED'])) { return; } $elementId = (int)$arFields['ID']; if (!$elementId) { return; } // Получаем текущие значения Edit-полей до перезаписи $res = CIBlockElement::GetByID($elementId); if (!$current = $res->GetNextElement()) { return; } $currentProps = $current->GetProperties(); // Восстанавливаем Edit-поля foreach (self::$editFields as $code) { if (isset($currentProps[$code])) { $arFields['PROPERTY_VALUES'][$code] = $currentProps[$code]['VALUE']; } } } }
Флаг FROM_FEED устанавливается в агенте синхронизации при вызове CIBlockElement::Update(). Это позволяет отличать обновления из фида от ручных правок в админке.
Структура поддоменов
redbrand-selmash.com ← основной домен, каталог-эталон ├── krasnodar.redbrand-selmash.com ← дилер Краснодар ├── omsk.redbrand-selmash.com ← дилер Омск ├── voronezh.redbrand-selmash.com ← дилер Воронеж └── ... (85 поддоменов)
Каждый поддомен — отдельный сайт в Битрикс Multisite, подключённый к тому же инфоблоку каталога, но с собственными Edit-полями и шаблоном.
Manticore Search: поиск артикулов со спецсимволами
Проблема
B2B-клиент знает точный артикул запчасти и ищет именно по нему. Один и тот же артикул может быть записан по-разному:
RSM 101-05-02RSM-101.05.02RSM101РСМ 101/05/02
Дефолтный MySQL Full-Text Search в Битрикс не справляется с такими запросами — он воспринимает дефис, точку и слэш как разделители и ищет по частям.
Решение: Manticore Search
Manticore Search — это форк Sphinx с активной разработкой, совместимый с MySQL-протоколом. Интегрируется с Битрикс через компонент bitrix.search.
Конфиг (manticore.conf):
ini
index bitrix_catalog { source = bitrix_catalog_source path = /var/lib/manticore/data/catalog # Таблица символов: цифры и латиница с приведением к нижнему регистру charset_table = 0..9, A..Z->a..z, _, U+410..U+42F->U+430..U+44F # КЛЮЧЕВАЯ НАСТРОЙКА: игнорируемые символы-разделители ignore_chars = -, ., /, \, _ min_word_len = 1 morphology = lemmatize_ru_all min_prefix_len = 3 expand_keywords = 1 } source bitrix_catalog_source : bitrix_catalog_base { sql_query = \ SELECT be.ID, be.NAME, bep.VALUE as ARTICLE \ FROM b_iblock_element be \ LEFT JOIN b_iblock_element_property bep \ ON bep.IBLOCK_ELEMENT_ID = be.ID \ AND bep.IBLOCK_PROPERTY_ID = ( SELECT ID FROM b_iblock_property WHERE CODE = 'ARTICLE' LIMIT 1 ) \ WHERE be.IBLOCK_ID = 5 AND be.ACTIVE = 'Y' }
Параметр ignore_chars — ключевой. Он заставляет Manticore игнорировать указанные символы как при индексации, так и при поиске. В итоге RSM-101.05.02 и RSM 101-05-02 индексируются и ищутся как RSM10105 02 — одинаково.
Результат: поиск находит товар по любому написанию артикула. Время ответа на запросы по каталогу 50k позиций — до 50ms.
OpenAI Batch API: AI-генерация описаний
Проблема
В 1С дилера 50 000 SKU запчастей. Описания — технические коды из конструкторской документации: «Вал 10.01.05», «Кронштейн 23.11.200-А». Фото есть у ~5% позиций. Каталог выглядит как Excel-выгрузка.
Парсинг каталога с сайта производителя запрещён политикой. Нанять копирайтеров на 50 000 описаний — ~400 часов работы.
Решение: Batch API
OpenAI Batch API позволяет отправить до 50 000 запросов одним пак��том. Обрабатывается в течение 24 часов. Стоимость — вдвое дешевле синхронного API.
Шаг 1: Выгрузка из 1С
python
import csv # CSV из 1С: артикул, название with open('parts_1c.csv', 'r', encoding='utf-8') as f: reader = csv.DictReader(f) parts = list(reader) print(f"Позиций к обработке: {len(parts)}") # Позиций к обработке: 49847
Шаг 2: Формирование JSONL
python
import json def build_prompt(name: str, sku: str) -> str: return f"""Ты — инженер-технолог производителя сельхозтехники. Запчасть: {name} (Артикул: {sku}) Напиши: 1. Описание детали (2-3 предложения) — что это, для чего нужна 2. Категорию узла (Гидравлика / Трансмиссия / Ходовая / Двигатель / Электрика / Кабина / Жатка / Другое) 3. ГОСТ или стандарт, если очевиден из названия Ответ строго в JSON без markdown: {{"description": "...", "category": "...", "gost": "..."}}""" with open('batch_input.jsonl', 'w', encoding='utf-8') as f: for i, part in enumerate(parts): request = { "custom_id": f"part-{part['SKU']}", "method": "POST", "url": "/v1/chat/completions", "body": { "model": "gpt-4o-mini", "messages": [ {"role": "user", "content": build_prompt( part['NAME'], part['SKU'] )} ], "max_tokens": 300, "response_format": {"type": "json_object"} } } f.write(json.dumps(request, ensure_ascii=False) + '\n')
Шаг 3: Отправка и ожидание
python
from openai import OpenAI client = OpenAI() # Загружаем файл with open('batch_input.jsonl', 'rb') as f: batch_file = client.files.create(file=f, purpose='batch') # Создаём батч batch = client.batches.create( input_file_id=batch_file.id, endpoint='/v1/chat/completions', completion_window='24h' ) print(f"Batch ID: {batch.id}") # Batch ID: batch_abc123... # Ждём 24 часа
Шаг 4: Импорт в Битрикс
python
import json from bitrix24 import Bitrix24 # или через CIBlockElement напрямую results = {} with open('batch_output.jsonl', 'r', encoding='utf-8') as f: for line in f: item = json.loads(line) sku = item['custom_id'].replace('part-', '') try: content = json.loads( item['response']['body']['choices'][0]['message']['content'] ) results[sku] = content except (KeyError, json.JSONDecodeError): continue # Импорт в свойство AI_DESCRIPTION инфоблока for sku, data in results.items(): # Находим элемент по артикулу и обновляем свойство CIBlockElement::SetPropertyValueCode( element_id, 'AI_DESCRIPTION', data['description'] )
Экономика:
49 847 запросов × $0.000150 (gpt-4o-mini Batch input) = $7.48
Альтернатива: 400 часов × $15/час = $6 000
Множитель экономии: 800×
Автоматизация: 85 Schema.org за 15 минут
Проблема
Развернуть 85 поддоменов с уникальным локальным контентом вручную — 340 часов (85 × 4 часа на регион). При этом для каждого нужна разметка Schema.org с точными координатами склада.
Скрипт геокодирования и генерации Schema.org
python
import requests import json from pathlib import Path YANDEX_API_KEY = "your_key_here" def geocode_address(address: str) -> tuple[float, float] | tuple[None, None]: """Получаем координаты адреса через Yandex Geocoder API""" url = "https://geocode-maps.yandex.ru/1.x/" params = { "geocode": address, "apikey": YANDEX_API_KEY, "format": "json" } response = requests.get(url, params=params, timeout=5) data = response.json() try: members = data['response']['GeoObjectCollection']['featureMember'] pos = members[0]['GeoObject']['Point']['pos'] lon, lat = pos.split() return float(lat), float(lon) except (KeyError, IndexError, ValueError): return None, None def generate_schema(dealer: dict, lat: float, lon: float) -> dict: """Генерируем Schema.org AutoDealer для дилера""" return { "@context": "https://schema.org", "@type": "AutoDealer", "name": dealer['name'], "url": f"https://{dealer['subdomain']}.основнойдомен.ru", "telephone": dealer['phone'], "email": dealer.get('email', ''), "address": { "@type": "PostalAddress", "streetAddress": dealer['street'], "addressLocality": dealer['city'], "addressRegion": dealer['region'], "postalCode": dealer['postal_code'], "addressCountry": "RU" }, "geo": { "@type": "GeoCoordinates", "latitude": lat, "longitude": lon }, "openingHoursSpecification": [ { "@type": "OpeningHoursSpecification", "dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], "opens": "09:00", "closes": "18:00" } ] } # Загружаем дилеров из CRM (CSV или API) dealers = load_dealers_from_crm() # ваша функция output_dir = Path("schema_output") output_dir.mkdir(exist_ok=True) for dealer in dealers: lat, lon = geocode_address( f"{dealer['city']}, {dealer['street']}" ) if lat is None: print(f"⚠️ Не удалось геокодировать: {dealer['name']}") continue schema = generate_schema(dealer, lat, lon) output_path = output_dir / f"{dealer['subdomain']}.json" with open(output_path, 'w', encoding='utf-8') as f: json.dump(schema, f, ensure_ascii=False, indent=2) print(f"✅ Сгенерировано {len(list(output_dir.glob('*.json')))} файлов") # ✅ Сгенерировано 85 файлов
Итоговые файлы импортируются в шаблоны Битрикс через REST API.
DSAC: сезонный контент по матрице
Техническая деталь, которую часто упускают при работе с агрорынком: фермер выбирает технику за 2–3 месяца до сезона. Контент, опубликованный в начале посевной, уже опоздал.
DSAC (Dynamic Seasonally-Adaptive Content) — это матрица запуска контента на поддоменах с опережением на 6–8 недель:
Месяц публикации | Регион | Триггер | Тип контента |
|---|---|---|---|
Январь | Краснодарский край | Подготовка к сверхранней посевной | Статья + лендинг трактора |
Март | Центральная Россия | Подготовка к посевной | Сравнение моделей + калькулятор |
Май | Омская область | Старт посевной в Сибири | Акция на технику + условия лизинга |
Август | Краснодарский край | Уборка пропашных | Настройки жатки для подсолнечника |
SEO-эффект накопительный: поддомены, запущенные в январе 2026, должны выйти в топ по сезонным запросам к маю. Это и есть контрольная точка для оценки результата.
Что не сделали и почему
Server-Side Tracking — в бэклоге. Стандартная Яндекс.Метрика с Opt-In баннером теряет 70–80% данных из-за 152-ФЗ. SST решает проблему, но требует отдельного внедрения на каждом поддомене. Планируем на Q2 2026.
A/B-тестирование DSAC-копирайта — нужен минимальный трафик для статистической значимости. Запускаем после накопления базы.
Интеграция с Яндекс.Бизнес API — для автоматической синхронизации складских остатков и часов работы на картах. API нестабильный, ждём стабильной версии.
Итого: что построено за 120 часов
Команда: 4 человека (аналитик, разработчик, SEO, PM).
Компонент | Технология | Результат |
|---|---|---|
Lock/Edit архитектура | Bitrix Event Handlers | Фид не ломает SEO дилеров |
Мультисайт | Bitrix Multisite | 85 поддоменов на одном инстансе |
Поиск артикулов | Manticore Search | Поиск по любому написанию артикула |
AI-описания | OpenAI Batch API (gpt-4o-mini) | 50k описаний за $7.50 |
Python + Yandex Geocoder | 85 файлов за 15 минут | |
Сезонный контент | DSAC-матрица | 12-месячный план по регионам |
Бизнес-результат оцениваем в июле 2026 — пик уборочной кампании. Метрики: лиды с поддоменов vs основной домен, позиции по гео-запросам, конверсия.
Теги: #bitrix #manticore #sphinx #seo #openai #b2b #агромаркетинг #cms
Олег Линьков, CEO & Tech Lead Webformula
С 2012 года разрабатываем цифровую инфраструктуру для крупнейших производителей сельхозтехники, спецтехники, средств защиты растений, селекционеров и производителей семян в России. Внедряем федеративные архитектуры данных и методологию DSAC.
