Всё началось с банальной задачи — я и моя напарница нейросеть Асси задумались над SEO-продвижением и адаптацией нашего сайта под современные реалии генеративного поиска (GEO / RAG) и краулеры языковых моделей (GPTBot, ClaudeBot, Perplexity). По старой привычке решили поглядеть, как эту задачу решают другие «взрослые дяди», открыли исходники популярных решений для синдикации в современных CMS… и, мягко говоря, офигели. То, что в корпоративном BigTech считается стандартом генерации банальной XML-ленты, на поверку оказалось кромешным инфраструктурным адом.
Анатомия корпоративного ада: Битрикс и остальные монстры.
Когда краулер стучится за RSS к типичному энтерпрайз-движку, на сервере начинается сущий кошмар, судите сами:
Bitrix (Король тормозов): Это вообще отдельный котел для мазохистов. Чтобы отдать к примеру 15 новостей, Битрикс поднимает всё своё монструозное ядро, подключает prolog_before.php, инициализирует тысячи констант и лезет в базу через ORM, которая генерирует SQL-запросы длиной в километр. Если у вас «Композитный сайт» — готовьтесь к тому, что кэш будет инвалидироваться дольше, чем бот ждет ответа. Итог: сервак потеет, память жрётся, а краулер получает ответ через 500–800 мс. За это время можно было бы запустить ракету в космос.
WordPress: Тут просыпается «прожорливое чудовище». WP_Query делает каскадные запросы к неоптимизированной базе, вытягивая метаданные и мусор. Потом это всё прогоняется через ад из сотен хуков и фильтров. Если стоят плагины типа Yoast — они перелопачивают строки в ОЗУ по кругу. Результат: 200–300 мс на ровном месте.
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(']]>', ']]>', $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&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Выводы.
Никакого мусора в ОЗУ. Мы не создаем объекты
FeedGeneratorFactoryInterface, мы просто пишем строки.Атомарность. Запись в статический файл защищает базу от DDoS-атак ботов-агрегаторов. Пусть Nginx потеет, отдавая статику, а PHP спит.
Жёсткий роутинг. Мы контролируем каждый байт. Если файл в библиотеке — мы принудительно ведем бота через роут со счетчиком. Если картинка в журнале — отдаем напрямую.
Спасибо за прочтение.






















