Мультирегиональный сайт на поддоменах — распространённая схема, и в ней есть неприятная асимметрия: контент один, а технические файлы должны быть разными. robots.txt обязан ссылаться на sitemap своего домена, а в sitemap.xml должны быть URL своего поддомена, иначе поисковик получает карту сайта, указывающую на чужой хост.
Решается это генерацией обоих файлов на PHP. Ниже рабочая схема — и три места, где типовая реализация делает хуже, чем было.
Проблема
На мультидоменном сайте (поддомены или разные домены) каждый домен должен иметь свой robots.txt и sitemap.xml с правильными URL. Статические файлы не подходят — нужна динамическая генерация.
Решение
- Создать PHP-скрипты для генерации robots и sitemap
- Настроить редиректы с .txt и .xml на PHP
- Удалить статические файлы
Шаг 1: Динамический robots.txt
Создайте /robots.php:
<?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
// Хост берём НЕ из запроса напрямую, а сверяем со списком сайтов Битрикса.
// $_SERVER['HTTP_HOST'] — это заголовок, которым управляет клиент
$requestedHost = (string) ($_SERVER['HTTP_HOST'] ?? '');
$allowedHosts = [];
$sites = \CSite::GetList('sort', 'asc', ['ACTIVE' => 'Y']);
while ($site = $sites->Fetch()) {
if (!empty($site['SERVER_NAME'])) {
$allowedHosts[] = $site['SERVER_NAME'];
}
}
if (!in_array($requestedHost, $allowedHosts, true)) {
header('HTTP/1.1 404 Not Found');
exit;
}
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $requestedHost;
$baseUrl = $protocol . '://' . $host;
// Заголовок
header('Content-Type: text/plain; charset=UTF-8');
header('X-Robots-Tag: noindex');
// Содержимое robots.txt
echo "User-agent: *\n";
echo "Disallow: /bitrix/\n";
echo "Disallow: /upload/iblock/\n";
echo "Disallow: /*?*\n";
echo "Disallow: /*utm_\n";
echo "Disallow: /*yclid=\n";
echo "Disallow: /*gclid=\n";
echo "Disallow: /search/\n";
echo "Disallow: /auth/\n";
echo "Disallow: /personal/\n";
echo "Disallow: /cart/\n";
echo "Disallow: /order/\n";
echo "Disallow: /compare/\n";
echo "Disallow: /*PAGEN_\n";
echo "Disallow: /*SHOWALL_\n";
echo "Disallow: /*SECTION_CODE=\n";
echo "Disallow: /*ELEMENT_CODE=\n";
echo "\n";
echo "Allow: /bitrix/templates/\n";
echo "Allow: /bitrix/css/\n";
echo "Allow: /bitrix/js/\n";
echo "\n";
echo "Clean-param: utm_source&utm_medium&utm_campaign&utm_content&utm_term\n";
echo "Clean-param: yclid&gclid&fbclid\n";
echo "\n";
echo "Sitemap: {$baseUrl}/sitemap.xml\n";Директива Host устарела. Яндекс перестал её учитывать ещё в 2018 году: главное зеркало определяется 301-редиректом и тегом canonical, а не строкой в robots.txt. Google её никогда не поддерживал. Оставлять строку не вредно, но она создаёт ложное ощущение, что вопрос с зеркалами решён, — а решается он редиректом на уровне веб-сервера.
Disallow: /*?* — самое опасное правило в этом файле. Оно закрывает от индексации все адреса с параметрами. Вместе с мусорными метками под запрет попадают страницы пагинации, результаты фильтров и любые полезные URL с параметрами, если такие у вас есть.
Плюс оно противоречит директиве Clean-param двумя строками ниже: Clean-param объясняет роботу, какие параметры игнорировать при склейке адресов, но применяется только к страницам, которые роботу разрешено сканировать. Закрытые Disallow страницы он просто не обходит, и Clean-param для них ничего не делает.
Выберите один механизм. Для Яндекса — Clean-param без общего запрета; для Google — canonical на странице, потому что Clean-param он не поддерживает.
Disallow: /upload/iblock/ закрывает картинки товаров. На интернет-магазине это означает отсутствие в Google Картинках — заметном источнике трафика для товарных сайтов. Правило кочует по шаблонным robots.txt с тех времён, когда в upload/ держали и служебные файлы. Если у вас там только медиа — не закрывайте.
Шаг 2: Динамический sitemap.xml
Простой вариант — обёртка над статическим файлом
<?php
// /sitemap.php
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
// Путь к статическому sitemap (сгенерированному через админку)
$staticSitemap = $_SERVER['DOCUMENT_ROOT'] . '/sitemap_static.xml';
if (!file_exists($staticSitemap)) {
header('HTTP/1.1 404 Not Found');
exit('Sitemap not found');
}
// Читаем и заменяем домен
$content = file_get_contents($staticSitemap);
// Заменяем домен ТОЛЬКО внутри тегов <loc>.
// Наивная замена по всему файлу ломает объявление пространства имён —
// см. предупреждение ниже
$content = preg_replace_callback(
'#<loc>\s*https?://[^/]+(/[^<]*)</loc>#i',
static fn(array $m): string => '<loc>' . $protocol . '://' . $host . $m[1] . '</loc>',
$content
);
header('Content-Type: application/xml; charset=UTF-8');
echo $content;Почему замену нельзя делать по всему файлу. Шаблон #https?://[^/]+/# попадает не только в <loc>, но и в объявление пространства имён в корневом теге:
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">После такой замены оно превращается в xmlns="https://moscow.site.ru/schemas/sitemap/0.9", и файл перестаёт быть валидным sitemap: формально XML корректен, но поисковик не распознаёт схему и отклоняет карту целиком. Диагностируется это плохо — в панели вебмастера будет невнятное «не удалось обработать файл», а глазами разница незаметна.
Продвинутый вариант — генерация из базы
<?php
// /sitemap.php
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
use Bitrix\Main\Loader;
Loader::includeModule('iblock');
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$baseUrl = $protocol . '://' . $host;
header('Content-Type: application/xml; charset=UTF-8');
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
// Главная
outputUrl($baseUrl . '/', date('c'), 'daily', '1.0');
// Статические страницы
$staticPages = [
'/about/' => ['weekly', '0.8'],
'/contacts/' => ['monthly', '0.7'],
'/services/' => ['weekly', '0.9'],
];
foreach ($staticPages as $url => $params) {
outputUrl($baseUrl . $url, null, $params[0], $params[1]);
}
// Товары из каталога
$products = \CIBlockElement::GetList(
['TIMESTAMP_X' => 'DESC'],
[
'IBLOCK_ID' => 2,
'ACTIVE' => 'Y',
],
false,
false,
['ID', 'TIMESTAMP_X', 'DETAIL_PAGE_URL']
);
while ($product = $products->GetNext(true, false)) {
outputUrl(
$baseUrl . $product['DETAIL_PAGE_URL'],
date('c', strtotime($product['TIMESTAMP_X'])),
'weekly',
'0.7'
);
}
// Разделы каталога
$sections = \CIBlockSection::GetList(
['TIMESTAMP_X' => 'DESC'],
[
'IBLOCK_ID' => 2,
'ACTIVE' => 'Y',
],
false,
['ID', 'TIMESTAMP_X', 'SECTION_PAGE_URL']
);
while ($section = $sections->GetNext(true, false)) {
outputUrl(
$baseUrl . $section['SECTION_PAGE_URL'],
date('c', strtotime($section['TIMESTAMP_X'])),
'weekly',
'0.8'
);
}
echo '</urlset>';
function outputUrl(string $loc, ?string $lastmod, string $changefreq, string $priority): void
{
echo '<url>' . "\n";
echo ' <loc>' . htmlspecialchars($loc) . '</loc>' . "\n";
if ($lastmod) {
echo ' <lastmod>' . $lastmod . '</lastmod>' . "\n";
}
echo ' <changefreq>' . $changefreq . '</changefreq>' . "\n";
echo ' <priority>' . $priority . '</priority>' . "\n";
echo '</url>' . "\n";
}Шаг 3: Скрипт конвертации sitemap из админки
После генерации sitemap через админку запустите конвертер:
<?php
// /sitemap_convert.php
$sourceFile = $_SERVER['DOCUMENT_ROOT'] . '/sitemap.xml';
$destFile = $_SERVER['DOCUMENT_ROOT'] . '/sitemap_static.xml';
if (!file_exists($sourceFile)) {
die('Source sitemap not found');
}
// Переименовываем
rename($sourceFile, $destFile);
echo "Converted: sitemap.xml -> sitemap_static.xml\n";
echo "Now /sitemap.xml will use dynamic PHP handler\n";Шаг 4: Редиректы в .htaccess
# Динамические robots и sitemap
RewriteEngine On
RewriteRule ^robots\.txt$ /robots.php [L]
RewriteRule ^sitemap\.xml$ /sitemap.php [L]Важно: Удалите статические файлы robots.txt и sitemap.xml из корня сайта, иначе редиректы не сработают.
Шаг 5: Класс для мультидоменного sitemap
<?php
namespace Local\Seo;
use Bitrix\Main\Loader;
class DynamicSitemap
{
private string $baseUrl;
private array $urls = [];
public function __construct()
{
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$this->baseUrl = $protocol . '://' . $host;
}
public function addUrl(
string $path,
?string $lastmod = null,
string $changefreq = 'weekly',
string $priority = '0.5'
): self {
$this->urls[] = [
'loc' => $this->baseUrl . $path,
'lastmod' => $lastmod,
'changefreq' => $changefreq,
'priority' => $priority,
];
return $this;
}
public function addStaticPages(array $pages): self
{
foreach ($pages as $path => $params) {
$this->addUrl(
$path,
null,
$params['changefreq'] ?? 'weekly',
$params['priority'] ?? '0.5'
);
}
return $this;
}
public function addIblockElements(int $iblockId, ?array $filter = null): self
{
Loader::includeModule('iblock');
$defaultFilter = [
'IBLOCK_ID' => $iblockId,
'ACTIVE' => 'Y',
];
$elements = \CIBlockElement::GetList(
['TIMESTAMP_X' => 'DESC'],
array_merge($defaultFilter, $filter ?? []),
false,
false,
['ID', 'TIMESTAMP_X', 'DETAIL_PAGE_URL']
);
while ($el = $elements->GetNext(true, false)) {
$this->addUrl(
$el['DETAIL_PAGE_URL'],
date('c', strtotime($el['TIMESTAMP_X'])),
'weekly',
'0.7'
);
}
return $this;
}
public function addIblockSections(int $iblockId): self
{
Loader::includeModule('iblock');
$sections = \CIBlockSection::GetList(
['TIMESTAMP_X' => 'DESC'],
['IBLOCK_ID' => $iblockId, 'ACTIVE' => 'Y'],
false,
['ID', 'TIMESTAMP_X', 'SECTION_PAGE_URL']
);
while ($section = $sections->GetNext(true, false)) {
$this->addUrl(
$section['SECTION_PAGE_URL'],
date('c', strtotime($section['TIMESTAMP_X'])),
'weekly',
'0.8'
);
}
return $this;
}
public function render(): void
{
header('Content-Type: application/xml; charset=UTF-8');
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
foreach ($this->urls as $url) {
echo '<url>' . "\n";
echo ' <loc>' . htmlspecialchars($url['loc']) . '</loc>' . "\n";
if ($url['lastmod']) {
echo ' <lastmod>' . $url['lastmod'] . '</lastmod>' . "\n";
}
echo ' <changefreq>' . $url['changefreq'] . '</changefreq>' . "\n";
echo ' <priority>' . $url['priority'] . '</priority>' . "\n";
echo '</url>' . "\n";
}
echo '</urlset>';
}
}Использование
<?php
// /sitemap.php
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
use Local\Seo\DynamicSitemap;
(new DynamicSitemap())
->addUrl('/', null, 'daily', '1.0')
->addStaticPages([
'/about/' => ['changefreq' => 'monthly', 'priority' => '0.8'],
'/contacts/' => ['changefreq' => 'monthly', 'priority' => '0.7'],
'/services/' => ['changefreq' => 'weekly', 'priority' => '0.9'],
])
->addIblockSections(2)
->addIblockElements(2)
->render();Итоги
Схема с генерацией на PHP рабочая, но у неё есть цена и есть места, где легко сделать хуже.
Цена. Каждый запрос robots.txt и sitemap.xml теперь поднимает PHP, а на генерации из базы — ещё и обходит каталог. Боты запрашивают эти файлы регулярно, а sitemap.xml большого каталога генерируется секундами. Кэш обязателен: файл на диске с TTL в несколько часов, а не «сгенерируем на каждый запрос».
Чек-лист по итогам разбора:
- Хост проверяется по списку сайтов, а не берётся из заголовка запроса.
- Директива
Hostубрана — главное зеркало задаётся редиректом. Disallow: /*?*иClean-paramне используются вместе — они противоречат друг другу.- Картинки товаров не закрыты от индексации без веской причины.
- Замена домена в готовом sitemap идёт только внутри
<loc>, а не по всему файлу. - Есть кэш сгенерированных файлов.
- Проверено
curl-ом с разных хостов:curl -H "Host: spb.site.ru" https://site.ru/robots.txtпокажет, что реально отдаётся каждому поддомену, без возни с DNS.
И самая частая ошибка внедрения: статические robots.txt и sitemap.xml остались лежать в корне. Веб-сервер отдаёт существующий файл, не доходя до правил переадресации на PHP, и вы долго проверяете «почему изменения не применяются». Удалите старые файлы, а не переименовывайте их в robots_old.txt рядом — по этому адресу их тоже кто-нибудь найдёт.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.