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