Canonical — тег, который чаще всего настраивают «на всякий случай», а потом удивляются, почему страницы вылетают из индекса. Между «указать поисковику основную версию страницы» и «случайно склеить весь каталог в одну карточку» — одна ошибка в шаблоне.
Разберём, как расставить canonical в Битриксе так, чтобы он решал задачу дублей и не создавал новых, и где в типовых примерах из интернета зарыты две мины: подстановка хоста из запроса и canonical на пагинации.
Что такое canonical
Тег <link rel="canonical"> указывает поисковикам основную версию страницы, когда один контент доступен по нескольким URL:
<link rel="canonical" href="https://site.ru/catalog/product/" />Типичные дубли в Битриксе:
/catalog/и/catalog/index.php- Страницы с UTM-метками и без
- HTTP и HTTPS версии
- С www и без www
- Пагинация:
/catalog/?PAGEN_1=2
Установка для статической страницы
В начале файла страницы (после подключения header.php):
<?php
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/header.php';
// Устанавливаем canonical
$APPLICATION->SetPageProperty(
'canonical',
'https://' . SITE_SERVER_NAME . $APPLICATION->GetCurDir()
);
?>Результат в <head>:
<link rel="canonical" href="https://site.ru/about/" />SetPageProperty сам по себе ничего не выводит. Он кладёт значение в свойства страницы — тег появится только если шаблон сайта это свойство отрисовывает. Если после настройки в исходном коде страницы пусто, проверьте header.php. Надёжнее не зависеть от шаблона и добавлять тег напрямую:
$APPLICATION->AddHeadString(
'<link rel="canonical" href="https://' . SITE_SERVER_NAME . $APPLICATION->GetCurDir() . '"/>',
true
);Универсальный обработчик для всего сайта
Расставлять canonical руками на каждой странице невозможно — забудут на первой же новой. Логичнее один обработчик, который проставляет адрес по умолчанию, а частные случаи переопределяют его явно.
Событие OnEpilog для этого подходит потому, что <head> в Битриксе рендерится через отложенные функции: реальная подстановка содержимого происходит уже после выполнения страницы, поэтому значение, установленное в эпилоге, всё ещё успевает попасть в вывод.
Добавьте в /local/php_interface/init.php:
<?php
use Bitrix\Main\EventManager;
use Bitrix\Main\Context;
EventManager::getInstance()->addEventHandler(
'main',
'OnEpilog',
'setCanonicalUrl'
);
function setCanonicalUrl(): void
{
global $APPLICATION;
// Пропускаем админку
if (defined('ADMIN_SECTION') && ADMIN_SECTION === true) {
return;
}
// Пропускаем если canonical уже установлен
$existingCanonical = $APPLICATION->GetPageProperty('canonical');
if (!empty($existingCanonical)) {
return;
}
// Базовый URL
$request = Context::getCurrent()->getRequest();
$protocol = $request->isHttps() ? 'https' : 'http';
$host = $request->getHttpHost();
$uri = $request->getRequestedPageDirectory();
// Удаляем index.php
$uri = preg_replace('/index\.php$/', '', $uri);
// Гарантируем trailing slash для директорий
if (!preg_match('/\.[a-z0-9]+$/i', $uri)) {
$uri = rtrim($uri, '/') . '/';
}
$canonicalUrl = "{$protocol}://{$host}{$uri}";
$APPLICATION->SetPageProperty('canonical', $canonicalUrl);
}Хост нельзя брать из запроса. getHttpHost() — это заголовок Host, который присылает клиент. Если веб-сервер отдаёт сайт на любой пришедший хост (а конфигурация по умолчанию именно такая), достаточно запроса с чужим Host, чтобы ваши страницы отдали <link rel="canonical" href="https://чужой-домен/...">. Это известный приём поисковой атаки, и «у нас так никто не делает» здесь плохая защита.
Берите хост из настроек сайта — SITE_SERVER_NAME или поле SERVER_NAME текущего сайта. Заодно это решает вопрос с www и протоколом: значение фиксировано и не зависит от того, как пришёл запрос.
Расширенный класс для управления canonical
<?php
namespace Local\Seo;
use Bitrix\Main\Context;
use Bitrix\Main\EventManager;
class CanonicalManager
{
private static bool $registered = false;
private static array $config = [
'protocol' => 'https',
'remove_www' => true,
'trailing_slash' => true,
'remove_index' => true,
'ignore_params' => ['utm_source', 'utm_medium', 'utm_campaign',
'utm_content', 'utm_term', 'yclid', 'gclid',
'fbclid', 'PAGEN_', 'SHOWALL_'],
'keep_params' => [], // Параметры, которые сохраняем
];
/**
* Регистрация обработчика
*/
public static function register(array $config = []): void
{
if (self::$registered) {
return;
}
self::$config = array_merge(self::$config, $config);
EventManager::getInstance()->addEventHandler(
'main',
'OnEpilog',
[self::class, 'handleEpilog']
);
self::$registered = true;
}
/**
* Обработчик события
*/
public static function handleEpilog(): void
{
global $APPLICATION;
if (defined('ADMIN_SECTION') && ADMIN_SECTION === true) {
return;
}
// Не перезаписываем явно установленный canonical
if ($APPLICATION->GetPageProperty('canonical')) {
return;
}
$canonical = self::buildCanonicalUrl();
$APPLICATION->SetPageProperty('canonical', $canonical);
}
/**
* Построение canonical URL
*/
public static function buildCanonicalUrl(): string
{
$request = Context::getCurrent()->getRequest();
// Протокол
$protocol = self::$config['protocol'];
// Хост
$host = $request->getHttpHost();
if (self::$config['remove_www']) {
$host = preg_replace('/^www\./i', '', $host);
}
// Путь
$path = $request->getRequestedPageDirectory();
// Удаляем index.php
if (self::$config['remove_index']) {
$path = preg_replace('/index\.php$/i', '', $path);
}
// Trailing slash
if (self::$config['trailing_slash'] && !preg_match('/\.[a-z0-9]+$/i', $path)) {
$path = rtrim($path, '/') . '/';
}
// Обработка параметров
$query = self::filterQueryString($request->getQueryString());
$url = "{$protocol}://{$host}{$path}";
if ($query) {
$url .= '?' . $query;
}
return $url;
}
/**
* Фильтрация query string
*/
private static function filterQueryString(?string $queryString): string
{
if (empty($queryString)) {
return '';
}
parse_str($queryString, $params);
$filtered = [];
foreach ($params as $key => $value) {
// Проверяем игнорируемые параметры
$ignore = false;
foreach (self::$config['ignore_params'] as $pattern) {
if (strpos($key, $pattern) === 0 || $key === $pattern) {
$ignore = true;
break;
}
}
// Если не игнорируем И (нет whitelist ИЛИ в whitelist)
if (!$ignore) {
if (empty(self::$config['keep_params']) ||
in_array($key, self::$config['keep_params'])) {
$filtered[$key] = $value;
}
}
}
return http_build_query($filtered);
}
/**
* Ручная установка canonical
*/
public static function set(string $url): void
{
global $APPLICATION;
$APPLICATION->SetPageProperty('canonical', $url);
}
/**
* Canonical для элемента каталога
*/
public static function setForCatalogElement(int $elementId, string $sectionPath = ''): void
{
$protocol = self::$config['protocol'];
$host = preg_replace('/^www\./i', '', Context::getCurrent()->getRequest()->getHttpHost());
$element = \CIBlockElement::GetByID($elementId)->GetNext();
if (!$element) {
return;
}
$url = "{$protocol}://{$host}{$sectionPath}{$element['CODE']}/";
self::set($url);
}
/**
* Canonical для раздела каталога
*/
public static function setForCatalogSection(int $sectionId): void
{
$protocol = self::$config['protocol'];
$host = preg_replace('/^www\./i', '', Context::getCurrent()->getRequest()->getHttpHost());
$section = \CIBlockSection::GetByID($sectionId)->GetNext();
if (!$section) {
return;
}
$url = "{$protocol}://{$host}/catalog/{$section['CODE']}/";
self::set($url);
}
}Регистрация в init.php
<?php
// /local/php_interface/init.php
\Local\Seo\CanonicalManager::register([
'protocol' => 'https',
'remove_www' => true,
'trailing_slash' => true,
'ignore_params' => [
'utm_source', 'utm_medium', 'utm_campaign',
'utm_content', 'utm_term',
'yclid', 'gclid', 'fbclid',
'PAGEN_', 'SHOWALL_',
'sort', 'order', // Сортировка каталога
],
]);Чёрный список параметров работает хуже белого. В конфиге перечислены utm_*, PAGEN_ и прочее — то есть всё неизвестное по умолчанию попадает в canonical. Появится новая рекламная метка (а они появляются), и в индексе снова окажутся дубли, пока кто-нибудь не заметит и не дополнит список. Для canonical практичнее обратная логика: пустой результат по умолчанию и явный список параметров, которые действительно меняют контент (обычно это один-два фильтра, и то не всегда).
Canonical для каталога товаров
В шаблоне компонента catalog.element:
<?php
// template.php
use Local\Seo\CanonicalManager;
// Устанавливаем canonical для товара
CanonicalManager::setForCatalogElement(
$arResult['ID'],
$arResult['SECTION']['SECTION_PAGE_URL'] ?? '/catalog/'
);Canonical для пагинации
Здесь находится самая распространённая ошибка по теме, поэтому разберём подробно.
Совет «на страницах пагинации ставьте canonical на первую страницу» кочует по статьям много лет. Он был спорным всегда, а сейчас прямо противоречит рекомендациям Google: страницы пагинации содержат разный контент, а canonical сообщает поисковику, что страницы одинаковые. Результат — вторая и последующие страницы не сканируются, товары с них не попадают в индекс, а внутренние ссылки с них не учитываются.
Правильное поведение — каждая страница пагинации канонична сама себе:
<?php
// В шаблоне списка
global $APPLICATION;
$request = \Bitrix\Main\Context::getCurrent()->getRequest();
$pageNum = (int) ($arResult['NAV_RESULT']->NavPageNomer ?? 1);
$url = 'https://' . SITE_SERVER_NAME . $APPLICATION->GetCurDir();
// Номер страницы — единственный параметр, который сохраняем
if ($pageNum > 1) {
$url .= '?PAGEN_1=' . $pageNum;
}
$APPLICATION->SetPageProperty('canonical', $url);Единственное исключение — страница «показать все» (SHOWALL_1=1), которая действительно содержит контент всех страниц разбивки. Вот на неё канонизировать страницы пагинации допустимо. Но если товаров тысячи, такой страницы у вас быть не должно по другим причинам.
Проверка настройки
# Проверка canonical на странице
curl -s https://site.ru/catalog/ | grep -i canonical
# Массовая проверка списка URL
while read url; do
canonical=$(curl -s "$url" | grep -oP 'rel="canonical" href="\K[^"]+')
echo "$url -> $canonical"
done < urls.txtИтоги
Canonical — инструмент точный и потому опасный: ошибка в нём не ломает сайт видимым образом, она просто убирает страницы из индекса, и заметите вы это по трафику через месяц.
Чек-лист, по которому стоит пройтись после настройки:
- Хост берётся из настроек сайта, а не из заголовка
Host. - Страницы пагинации каноничны сами себе, а не первой странице.
- Canonical абсолютный и с тем же протоколом, что и рабочая версия сайта.
- Адрес в canonical отдаёт 200, а не редирект и не 404. Canonical на редирект поисковик проигнорирует.
- Страница не закрыта в robots.txt. Если целевой URL запрещён к сканированию, поисковик не сможет подтвердить связь.
- Проверено на живых страницах:
curl -s URL | grep canonicalбыстрее, чем ждать отчёта в Search Console.
И главное про природу тега. Canonical — рекомендация, а не директива: поисковик вправе выбрать другой URL, если сочтёт его более подходящим. Если дубли нужно устранить гарантированно — нужен 301-редирект. Canonical применяется там, где обе версии страницы должны остаться доступными для пользователя.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.