Реализация RSS-генератора на «коленке» или наш ответ Чемберлену

от автора

Всё началось с банальной задачи — я и моя напарница нейросеть Асси задумались над 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. Жёсткий роутинг. Мы контролируем каждый байт. Если файл в библиотеке — мы принудительно ведем бота через роут со счетчиком. Если картинка в журнале — отдаем напрямую.

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

ссылка на оригинал статьи https://habr.com/ru/articles/1051178/