Главная/Статьи/Собственный модуль генерации sitemap для 1С-Битрикс без правок ядра

Собственный модуль генерации sitemap для 1С-Битрикс без правок ядра

Создаём независимый модуль для генерации карт сайта с разбиением по 4000 URL. Решаем проблему Google, который не принимает большие Sitemap-файлы.

ДМ
Дмитрий Мещеряков
📅 5 июня 2026 г.📖 7 мин чтения

История про то, как задача «разбить 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

Архитектура модуля

Основные компоненты

text
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

Структура базы данных

Модуль использует четыре собственные таблицы:

sql
-- Профили генерации
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
<?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
<?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
<?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.