Wildcard-DNS удобен: завёл *.domain.ru — и новый регион добавляется без похода к администратору. Обратная сторона в том, что вместе с рабочими поддоменами открывается и бесконечное множество нерабочих, причём с кодом 200 и полной копией сайта.
Для поисковика это дубликаты контента на неограниченном числе хостов. Разберём, как отсекать несуществующие поддомены — и почему делать это в PHP хуже, чем в nginx.
Проблема
При настройке wildcard DNS (*.domain.ru) для мультирегионального сайта любой несуществующий поддомен открывает главную страницу:
moscow.domain.ru→ Московская версия ✓spb.domain.ru→ Петербургская версия ✓abrakadabra.domain.ru→ Тоже открывается! ✗
Это плохо для SEO и создаёт бесконечное количество дублей контента.
Решение: обработчик OnEpilog
OnEpilog — поздняя точка, и для этой задачи она выбрана неудачно. К моменту его вызова страница уже сформирована: компоненты отработали, запросы к базе выполнены, кэш создан. Отдав 404 на этом этапе, вы решаете SEO-задачу, но не решаете задачу нагрузки — а несуществующие поддомены обычно перебирает как раз бот, и в объёме.
Для проверки, которая ничего не знает о содержимом страницы, правильная точка — OnPageStart или OnBeforeProlog: ядро уже поднято, а генерация ещё не началась. А самый дешёвый вариант — вообще не доводить запрос до PHP, см. раздел про nginx ниже.
Добавьте в /local/php_interface/init.php:
<?php
use Bitrix\Main\EventManager;
use Bitrix\Main\Application;
EventManager::getInstance()->addEventHandler(
'main',
'OnEpilog',
'checkSubdomainExists'
);
function checkSubdomainExists(): void
{
// Только для frontend
if (defined('ADMIN_SECTION') && ADMIN_SECTION === true) {
return;
}
// preg_replace с якорем ^, а не str_replace: последний вырежет "www."
// в любом месте строки и сломает хосты вроде "mywww.domain.ru"
$host = preg_replace('/^www\./i', '', (string) ($_SERVER['HTTP_HOST'] ?? ''));
$parts = explode('.', $host);
// Проверяем только поддомены третьего уровня
if (count($parts) <= 2) {
return; // Это основной домен
}
$subdomain = $parts[0];
// Получаем список валидных поддоменов
$validSubdomains = getValidSubdomains();
if (!in_array($subdomain, $validSubdomains)) {
// Поддомен не существует — показываем 404
show404Page();
}
}
/**
* Получение списка валидных поддоменов из БД
*/
function getValidSubdomains(): array
{
static $cache = null;
if ($cache !== null) {
return $cache;
}
// Кэшируем на время запроса
$cacheId = 'valid_subdomains';
$cacheDir = '/valid_subdomains/';
$cacheTime = 3600; // 1 час
$obCache = new \CPHPCache();
if ($obCache->InitCache($cacheTime, $cacheId, $cacheDir)) {
$cache = $obCache->GetVars()['subdomains'];
return $cache;
}
$cache = [];
// Вариант 1: Сотбит.Мультирегиональность
if (\Bitrix\Main\Loader::includeModule('sotbit.regions')) {
$connection = Application::getConnection();
$result = $connection->query("SELECT CODE FROM sotbit_regions WHERE ACTIVE = 'Y'");
while ($row = $result->fetch()) {
$subdomain = extractSubdomain($row['CODE']);
if ($subdomain) {
$cache[] = $subdomain;
}
}
}
// Вариант 2: Из инфоблока
// $cache = getSubdomainsFromIblock(REGIONS_IBLOCK_ID);
// Вариант 3: Из конфига
// $cache = ['moscow', 'spb', 'ekb', 'nsk', 'kzn'];
if ($obCache->StartDataCache()) {
$obCache->EndDataCache(['subdomains' => $cache]);
}
return $cache;
}
/**
* Извлечение поддомена из URL
*/
function extractSubdomain(string $url): ?string
{
$url = str_replace(['https://', 'http://'], '', $url);
$parts = explode('.', $url);
if (count($parts) > 2) {
return $parts[0];
}
return null;
}
/**
* Показ страницы 404
*/
function show404Page(): void
{
global $APPLICATION;
// Устанавливаем HTTP-статус
\CHTTP::SetStatus('404 Not Found');
// Подключаем шаблон 404
$file404 = $_SERVER['DOCUMENT_ROOT'] . '/404.php';
if (file_exists($file404)) {
include $file404;
} else {
echo '<!DOCTYPE html><html><head><title>404 Not Found</title></head>';
echo '<body><h1>404 - Страница не найдена</h1>';
echo '<p>Запрошенный регион не существует.</p></body></html>';
}
die();
}Расширенная версия с логированием
<?php
namespace Local\Multiregion;
use Bitrix\Main\Application;
use Bitrix\Main\EventManager;
use Bitrix\Main\Data\Cache;
class SubdomainValidator
{
private const CACHE_TTL = 3600;
private const CACHE_DIR = '/subdomain_validator/';
/**
* Основной домен сайта (без www)
*/
private static string $mainDomain = '';
/**
* Регистрация обработчика
*/
public static function register(string $mainDomain): void
{
self::$mainDomain = $mainDomain;
EventManager::getInstance()->addEventHandler(
'main',
'OnEpilog',
[self::class, 'validate']
);
}
/**
* Валидация поддомена
*/
public static function validate(): void
{
if (defined('ADMIN_SECTION') && ADMIN_SECTION === true) {
return;
}
$subdomain = self::getCurrentSubdomain();
if ($subdomain === null) {
return; // Основной домен
}
$validSubdomains = self::getValidSubdomains();
if (!in_array($subdomain, $validSubdomains, true)) {
self::logInvalidSubdomain($subdomain);
self::show404();
}
}
/**
* Получение текущего поддомена
*/
private static function getCurrentSubdomain(): ?string
{
$host = strtolower($_SERVER['HTTP_HOST']);
$host = preg_replace('/^www\./', '', $host);
// Проверяем, что это наш домен
if (!str_ends_with($host, self::$mainDomain)) {
return null;
}
// Извлекаем поддомен
$subdomain = str_replace('.' . self::$mainDomain, '', $host);
// Если совпадает с основным доменом — нет поддомена
if ($subdomain === self::$mainDomain || $subdomain === '') {
return null;
}
return $subdomain;
}
/**
* Получение валидных поддоменов
*/
private static function getValidSubdomains(): array
{
$cache = Cache::createInstance();
if ($cache->initCache(self::CACHE_TTL, 'valid_subdomains', self::CACHE_DIR)) {
return $cache->getVars()['list'];
}
$cache->startDataCache();
$list = self::loadSubdomainsFromDatabase();
$cache->endDataCache(['list' => $list]);
return $list;
}
/**
* Загрузка из БД (пример для Сотбит)
*/
private static function loadSubdomainsFromDatabase(): array
{
$subdomains = [];
try {
$connection = Application::getConnection();
// Сотбит.Мультирегиональность
$sql = "SELECT CODE FROM sotbit_regions WHERE ACTIVE = 'Y'";
$result = $connection->query($sql);
while ($row = $result->fetch()) {
$url = $row['CODE'];
$parts = explode('.', str_replace(['http://', 'https://'], '', $url));
if (count($parts) > 2) {
$subdomains[] = $parts[0];
}
}
} catch (\Throwable $e) {
// Логируем ошибку, но не падаем
AddMessage2Log("SubdomainValidator error: " . $e->getMessage());
}
return array_unique($subdomains);
}
/**
* Логирование невалидных поддоменов
*/
private static function logInvalidSubdomain(string $subdomain): void
{
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$ua = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
AddMessage2Log(
sprintf(
"Invalid subdomain: %s | IP: %s | UA: %s",
$subdomain,
$ip,
substr($ua, 0, 100)
),
'subdomain_validator'
);
}
/**
* Показ 404 страницы
*/
private static function show404(): void
{
\CHTTP::SetStatus('404 Not Found');
// Очищаем буфер вывода
while (ob_get_level()) {
ob_end_clean();
}
$file404 = Application::getDocumentRoot() . '/404.php';
if (file_exists($file404)) {
include $file404;
} else {
header('Content-Type: text/html; charset=UTF-8');
echo '<!DOCTYPE html><html lang="ru"><head>';
echo '<meta charset="UTF-8"><title>404 - Регион не найден</title></head>';
echo '<body><h1>Регион не найден</h1>';
echo '<p>Запрошенный регион не существует.</p>';
echo '<a href="https://' . self::$mainDomain . '/">Перейти на главную</a>';
echo '</body></html>';
}
die();
}
/**
* Сброс кэша
*/
public static function clearCache(): void
{
$cache = Cache::createInstance();
$cache->cleanDir(self::CACHE_DIR);
}
}Регистрация
<?php
// /local/php_interface/init.php
\Local\Multiregion\SubdomainValidator::register('domain.ru');Альтернатива: проверка на уровне Nginx (предпочтительный вариант)
Если список регионов меняется редко — а он меняется редко, — то проверять поддомен в PHP незачем. nginx отсечёт запрос до запуска PHP-FPM, и стоимость обработки мусорного хоста упадёт с миллисекунд до микросекунд.
Если список поддоменов фиксированный, эффективнее проверять на уровне веб-сервера:
# Список валидных поддоменов
map $host $valid_subdomain {
default 0;
~^(moscow|spb|ekb|nsk|kzn)\. 1;
~^www\. 1; # www разрешён
domain.ru 1; # основной домен
}
server {
listen 443 ssl;
server_name *.domain.ru domain.ru;
# Проверка валидности поддомена
if ($valid_subdomain = 0) {
return 404;
}
# ... остальная конфигурация
}Как держать список регионов в актуальном состоянии. Конфиг nginx с перечислением поддоменов — не самый удобный вариант, если регионы заводят маркетологи через админку. Рабочая схема: список хранится в Битриксе, а обработчик события на сохранении региона генерирует map-файл для nginx и перезагружает конфиг. Тогда добавление региона остаётся операцией внутри админки, а проверка выполняется на входе.
Если такой автоматики нет — оставляйте проверку в PHP, но перенесите её на OnPageStart и обязательно кэшируйте список валидных поддоменов: обращение к базе на каждый запрос по несуществующему хосту как раз и создаёт ту нагрузку, от которой вы защищаетесь.
Итоги
Проверяйте как можно раньше. Порядок предпочтений: nginx → OnPageStart в PHP → OnEpilog. Разница между первым и последним вариантом — полный цикл генерации страницы на каждый мусорный запрос.
Отдавайте 404, а не редирект на основной домен. Редирект сообщает поисковику, что хост существует, и робот продолжит его проверять.
Не забудьте про www и про регистр — хост из заголовка приходит в том виде, в каком его прислал клиент.
Каждая региональная версия должна иметь свой canonical на собственный поддомен, а не на основной домен: иначе вы своими руками говорите поисковику, что региональных страниц не существует, и весь смысл мультирегиональности пропадает.
Про совет закрывать несуществующие поддомены в robots.txt: он не работает. robots.txt читается для каждого хоста отдельно, то есть для несуществующего поддомена робот запросит его же несуществующий robots.txt — и получит копию основного. Отсекать нужно кодом ответа, а не файлом.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.