История про то, как задача «разбить sitemap на части» превратилась в отдельный модуль. Спойлер: не потому, что разбиение сложное — оно тривиальное, — а потому, что делать это внутри стандартного SEO-модуля означало подписаться на его сопровождение при каждом обновлении Битрикса.
Проблема
Сначала уточню исходную посылку, потому что она гуляет по SEO-статьям в неверном виде. Утверждение «Google не принимает sitemap больше 4000 ссылок» неверно: ограничение спецификации — 50 000 URL и 50 МБ в несжатом виде на файл, и оно едино для всех поисковых систем. Никакого требования про 4000 не существует.
Разбивать файлы всё равно правильно, но по инженерным, а не поисковым причинам:
- генерация не упирается в лимиты PHP — сборка одного файла на сотни тысяч URL съедает память и время;
- диагностика по частям — в Search Console статистика показывается для каждого файла отдельно, и разбиение по инфоблокам сразу показывает, где просели страницы;
- частичная перегенерация — при изменениях в одном инфоблоке не нужно перестраивать всё.
Так что 4000 в настройках — это разумно выбранное значение, а не соответствие чужому требованию. Понимать разницу важно: из «требования» вырастает жёсткая константа в коде, из «настройки» — параметр, который можно поменять.
Стандартный SEO-модуль Битрикс не умеет разбивать карты сайта на части — отсюда задача.
Почему не расширять стандартный модуль
Рассмотрели вариант модификации встроенного функционала, но выявили серьёзные риски:
- Зависимость от модуля
bitrix/modules/seo - Опасность поломки при обновлениях системы
- Сложность диагностики проблем
- Затруднённая работа других разработчиков
Вместо переопределения класса Bitrix\Seo\Sitemap\Generator решили создать полностью независимый модуль.
Отдельно про «не расширять стандартный модуль». Аргументы выше верны, но у решения есть и обратная сторона, которую стоит проговорить: свой модуль означает, что вы сами сопровождаете всё — генерацию, админку, обработку ошибок, совместимость с новыми версиями PHP. Стандартный модуль это делает вендор.
Граница проходит примерно так: если расширение требует переопределять внутренние методы чужого класса — пишите своё, потому что чужие внутренности сломаются при обновлении и молча. Если задача решается штатными точками расширения (события, настройки) — не плодите сущности.
В этом случае выбор в пользу своего модуля оправдан: Bitrix\Seo\Sitemap\Generator точек расширения для разбиения не предоставляет.
Требования к решению
- Отдельный модуль в папке
local/modules/client.sitemapgenerator - Собственный интерфейс, визуально похожий на стандартный
- Разбиение файлов инфоблоков по 4000 URL
- Создание отдельных карт для статических файлов
- Поддержка региональных копий для проектов на Aspro Max
- Автоматическое обновление robots.txt
Архитектура модуля
Основные компоненты
local/modules/client.sitemapgenerator/
├── install/
│ └── index.php # Регистрация и создание таблиц
├── admin/
│ └── sitemap_list.php # Админ-интерфейс
├── lib/
│ ├── Service/
│ │ ├── Generator.php # Основной движок
│ │ ├── ChunkedXmlWriter.php # Вывод XML с разбиением
│ │ └── JobRunner.php # Пошаговое выполнение
│ └── Model/
│ └── ProfileTable.php # ORM-таблица профилей
└── include.phpСтруктура базы данных
Модуль использует четыре собственные таблицы:
-- Профили генерации
CREATE TABLE b_client_sitemap_profile (
ID int AUTO_INCREMENT PRIMARY KEY,
NAME varchar(255),
SITE_ID char(2),
DOMAIN varchar(255),
ACTIVE char(1) DEFAULT 'Y',
CREATED_AT datetime,
UPDATED_AT datetime
);
-- Параметры инфоблоков
CREATE TABLE b_client_sitemap_profile_iblock (
ID int AUTO_INCREMENT PRIMARY KEY,
PROFILE_ID int,
IBLOCK_ID int,
PRIORITY decimal(2,1) DEFAULT 0.5,
CHANGEFREQ varchar(20) DEFAULT 'weekly'
);
-- Правила по разделам
CREATE TABLE b_client_sitemap_profile_rule (
ID int AUTO_INCREMENT PRIMARY KEY,
PROFILE_ID int,
SECTION_ID int,
INCLUDE char(1) DEFAULT 'Y'
);
-- Состояние процессов генерации
CREATE TABLE b_client_sitemap_job (
ID int AUTO_INCREMENT PRIMARY KEY,
PROFILE_ID int,
STATUS varchar(50),
PROGRESS int DEFAULT 0,
CURRENT_STEP varchar(255),
STARTED_AT datetime,
FINISHED_AT datetime
);Важно: Модуль полностью изолирован от таблиц стандартного SEO-модуля. Это гарантирует отсутствие конфликтов при обновлениях.
Реализация разбиения на части
ChunkedXmlWriter
<?php
namespace Client\SitemapGenerator\Service;
class ChunkedXmlWriter
{
private const MAX_URLS_PER_FILE = 4000;
private string $basePath;
private string $baseName;
private int $currentChunk = 1;
private int $urlCount = 0;
private array $chunks = [];
public function __construct(string $basePath, string $baseName)
{
$this->basePath = $basePath;
$this->baseName = $baseName;
}
public function addUrl(string $loc, string $lastmod, string $changefreq, float $priority): void
{
if ($this->urlCount >= self::MAX_URLS_PER_FILE) {
$this->finishCurrentChunk();
$this->currentChunk++;
$this->urlCount = 0;
}
$this->getCurrentWriter()->startElement('url');
$this->getCurrentWriter()->writeElement('loc', $loc);
$this->getCurrentWriter()->writeElement('lastmod', $lastmod);
$this->getCurrentWriter()->writeElement('changefreq', $changefreq);
$this->getCurrentWriter()->writeElement('priority', $priority);
$this->getCurrentWriter()->endElement();
$this->urlCount++;
}
public function getChunkFiles(): array
{
return $this->chunks;
}
private function getChunkFileName(): string
{
if ($this->currentChunk === 1) {
return $this->baseName . '.xml';
}
return $this->baseName . '-' . $this->currentChunk . '.xml';
}
}Результат разбиения
Вместо одного крупного файла sitemap-iblock-25.xml генерируются:
sitemap-iblock-25-1.xml(до 4000 URL)sitemap-iblock-25-2.xml(до 4000 URL)sitemap-iblock-25-3.xml(до 4000 URL)
Пошаговая генерация
Таблица b_client_sitemap_job позволяет реализовать асинхронную генерацию без риска timeout-ов:
<?php
namespace Client\SitemapGenerator\Service;
class JobRunner
{
private const BATCH_SIZE = 500;
public function runStep(int $jobId): bool
{
$job = $this->loadJob($jobId);
switch ($job['STATUS']) {
case 'pending':
return $this->startJob($job);
case 'processing_iblock':
return $this->processIblockBatch($job);
case 'processing_static':
return $this->processStaticFiles($job);
case 'finalizing':
return $this->finalize($job);
default:
return false;
}
}
private function processIblockBatch(array $job): bool
{
$elements = \CIBlockElement::GetList(
['ID' => 'ASC'],
[
'IBLOCK_ID' => $job['CURRENT_IBLOCK_ID'],
'>ID' => $job['LAST_ELEMENT_ID'],
],
false,
['nTopCount' => self::BATCH_SIZE],
['ID', 'DETAIL_PAGE_URL', 'TIMESTAMP_X']
);
$count = 0;
while ($element = $elements->GetNext()) {
$this->writer->addUrl(
$element['DETAIL_PAGE_URL'],
$element['TIMESTAMP_X'],
'weekly',
0.8
);
$count++;
$job['LAST_ELEMENT_ID'] = $element['ID'];
}
$this->saveJob($job);
// Возвращаем true, если есть ещё данные для обработки
return $count === self::BATCH_SIZE;
}
}Интеграция в админку
Интерфейс повторяет логику стандартного SEO-модуля:
<?php
// admin/sitemap_list.php
require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_admin_before.php');
\Bitrix\Main\Loader::includeModule('client.sitemapgenerator');
$APPLICATION->SetTitle('Генератор Sitemap');
// Отображение списка профилей
$profiles = \Client\SitemapGenerator\Model\ProfileTable::getList([
'order' => ['ID' => 'DESC'],
]);
// Форма создания/редактирования профиля
// Кнопки запуска генерации
// Статус текущих задачПреимущества реализации
Сохранение целостности ядра
- Возможность обновления Битрикса без конфликтов
- Простота отката путём отключения модуля
- Отсутствие побочных эффектов на другой функционал
Контролируемое разбиение файлов
Каждый файл содержит не более 4000 ссылок — значение вынесено в настройку профиля, а не зашито в код. Верхняя граница, за которую выходить нельзя, — 50 000 URL и 50 МБ по спецификации.
Пошаговая генерация
- Нет риска timeout-ов на больших каталогах
- Возможность продолжить процесс после перезапуска
- Отслеживание прогресса в интерфейсе
Пошаговая генерация — главное решение в этом модуле
Из всего перечисленного именно она отличает работающий инструмент от скрипта, который «в целом работает».
Генерация sitemap для крупного каталога — это минуты работы и сотни тысяч записей. Любой однопроходный вариант рано или поздно упирается в max_execution_time или в память, причём на боевом каталоге, а не на тестовом. Пошаговая схема с сохранением позиции превращает отказ из катастрофы в паузу: процесс продолжается с того места, где остановился.
Побочные выгоды тоже существенные: виден прогресс (а значит, никто не жмёт «сгенерировать» второй раз, думая, что зависло), и генерацию можно вести из админки, не уходя в cron.
Итоги
Изначальная задача звучала как «разбить один файл на несколько», а закончилась отдельным модулем — и это типичная траектория для правок, затрагивающих чужой код. Полезно уметь распознавать её заранее: если решение требует лезть во внутренности вендорского класса, оценивайте его сразу как отдельный модуль, а не как «правку на пару часов».
Что стоит забрать из этого разбора независимо от sitemap:
- Проверяйте исходные требования. «Google не принимает больше 4000» оказалось мифом, и на нём чуть не выросла жёсткая константа в коде вместо настройки.
- Генерация чего угодно объёмного должна быть возобновляемой. Не «оптимизируем потом», а с самого начала: на боевых данных однопроходные скрипты падают всегда.
- Граница «расширять или писать своё» проходит по точкам расширения. Есть штатные — используйте их. Нет — пишите отдельное, но с ясным пониманием, что сопровождение теперь ваше.
Ключевой вывод: Критичную бизнес-логику лучше выносить в отдельные модули с прозрачной структурой, нежели глубоко интегрировать в существующий функционал.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.