Всё началось с банальной задачи — я и моя напарница нейросеть Асси задумались над SEO-продвижением и адаптацией нашего сайта под современные реалии генеративного поиска (GEO / RAG) и краулеры языковых моделей (GPTBot, ClaudeBot, Perplexity). По старой привычке решили поглядеть, как эту задачу решают другие «взрослые дяди», открыли исходники популярных решений для синдикации в современных CMS… и, мягко говоря, офигели. То, что в корпоративном BigTech считается стандартом генерации банальной XML-ленты, на поверку оказалось кромешным инфраструктурным адом.

Анатомия корпоративного ада: Битрикс и остальные монстры.

Когда краулер стучится за RSS к типичному энтерпрайз-движку, на сервере начинается сущий кошмар, судите сами:

  1. Bitrix (Король тормозов): Это вообще отдельный котел для мазохистов. Чтобы отдать к примеру 15 новостей, Битрикс поднимает всё своё монструозное ядро, подключает prolog_before.php, инициализирует тысячи констант и лезет в базу через ORM, которая генерирует SQL-запросы длиной в километр. Если у вас «Композитный сайт» — готовьтесь к тому, что кэш будет инвалидироваться дольше, чем бот ждет ответа. Итог: сервак потеет, память жрётся, а краулер получает ответ через 500–800 мс. За это время можно было бы запустить ракету в космос.

  2. WordPress: Тут просыпается «прожорливое чудовище». WP_Query делает каскадные запросы к неоптимизированной базе, вытягивая метаданные и мусор. Потом это всё прогоняется через ад из сотен хуков и фильтров. Если стоят плагины типа Yoast — они перелопачивают строки в ОЗУ по кругу. Результат: 200–300 мс на ровном месте.

  3. Magento / Drupal: Тут вообще тушите свет. Чтобы выплюнуть тег , система оборачивает файл в десяток объектов, проверяет права доступа через три слоя абстракций и тратит прорву ресурсов на сериализацию.

    Unix-way или наш ответ Чемберлену.

Мы выкинули на мороз все зависимости. Наша логика простая как выстрел: один прямой UNION-запрос (собираем данные сразу из нескольких таблиц за один заход), кристально чистая потоковая буферизация в XML-строку и жёсткий роутинг. Никакого мусора, только чистые такты процессора. Чтобы не дёргать базу при каждом чихе ИИ-краулера, мы используем стратегию дефрагментации на диск. Скрипт отрабатывает и сохраняет статический rss.xml.

try {
    // Путь к файлу и конфигурация
    $rss_file = ROOT_DIR . 'rss.xml';
    $site_url = "https://yourdomain.com"; // Укажи свой домен (без слэша на конце)
    
    // Универсальный SQL-запрос (замени таблицы и поля на свои)
    $rss_sql = "(SELECT id, title, content, 'section_one' as src, file_name as f_check, date_add, keywords 
                 FROM table_one WHERE is_active = 1) 
                UNION 
                (SELECT id, title, content, 'section_two' as src, file_path as f_check, date_add, keywords 
                 FROM table_two WHERE is_active = 1) 
                ORDER BY date_add DESC LIMIT 15";
                
    $rss_stmt = $pdo->query($rss_sql);
    $rss_items = $rss_stmt->fetchAll(PDO::FETCH_ASSOC);

    $xml = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
    $xml .= '<rss xmlns:yandex="http://news.yandex.ru" xmlns:media="http://search.yahoo.com/mrss/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">' . PHP_EOL;
    $xml .= '<channel>' . PHP_EOL;
    $xml .= '  <title>Название вашего ресурса</title>' . PHP_EOL;
    $xml .= '  <link>' . $site_url . '</link>' . PHP_EOL;
    $xml .= '  <atom:link href="' . $site_url . '/rss.xml" rel="self" type="application/rss+xml" />' . PHP_EOL;
    $xml .= '  <description>Описание вашего ресурса для поисковых роботов и агрегаторов</description>' . PHP_EOL;
    $xml .= '  <language>ru</language>' . PHP_EOL;
    $xml .= '  <lastBuildDate>' . date(DATE_RSS) . '</lastBuildDate>' . PHP_EOL;

    foreach ($rss_items as $rss_i) {
        // Универсальный роутинг для страниц контента
        $rss_item_url = $site_url . "/" . urlencode($rss_i['src']) . "?id=" . (int)$rss_i['id'];
        $is_section_two = ($rss_i['src'] === 'section_two');
        
        $xml .= '    <item>' . PHP_EOL;
        $xml .= '      <title>' . ($is_section_two ? '[?] ' : '') . htmlspecialchars($rss_i['title'], ENT_XML1, 'UTF-8') . '</title>' . PHP_EOL;
        $xml .= '      <link>' . $rss_item_url . '</link>' . PHP_EOL;
        
        // Безопасная обработка текста
        $rss_clean_text = strip_tags($rss_i['content']);
        $rss_clean_safe = str_replace(']]>', ']]&gt;', $rss_clean_text);
        
        $xml .= '      <description><![CDATA[' . mb_strimwidth($rss_clean_safe, 0, 300, "...") . ']]></description>' . PHP_EOL;
        
        // Уникальный GUID записи
        $xml .= '      <guid isPermaLink="false">core-' . $rss_i['src'] . '-' . (int)$rss_i['id'] . '</guid>' . PHP_EOL;
        $xml .= '      <pubDate>' . gmdate(DATE_RSS, strtotime($rss_i['date_add'])) . '</pubDate>' . PHP_EOL;

        // Обработка прикрепленных файлов / медиа
        if (!empty($rss_i['f_check'])) {
            $rss_subfolder = $is_section_two ? 'folder_two' : 'folder_one';
            $rss_file_path = ROOT_DIR . 'storage/' . $rss_subfolder . '/' . $rss_i['f_check'];

            if (file_exists($rss_file_path)) {
                $rss_file_size = filesize($rss_file_path);
                $rss_ext = strtolower(pathinfo($rss_i['f_check'], PATHINFO_EXTENSION));
                
                $rss_mime = match($rss_ext) {
                    'webp'        => 'image/webp',
                    'jpg', 'jpeg' => 'image/jpeg',
                    'png'         => 'image/png',
                    'svg'         => 'image/svg+xml',
                    'gz'          => 'application/gzip',
                    'tar'         => 'application/x-tar',
                    'zip'         => 'application/zip',
                    default       => 'application/octet-stream'
                };

                //  Чистый роутинг для скачивания файлов через скрипт или напрямую
                if ($is_section_two) {
                    $rss_img_url = $site_url . "/section_two?get_file=1&amp;id=" . (int)$rss_i['id'];
                } else {
                    $rss_img_url = $site_url . "/storage/folder_one/" . rawurlencode($rss_i['f_check']);
                    $rss_img_url = str_replace('%2F', '/', htmlspecialchars($rss_img_url, ENT_XML1, 'UTF-8'));
                }

                if (str_starts_with($rss_mime, 'image/')) {
                    $xml .= '        <media:content url="' . $rss_img_url . '" type="' . $rss_mime . '" />' . PHP_EOL;
                }
                $xml .= '        <enclosure url="' . $rss_img_url . '" type="' . $rss_mime . '" length="' . $rss_file_size . '" />' . PHP_EOL;
            }
        }

        // Передача полного текста для ИИ-краулеров и GEO оптимизации
        $xml .= '      <yandex:full-text><![CDATA[' . $rss_clean_safe . ']]></yandex:full-text>' . PHP_EOL;
        $xml .= '      <content:encoded><![CDATA[' . $rss_clean_safe . ']]></content:encoded>' . PHP_EOL;
       
        // Теги категорий / Ключевые слова
        if (!empty($rss_i['keywords'])) {
            $keywords = array_filter(array_map('trim', explode(',', $rss_i['keywords'])));
            foreach ($keywords as $keyword) {
                $xml .= '      <category>' . htmlspecialchars($keyword, ENT_XML1, 'UTF-8') . '</category>' . PHP_EOL;
            }
        }

        $xml .= '    </item>' . PHP_EOL;
    }

    $xml .= '  </channel>' . PHP_EOL . '</rss>';
    file_put_contents($rss_file, $xml);
    $report .= " > RSS_GEN | STATUS: SUCCESS - CLEAN_TEMPLATE ✅\n";

} catch (PDOException $e) {
    $report .= " > RSS_GEN | ERROR: " . $e->getMessage() . "\n";
}

Примеры интеграции: Как внедрить подобный RSS-генератор в свой проект:

Вариант А: Интеграция в админку (Событийная)

Вызывайте генератор строго в момент нажатия кнопки «Сохранить» внутри вашей панели управления. Это полностью исключит нагрузку на СУБД при просмотре фида роботами.

    // Внутри обработчика вашей админки
    if ($sql_success) {
    // Запускаем мгновенную пересборку статического XML-файла
    include_once __DIR__ . '/cron/rss_machine.php'; 
    // Бросаем чистый редирект, юзер доволен
    header("Location: /admin.php?status=ok");
    exit;
}

Вариант Б: Жёсткое простукивание через системный Cron

Если контент залетает через API или парсеры, просто повесьте скрипт на нативный планировщик вашего VDS, Сервера, Etc; . Никакого докера, чисто нативный crontab.

//Обновляем статику раз в 15 минут
*/15 * * * * /usr/bin/php /var/www/html/cron/rss_machine.php > /dev/null 2>&1

Выводы.

  1. Никакого мусора в ОЗУ. Мы не создаем объекты FeedGeneratorFactoryInterface, мы просто пишем строки.

  2. Атомарность. Запись в статический файл защищает базу от DDoS-атак ботов-агрегаторов. Пусть Nginx потеет, отдавая статику, а PHP спит.

  3. Жёсткий роутинг. Мы контролируем каждый байт. Если файл в библиотеке — мы принудительно ведем бота через роут со счетчиком. Если картинка в журнале — отдаем напрямую.

Спасибо за прочтение.

Комментарии (2)


  1. Hoksmur
    24.06.2026 03:53

    Плюсик только за одно "Unix way" - я тоже его сторонник. И тоже считаю, что модульность и предсказуемость в разы упрощают модификацию и поддержку решений.


  1. nikon_y
    24.06.2026 03:53

    Идея с переходом на статический RSS вполне рабочая и реально может сильно разгрузить сервер при бот-трафике. Но проблема чаще не в CMS, а в отсутствии нормального кеширования и оптимизации запросов, поэтому убить всё и генерить статику не единственный, но просто самый радикальный вариант.