Умный фильтр устроен так, что любая последовательность сегментов в адресе разбирается как условия фильтрации, а результат — даже пустой — отдаётся с кодом 200 OK. Для сайта с каталогом это означает бесконечное множество валидных с точки зрения сервера адресов, ведущих в никуда.
Ниже — реализация валидатора: как строить белый список допустимых сегментов из настроек самого каталога, а не из захардкоженного массива, и как внедрять это, не выбив из индекса работающие страницы.
Проблема
Умный фильтр Битрикса генерирует ЧПУ-адреса для комбинаций свойств:
/catalog/kontejnery/brand-is-tara_ru/
/catalog/kontejnery/prop_shirina-from-330/
/catalog/kontejnery/brand-is-tara_ru/prop_shirina-from-100-to-500/Проблема: любой URL возвращает 200 OK, даже если комбинация не существует или пуста. Это приводит к:
- SEO-проблемам: дубли контента, размытие релевантности, потеря краулингового бюджета
- Росту кэша: каждый URL кэшируется отдельно
- Нагрузке: боты индексируют бесконечные комбинации
Решение: валидация URL
Проверяем URL перед обработкой и возвращаем 404 для невалидных комбинаций.
Ключевой принцип — белый список. Валидатор разрешает то, что явно описано: существующие коды свойств, реальные значения, разумные границы диапазонов. Попытка перечислить «плохие» варианты бессмысленна: их бесконечно много, в этом и состоит исходная проблема.
Из этого следует главное требование к реализации: источником списка должны быть настройки самого каталога — свойства инфоблока и значения умного фильтра, — а не массив в коде. Захардкоженный список устаревает в момент, когда контент-менеджер добавляет новое свойство, и превращается в 404 на работающих страницах.
Класс валидатора
<?php
// /local/php_interface/classes/Catalog/SmartFilterValidator.php
namespace Local\Catalog;
use Bitrix\Main\Loader;
class SmartFilterValidator
{
private int $iblockId;
private array $validPropertyCodes = [];
private array $validBrands = [];
private array $validSections = [];
public function __construct(int $iblockId)
{
Loader::includeModule('iblock');
$this->iblockId = $iblockId;
$this->loadValidData();
}
/**
* Проверка текущего запроса
*/
public static function validateCurrentRequest(): void
{
$requestPath = self::getRequestPath();
// Паттерн URL каталога с фильтром
if (!preg_match('~^/catalog/([^/]+)/(.+?)/?$~u', $requestPath, $matches)) {
return;
}
$sectionCode = $matches[1];
$filterTail = trim($matches[2], '/');
if (empty($filterTail)) {
return;
}
// Разбиваем на сегменты
$segments = self::splitFilterSegments($filterTail);
// Проверяем, что это URL фильтра
if (!self::hasFilterHints($segments)) {
return;
}
// Получаем ID инфоблока по разделу
$iblockId = self::getIblockIdBySection($sectionCode);
if (!$iblockId) {
self::trigger404();
return;
}
// Валидируем сегменты
$validator = new self($iblockId);
if (!$validator->validateSegments($segments)) {
self::trigger404();
}
}
/**
* Валидация сегментов фильтра
*/
public function validateSegments(array $segments): bool
{
foreach ($segments as $segment) {
if (!$this->validateSegment($segment)) {
return false;
}
}
return true;
}
/**
* Валидация одного сегмента
*/
private function validateSegment(string $segment): bool
{
// Бренд: brand-is-{code}
if (preg_match('/^brand-is-(.+)$/', $segment, $m)) {
return $this->isValidBrand($m[1]);
}
// Свойство с точным значением: prop_{code}-is-{value}
if (preg_match('/^prop_([^-]+)-is-(.+)$/', $segment, $m)) {
return $this->isValidPropertyValue($m[1], $m[2]);
}
// Свойство с диапазоном: prop_{code}-from-{min}[-to-{max}]
if (preg_match('/^prop_([^-]+)-from-(\d+)(?:-to-(\d+))?$/', $segment, $m)) {
return $this->isValidPropertyRange($m[1], (int)$m[2], isset($m[3]) ? (int)$m[3] : null);
}
// Цена: price-from-{min}[-to-{max}]
if (preg_match('/^price-from-(\d+)(?:-to-(\d+))?$/', $segment)) {
return true; // Цена всегда валидна
}
// Неизвестный формат — невалидно
return false;
}
/**
* Проверка валидности бренда
*/
private function isValidBrand(string $code): bool
{
return in_array($code, $this->validBrands, true);
}
/**
* Проверка валидности значения свойства
*/
private function isValidPropertyValue(string $propertyCode, string $value): bool
{
$propertyCode = strtoupper($propertyCode);
if (!isset($this->validPropertyCodes[$propertyCode])) {
return false;
}
// Проверяем существование значения
$property = $this->validPropertyCodes[$propertyCode];
if ($property['TYPE'] === 'L') {
// Для списков проверяем XML_ID варианта
return $this->isValidEnumValue($property['ID'], $value);
}
return true; // Для других типов просто проверяем наличие свойства
}
/**
* Проверка валидности диапазона свойства
*/
private function isValidPropertyRange(string $propertyCode, int $from, ?int $to): bool
{
$propertyCode = strtoupper($propertyCode);
if (!isset($this->validPropertyCodes[$propertyCode])) {
return false;
}
// Проверяем логичность диапазона
if ($to !== null && $from > $to) {
return false;
}
return true;
}
/**
* Загрузка валидных данных из БД
*/
private function loadValidData(): void
{
// Свойства инфоблока
$properties = \CIBlockProperty::GetList(
['SORT' => 'ASC'],
['IBLOCK_ID' => $this->iblockId, 'ACTIVE' => 'Y']
);
while ($prop = $properties->Fetch()) {
$this->validPropertyCodes[strtoupper($prop['CODE'])] = [
'ID' => $prop['ID'],
'TYPE' => $prop['PROPERTY_TYPE'],
'USER_TYPE' => $prop['USER_TYPE'],
];
}
// Бренды (если есть highload-блок или свойство)
$this->loadValidBrands();
}
/**
* Загрузка валидных брендов
*/
private function loadValidBrands(): void
{
// Вариант 1: Бренды в свойстве типа "Список"
if (isset($this->validPropertyCodes['BRAND'])) {
$enums = \CIBlockPropertyEnum::GetList(
['SORT' => 'ASC'],
['PROPERTY_ID' => $this->validPropertyCodes['BRAND']['ID']]
);
while ($enum = $enums->Fetch()) {
$this->validBrands[] = $enum['XML_ID'] ?: \CUtil::translit(
$enum['VALUE'],
'ru',
['replace_space' => '_', 'replace_other' => '_']
);
}
}
// Вариант 2: Highload-блок (расширить по необходимости)
}
/**
* Проверка значения списка
*/
private function isValidEnumValue(int $propertyId, string $xmlId): bool
{
$enum = \CIBlockPropertyEnum::GetList(
[],
['PROPERTY_ID' => $propertyId, 'XML_ID' => $xmlId]
)->Fetch();
return !empty($enum);
}
/**
* Разбивка URL на сегменты фильтра
*/
private static function splitFilterSegments(string $tail): array
{
return array_filter(explode('/', $tail));
}
/**
* Проверка наличия паттернов фильтра
*/
private static function hasFilterHints(array $segments): bool
{
foreach ($segments as $segment) {
if (preg_match('/(?:-is-|^prop_|^price-|^brand-)/', $segment)) {
return true;
}
}
return false;
}
/**
* Получение пути запроса
*/
private static function getRequestPath(): string
{
$uri = $_SERVER['REQUEST_URI'] ?? '';
$path = parse_url($uri, PHP_URL_PATH);
return $path ?: '/';
}
/**
* Получение ID инфоблока по коду раздела
*/
private static function getIblockIdBySection(string $sectionCode): ?int
{
$section = \CIBlockSection::GetList(
[],
['CODE' => $sectionCode, 'ACTIVE' => 'Y'],
false,
['ID', 'IBLOCK_ID']
)->Fetch();
return $section ? (int)$section['IBLOCK_ID'] : null;
}
/**
* Выдача 404 ошибки
*/
private static function trigger404(): void
{
if (!defined('ERROR_404')) {
define('ERROR_404', 'Y');
}
\Bitrix\Iblock\Component\Tools::process404(
'',
true,
true,
true,
'/404.php'
);
exit;
}
}Подключение валидации
<?php
// /local/php_interface/init.php
use Bitrix\Main\EventManager;
EventManager::getInstance()->addEventHandler(
'main',
'OnProlog',
function() {
// Только для фронтенда
if (defined('ADMIN_SECTION') && ADMIN_SECTION === true) {
return;
}
\Local\Catalog\SmartFilterValidator::validateCurrentRequest();
}
);Результаты
| До валидации | После валидации |
|---|---|
/catalog/chairs/brand-is-fake123/ → 200 OK (пустая страница) | → 404 |
/catalog/chairs/prop_width-from-999999/ → 200 OK | → 404 |
/catalog/chairs/random-garbage/ → 200 OK | → 404 |
/catalog/chairs/brand-is-ikea/ → 200 OK (есть товары) | → 200 OK ✓ |
Дополнительно: проверка наличия товаров
/**
* Проверка, что комбинация фильтра даёт результаты
*/
public function hasProducts(array $filter): bool
{
$element = \CIBlockElement::GetList(
[],
array_merge($filter, [
'IBLOCK_ID' => $this->iblockId,
'ACTIVE' => 'Y',
]),
false,
['nTopCount' => 1],
['ID']
)->Fetch();
return !empty($element);
}Проверка «есть ли товары» — отдельное решение, и по умолчанию я бы его не включал.
Технически всё верно: запрос с nTopCount => 1 дёшев. Проблема в смысле. Пустая выдача фильтра — не то же самое, что несуществующий адрес: товар мог закончиться на складе, его могли временно деактивировать, поставка приедет завтра. Отдав 404, вы выбьете из индекса страницу, которая через неделю снова будет с товарами, и возвращать её придётся заново.
Разумная граница: 404 — за синтаксически невалидные адреса (несуществующее свойство, значение вне справочника, бессмысленный диапазон), пустая страница с текстом «по вашему запросу ничего не найдено» и noindex — за валидные, но пустые комбинации. Первое означает «такого не бывает», второе — «сейчас пусто».
Кэшируйте результат валидации. Разбор адреса и сверка со справочниками выполняются на каждом запросе, включая ботовые, — а именно ботовая нагрузка и была причиной, по которой всё затевалось. Список допустимых значений меняется редко: кэшируйте его на час с тегом инфоблока, чтобы сброс происходил автоматически при изменении каталога.
Порядок внедрения
Валидатор — инструмент, который выдаёт 404 на боевом каталоге. Ошибка в нём стоит трафика, поэтому включать его нужно поэтапно.
- Режим наблюдения. Валидатор работает, но вместо 404 пишет в лог: «признал бы невалидным такой-то адрес». Сутки-двое такого лога дают полную картину.
- Разбор лога. В нём не должно быть адресов из sitemap, из внутренних ссылок и из отчёта по страницам с трафиком в панели вебмастера. Если такие есть — правила слишком строгие.
- Включение 404 и наблюдение за отчётом об индексировании ближайшие недели.
Быстрая проверка после включения:
# Раздел и валидный фильтр — 200
curl -o /dev/null -s -w "%{http_code} %{url_effective}\n" \
https://site.ru/catalog/kontejnery/ \
https://site.ru/catalog/kontejnery/brand-is-tara_ru/
# Несуществующее свойство и абсурдный диапазон — 404
curl -o /dev/null -s -w "%{http_code} %{url_effective}\n" \
https://site.ru/catalog/kontejnery/prop_nesushchestvuet-is-x/ \
https://site.ru/catalog/kontejnery/prop_shirina-from-999999/Итоги
Валидация URL умного фильтра решает три задачи разом: чистый индекс, сэкономленный краулинговый бюджет и остановленный рост кэша каталога. Что важно при внедрении:
- Белый список, источник — настройки каталога. Захардкоженный массив значений устареет и начнёт отдавать 404 на живых страницах.
- Проверка до тяжёлой работы. Смысл не только в коде ответа: под ботовой нагрузкой важнее, что запросы к базе вообще не выполнятся.
- 404 — за невалидный синтаксис,
noindex— за пустую выдачу. Это разные ситуации с разными последствиями. - Поэтапное включение через режим наблюдения. Валидатор, выбивший из индекса работающие разделы, обходится дороже проблемы, которую он решал.
- Это половина решения. Вторая половина — политика кэширования для тяжёлых URL и лимит частоты для ботов.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.