Разработка на 1С-Битрикс Михаил Базаров
Блог-note

Уникальная ссылка и СЕО информация для каждого торгового предложения, на новой системе роутингов

Просмотров: 11
Задача: у каждого торгового предложения (SKU) должен быть свой адрес в виде человекочитаемого ЧПУ с ID предложения, автоматический выбор предложения при заходе по прямой ссылке, а также своя мета-информация — h1, title, keywords, description и канонический адрес.

Это улучшенный вариант двух прошлых заметок: Уникальный URL для торговых предложений (SKU) и «Уникальная СЕО информация для каждого торгового предложения». Там всё строилось на GET-параметре ?offer= и кастомном 404.php. Здесь то же самое делается аккуратно, на новой системе роутингов Битрикса.

Почему прошлый способ стоит улучшить

  • ЧПУ с ID предложения в пути обрабатывался через кастомный 404.php, который на каждую несуществующую страницу перебирает все предложения инфоблока — лишняя нагрузка и хрупкая логика.
  • Мета-информация подставлялась через ручное выключение штатных SET_TITLE/SET_META_* в element.php — легко сломать при обновлении типового компонента.
  • URL получался с GET-параметром ?offer=ID — не самый красивый вариант для шэринга и SEO.

Новая система роутингов (класс Bitrix\Main\Routing\RoutingConfigurator, файл local/routes/web.php) позволяет объявить URL с ID предложения заранее, поэтому всё перечисленное не нужно.

Как это устроено

Схема такая: роутер принимает ЧПУ вида /catalog/<раздел>/<товар>/<ID-ТП>/, забирает числовой ID из последнего сегмента, кладёт его в запрос как OFFER_ID и отдаёт управление каталогу уже с «чистым» путём. Дальше битриксовый bitrix:catalog находит обычный товар, а шаблон карточки по OFFER_ID выбирает предложение и подставляет его мета-информацию.

1. Маршруты

Файл local/routes/web.php. Важен порядок: сначала маршрут с ID предложения, потом общий по каталогу — иначе общий правило «съест» адрес с ID.

<?php

use Bitrix\Main\Application;
use Bitrix\Main\Routing\RoutingConfigurator;
use Bitrix\Main\Routing\Controllers\PublicPageController;

return static function (RoutingConfigurator $routes) {
    $routes->any('/catalog', new PublicPageController('/catalog/index.php'));
    $routes->any('/catalog/', new PublicPageController('/catalog/index.php'));

    // URL торгового предложения: /catalog/<раздел>/<товар>/<ID-ТП>/
    // Числовой ID последним сегментом отдаём каталогу как OFFER_ID,
    // а путь нормализуем до чистого SEF, чтобы bitrix:catalog нашёл товар.
    $routes
        ->any('/catalog/{path}/{offerId}/', function ($path, $offerId) {
            $request = Application::getInstance()->getContext()->getRequest();
            $request->set('OFFER_ID', (int)$offerId);
            $_REQUEST['OFFER_ID'] = (int)$offerId;

            $cleanUri = '/catalog/' . $path . '/';
            $_SERVER['REQUEST_URI'] = $cleanUri;
            $request->setRequestUri($cleanUri);

            require $_SERVER['DOCUMENT_ROOT'] . '/catalog/index.php';
        })
        ->where(['path' => '.+', 'offerId' => '\d+']);

    // Остальные страницы каталога: разделы, товары, фильтр.
    $routes
        ->any('/catalog/{path}/', new PublicPageController('/catalog/index.php'))
        ->where('path', '.+');
};
  • offerId ограничен регуляркой \d+, поэтому маршрут срабатывает только на числовой последний сегмент.
  • В замыкании ID уходит в запрос как OFFER_ID, а REQUEST_URI подменяется на чистый путь без ID — SEF-движок каталога видит обычный товар.
  • Если у товаров могут быть числовые символьные коды, добавьте проверку, что последний сегмент действительно является торговым предложением этого товара.

2. ЧПУ и подмена h1 при переключении

В script.js шаблона catalog.element добавляем две функции и вызываем их в конце selectOfferProp. ID дописывается в конец пути:

setOffer: function(offerNum)
{
    this.offerNum = parseInt(offerNum);
    this.setCurrent();
    this.updateH1ByOffer();
},

updateUrlByOffer: function()
{
    var offerId = (this.offers && this.offers[this.offerNum]) ? this.offers[this.offerNum].ID : 0;
    if (!offerId)
        return;

    // Снимаем старый ID предложения из конца пути и дописываем выбранный.
    var path = window.location.pathname
        .replace(/\/\d+\/?$/, '/')   // убрать предыдущий ID
        .replace(/\/+$/, '');        // убрать хвостовые слэши
    var url = path + '/' + offerId + '/';

    if (url !== window.location.pathname)
    {
        history.replaceState(null, '', url);
    }
},

updateH1ByOffer: function()
{
    var h1 = document.querySelector('h1');
    if (!h1)
        return;

    var offerName = (this.offers && this.offers[this.offerNum]) ? this.offers[this.offerNum].NAME : '';
    if (offerName)
    {
        h1.textContent = offerName;
    }
},

И в selectOfferProp, в самом конце перед закрывающей скобкой:

// start CUSTOM
this.updateUrlByOffer();
this.updateH1ByOffer();
// end CUSTOM
  • updateUrlByOffer() меняет адрес без перезагрузки и получает ЧПУ вида /catalog/.../tovar/123/.
  • updateH1ByOffer() подменяет текст тега <h1> на название предложения. Ищем именно тег, а не id=pagetitle, — код не зависит от вёрстки.

3. Уникальные СЕО-мета при заходе по прямой ссылке

Парсить URL в шаблоне не нужно — роутер уже положил ID в запрос как OFFER_ID. В component_epilog.php шаблона catalog.element читаем его и подставляем мету конкретного предложения:

<?php
// select target offer
$offerId = (int)$this->request->get('OFFER_ID');
if ($offerId <= 0)
{
    $offerId = (int)($_REQUEST['OFFER_ID'] ?? 0);
}

$offerNum = false;
if ($offerId > 0 && !empty($templateData['OFFER_IDS']) && is_array($templateData['OFFER_IDS']))
{
    $offerNum = array_search($offerId, $templateData['OFFER_IDS']);
}

if ($offerNum !== false && !empty($templateData['ITEM']['JS_OFFERS'][$offerNum]['ID']))
{
    $realOfferId = (int)$templateData['ITEM']['JS_OFFERS'][$offerNum]['ID'];

    $offer = \CIBlockElement::GetList(
        array(),
        array('=ID' => $realOfferId),
        false,
        array('nTopCount' => 1),
        array('ID', 'NAME', 'IBLOCK_ID', 'CANONICAL_PAGE_URL')
    )->GetNext();

    if ($offer)
    {
        // Штатные СЕО-свойства торгового предложения
        $seoValues = new \Bitrix\Iblock\InheritedProperty\ElementValues(
            $offer['IBLOCK_ID'],
            $offer['ID']
        );
        $seoProps = $seoValues->getValues();

        $APPLICATION->SetTitle($offer['NAME']);
        $APPLICATION->SetPageProperty('title', $seoProps['ELEMENT_META_TITLE']);
        $APPLICATION->SetPageProperty('keywords', $seoProps['ELEMENT_META_KEYWORDS']);
        $APPLICATION->SetPageProperty('description', $seoProps['ELEMENT_META_DESCRIPTION']);
        if (!empty($offer['CANONICAL_PAGE_URL']))
        {
            $APPLICATION->SetPageProperty('canonical', $offer['CANONICAL_PAGE_URL']);
        }
    }
    ?>
    <script>
        BX.ready(function () {
            if (!!window.<?=$templateData['JS_OBJ']?>) {
                window.<?=$templateData['JS_OBJ']?>.setOffer(<?=$offerNum?>);
            }
        });
    </script>
    <?php
}
  • OFFER_ID из запроса через array_search превращаем в позицию предложения в OFFER_IDS.
  • CIBlockElement::GetList отдаёт название, ID инфоблока и канонический URL предложения.
  • InheritedProperty\ElementValues возвращает SEO-параметры именно этого предложения.
  • $APPLICATION->SetPageProperty устанавливаем мета-теги, SetTitle — заголовок страницы.
  • На BX.ready вызываем setOffer() — предложение выбирается при загрузке, а h1 приводит в порядок JS из шага 2.

Почему h1 всё равно меняем на клиенте

В шаблоне каталога тег <h1> выводится до подключения вложенного bitrix:catalog.element. К моменту выполнения component_epilog.php h1 уже отрисован, поэтому серверный SetTitle() его не изменит. Серверный SetTitle нужен для корректного заголовка при прямом заходе, а видимый h1 обязательно правится клиентским updateH1ByOffer().

Про canonical и кеш

  • ЧПУ с ID предложения — это тот же документ товара. Если у предложения не задан свой канонический URL, такие адреса должны канонизироваться на базовый URL товара, иначе получите дубли в индексе. Отдельные страницы под SKU — только с явно заданным CANONICAL_PAGE_URL.
  • Так как путь перед каталогом нормализуется до одного и того же вида, кеш компонента может отдавать одинаковую разметку для разных предложений. component_epilog.php выполняется и на закешированной странице, поэтому серверная мета и вызов setOffer() отработают корректно. Но если в шаблоне появляется зависимая от предложения разметка в теле компонента — учитывайте OFFER_ID в кеше или отключайте кеш для таких URL.

Итог

Порядок действий: объявляем каталог и отдельный маршрут для предложения в local/routes/web.php; ID предложения принимаем числовым сегментом, кладём в OFFER_ID и нормализуем путь; ЧПУ формируем в JS через history.replaceState; уникальные title/keywords/description/canonical подставляем в component_epilog.php через InheritedProperty\ElementValues; видимый h1 меняем на клиенте. На выходе — человекочитаемый URL с ID предложения, автоматический выбор SKU по ссылке и своя мета для каждого ТП, без кастомного 404 и без ручного дёргания параметров компонента.

Услуги

Стоимость разработки на 1С-Битрикс

Стоимость разработки сайта зависит от объёма и сложности проекта. Ниже приведены ориентировочные цены, как правило не выходят за обозначенные рамки. Срок разработки зависит от сложности проекта: как правило называю сроки с запасом.

Финальная стоимость и сроки разработки обговариваются на этапе обсуждения. Скачайте опросник на разработку, заполните как можно подробнее и вышлите удобным способом. После ознакомления смогу задать уточняющие вопросы и оценить проект.

Включено в стоимость разработки

В цену уже заложено всё, что нужно для легального и быстрого запуска. Доплачивать за базовые вещи не придётся.

  • Лицензия и модули

    Лицензия на «1С-Битрикс» нужной редакции, дополнительные модули и видео-инструкции по работе с готовым проектом.

  • Скорость и SEO

    Оптимизация кода и сервера под быструю загрузку, базовая SEO-оптимизация и добавление сайта в поисковые системы.

Блог-note

Заметки по 1С-Битрикс