new

mFilter

Фильтр для MODX3 с поддержкой SEO-friendly URL
Автор дополнения
Николай Савин
Пакетов
20
Закачек
33 120
Обычно отвечает в течение суток
Автор дополнения
Пакетов
20
Закачек
33 120
Обычно отвечает в течение суток
Версия 1.5.1-beta1
Дата выпуска 08.09.2026
Загрузки 178
Просмотры 1 737
Внимание, этот компонент требует PHP 8.1 или выше!
Внимание, этот компонент требует MODX 3 или выше.



Возможности



  • Фасетная фильтрация — фильтрация по любым полям ресурсов, TV, опциям MiniShop3
  • SEO URL — человекочитаемые URL вида /catalog/brand_apple/color_black/
  • Кросс-фильтрация — подсчёт доступных значений с учётом активных фильтров
  • AJAX — обновление результатов без перезагрузки страницы
  • Headless API — REST API для интеграции с Vue, React, Svelte
  • SEO оптимизация — динамические title, description, H1, canonical
  • Словоформы — склонение названий фильтров в SEO-текстах
  • Интеграция — работает с MiniShop3, mSearch, pdoTools
  • Vue-интерфейс — современная админ-панель на Vue 3 + PrimeVue

Системные требования



  • MODX Revolution | 3.0.0+
  • PHP | 8.1+
  • MySQL | 5.7+ / MariaDB 10.3+

Зависимости


  • pdoTools 3.x — для работы сниппетов и шаблонизатора Fenom
  • VueTools — для административного интерфейса
  • MiniShop3 (опционально) — для фильтрации товаров

Быстрый старт


Добро пожаловать в документацию — docs.modx.pro/components/mfilter/quick-start

1.5.1-beta1

Исправления

  • Переменные, заявленные в документации, не доходили до чанков. Четыре штуки, все одного вида: значение собиралось из источника, которого не существует, и молча подменялось запасным. Заметить это мог только тот, кто переопределил чанк — стандартные чанки половину этих переменных не выводят.

    Чанк Переменная Что выводилось
    tplSlider $label ключ фильтра вместо названия из поля «Заголовок»
    tplSlider $prefix, $suffix пусто при любых настройках
    tplReset («Выбрано») $url пусто — переменная не передавалась вовсе
    tplOuter («Выбрано») $total пусто — код передавал только $count

    Причина у первых трёх общая: значения читались из результата getFiltersData(), который возвращает key, type, config и values — и ничего больше. Теперь заголовок берётся по той же цепочке, что и в обёртке фильтра: поле «Заголовок» набора → подпись TV или опции MS3 → лексикон. Префикс и суффикс — из настроек фильтра.

    $total передаётся рядом с $count как второе имя того же значения: переименование сломало бы чанки, опирающиеся на существующее.

  • $hex в чанке цветового свотча ставился не всегда. Если у значения не задан HEX, переменная отсутствовала, и чанк должен был это предусматривать. Теперь всегда приходит, пустой строкой.

  • $filters в tplOuter сниппета mFilter существовал только в native-режиме. С параметром element чанк, перебирающий $filters, молча выводил пустоту. Теперь оба режима передают его одинаково.

  • Ссылки пагинации вели в 404 на сайтах с нестандартным разделителем. Чанк mfilter.pagination собирал сегмент как page_2, игнорируя настройку mfilter.url_separator. На установках с -- — а это значение по умолчанию для новых установок с 1.4.0 — роутер такой адрес не разбирал. Изнутри сайта дефект незаметен: JS перехватывает клик и строит адрес сам, минуя роутер. В 404 попадали при прямом открытии — из закладки, новой вкладки или поиска. Разделитель теперь берётся из настройки: {var $sep = 'mfilter.url_separator' | option}.

  • Настройка mfilter.default_limit не влияла на размер страницы. Её читали роутер, UrlBuilder и native-режим, но не сниппет — там было зашито 20. Менялось только то, какие адреса несут сегмент лимита, а каталог продолжал отдавать по двадцать. Теперь настройка задаёт значение по умолчанию для &limit, а переданный &limit её перекрывает.

    Значение доводится и до AJAX. Иначе исправление создало бы разрыв ровно на тех сайтах, ради которых сделано: первая отрисовка по 12, первый же клик по фильтру — по 20, число страниц меняется под посетителем, а товары между двумя размерами страницы становятся недостижимы.

  • Селект сортировки и переключатель вида в mfilter.outer всегда показывали значения по умолчанию. Чанк читал $_pls['mfilter.sort'] и $_pls['mfilter.tpl'], а $_pls — это не массив глобальных плейсхолдеров: pdoTools заполняет его из $modx->placeholders при разборе шаблона ресурса, но данными самого чанка при вызове getChunk(). Этих ключей там никогда не было. Страница, открытая по адресу /catalog/sort_price-asc/, показывала в селекте «По названию», а переключатель подсвечивал сетку независимо от вида. Сниппет теперь передаёт $currentSort и $currentTpl готовыми значениями.

  • &pageParam не доходил до чанка пагинации в element-режиме. Из трёх мест отрисовки его передавали два. Кастомный чанк со ссылками вида ?page=2 выдавал там ?=2. Параметр задаёт имя GET-параметра, а не сегмента ЧПУ: имя сегмента — всегда page.

  • Пример шаблона каталога переведён на вызов сниппета в переменную ({set $mfilterOutput = '!mFilter' | snippet : [...]}) с выводом ниже по разметке. Плейсхолдеры сниппета появляются только после того, как он отработал, поэтому шаблон, читающий mfilter.seo.h1 или mfilter.seo.text выше вызова, получал пустоту. Обёртку результатов пример больше не дублирует — её рисует чанк tplOuter.

Внимание при обновлении: чанки не перезаписываются

Стандартные чанки при обновлении компонента не затираются — правки в mfilter.pagination и mfilter.outer дойдут только до новых установок. Сайтам со своей копией их нужно обновить вручную, иначе:

  • в mfilter.pagination останется зашитый page_, и ссылки пагинации продолжат отдавать 404 при прямом открытии;
  • в mfilter.outer останется собственное {var $currentSort = $_pls['mfilter.sort'] ?: 'pagetitle-asc'}, которое перекроет переданную переменную своим же значением по умолчанию — молча, без ошибок.

Симптом в обоих случаях выглядит как поломка компонента, а причина — в устаревшем чанке.

Внимание при обновлении: $_pls в шаблоне и в чанке — разные массивы

Это не изменение поведения, а причина, по которой разметка, перенесённая из чанка в шаблон ресурса, ломается. {$_pls['hash']} работает в чанке mfilter.outer (ключ hash есть в его данных) и не работает в шаблоне: там $_pls — глобальные плейсхолдеры, и нужный называется mfilter.hash.

Практическое следствие: не дублируйте обёртку data-mfilter-results в шаблоне. Получатся два вложенных контейнера, JS привяжется к внешнему, а data-mfilter-hash в нём будет пуст. AJAX без хэша не находит сохранённую конфигурацию вызова и уходит в native-режим — в выдачу попадают категории и прочие ресурсы, которых не было при первой загрузке. Молчаливость этого отката вынесена в отдельную задачу.

Внимание при обновлении: подписи над слайдерами изменятся

Там, где в чанке слайдера стоит {$label}, вместо ключа фильтра появится его название — price сменится на «Цена». Это и есть исправление, но подписи на существующих страницах поменяются сами собой, без правок в вёрстке.

Отдельно про $hex: он теперь приходит всегда, пустой строкой вместо отсутствия. Чанк с проверкой {if isset($hex)} пойдёт по истинной ветке в любом случае — проверять надо {if $hex}.

1.5.0-beta1

Исправления

  • SEO-шаблон не срабатывал на страницах с диапазоном. Шаблон подбирается по ключам фильтров, а SEO-адрес отдаёт диапазон двумя записями — price|min и price|max. Шаблон, настроенный на price, не совпадал ни с одной, и страница уходила на генерацию по умолчанию: вместо «Категория ценой от 10600» получалось «Категория - 10600, 15500». Теперь границы сворачиваются к базовому ключу перед подбором.

    Свёртка применяется только к именованию — подбору шаблона, плейсхолдерам и тексту по умолчанию. Правила индексации и canonical читают исходный набор границ: для mfilter.seo_max_values свёрнутый диапазон выглядел бы двумя значениями и при значении настройки 1 закрывал бы от индексации каждую такую страницу, а UrlBuilder опознаёт диапазон именно по суффиксам и собрал бы адрес вида price_10600-or-15500 — многозначный, с другим смыслом.

  • Диапазон в заголовке читается по-человечески. Раньше границы перечислялись как отдельные значения — «Категория - 10600, 15500», а односторонний диапазон давал «Категория - 10600» без указания, что это минимум. Теперь: «10600 — 15500», «от 10600», «до 15500» — через новые лексиконы mfl_seo_range_from, mfl_seo_range_to, mfl_seo_range_separator.

    Это часть свёртки из предыдущего пункта: при ней недостающая граница заменяется служебным числом 9223372036854775807, пригодным для условия в запросе и негодным для заголовка. До свёртки такая подстановка на этом пути не выполнялась вовсе.

  • Сортировка каталога при AJAX работала только внутри страницы. В element-режиме контроллер не передавал текущую сортировку элементу: тот отдавал ID в порядке по умолчанию, нарезка шла в нём, и сортировал только пагинатор — внутри полученного среза. Тот же дефект, что закрыт для серверной отрисовки в 1.4.7; здесь он жил на пути, которым сортировку и меняют.

    Заодно ключ сессионного кэша baseIds стал учитывать сортировку. Без этого исправление не доезжало: второе переключение сортировки без перезагрузки попадало в кэш и получало прежний порядок. Формат ключа изменился — после обновления первый запрос каждой сортировки пересчитает выборку.

  • Кнопка «Сбросить всё» не работала на странице, отфильтрованной только диапазоном. Такая страница рендерится с кнопкой и без чипов — снять диапазон по одному значению нельзя. JS определял серверный рендер по наличию чипов, не находил их, считал что рендера не было и перерисовывал блок своей стандартной разметкой: кастомные чанки заменялись, а обработчик кнопки не навешивался. Признаком теперь служит атрибут data-mfilter-selected-hash.

  • Параметр &searchVar сниппета mFilter задаёт имя параметра запроса с поисковой фразой; раньше было жёстко mse_query, и сайт, переименовавший его в mSearchForm, получал каталог, который запроса не видел. Работает в element-режиме. Имя намеренно отличается от queryVar: тот документирован как параметр элемента mSearch и пробрасывается ему — чтение того же имени запускало бы второй полный поиск на каждый запрос.

  • Кнопку сброса формы можно помечать атрибутом data-mfilter-reset наравне с классом mfilter-reset-btn. Оба маркера управляют и видимостью, и самим сбросом — раньше класс отвечал только за видимость, а сброс навешивался на type="reset" / .mfilter-reset, поэтому кнопка без type="reset" появлялась и пряталась, ничего не делая. Класс поддерживается по-прежнему.

  • Неразобранный сегмент URL влиял на SEO. Служебный ключ _hidden, куда JS складывает сегменты адреса, которые не удалось разобрать, доходил до заголовка страницы, участвовал в подсчёте лимитов индексации и попадал в canonical — тот собирался в виде /категория/_hidden_мусор/, а для мусорного значения ещё и создавалась запись слага. Ключ отбрасывается на входе в генерацию SEO.

  • Дублирование класса mfilter-pagination. Класс стоял и на обёртке в mfilter.outer, и на внутри `mfilter.pagination`. Снят с: контрактом объявлена обёртка — именно её ищет JS, чтобы заменить содержимое.

  • Переустановка пакета ломала все SEO-шаблоны. Колонка text_template добавлялась только при обновлении. Удаление компонента таблицы не трогает — они сохраняются намеренно, — а createObjectContainer() досыпает только отсутствующие таблицы, но не колонки в существующие. После цикла «снести и поставить заново» таблица оставалась со старым набором колонок под моделью, которая новое поле уже объявляет: xPDO подставлял его в каждый SELECT, все запросы к шаблонам падали с Unknown column, список шаблонов приходил пустым, и любая отфильтрованная страница уходила на дефолтную генерацию. Снаружи — «шаблоны перестали работать», в админке пустая вкладка, причина видна только в журнале. Проверка колонки теперь выполняется и при установке.
  • Приоритет SEO-шаблонов не работал. Сортировка шла по полю priority, которого у шаблонов нет — это колонка паттернов. Оба значения всегда были нулевыми, порядок перебора совпадал с порядком выдачи MySQL, и при двух подходящих шаблонах выбор был по сути случайным. Теперь сортировка идёт по sort_order — тому самому «Порядку» из интерфейса, где меньше значит выше. На сайтах с несколькими подходящими шаблонами выбор может измениться — в сторону задокументированного поведения.
  • При пустой выдаче AJAX-ответ не нёс SEO и адреса. Ранний возврат для случая «ничего не найдено» собирался без ключей seo и urls. JS трогает элементы только когда ключ пришёл, поэтому на пустом разделе оставались заголовок, описание и текст предыдущей подборки, а адресная строка — прежний URL.
  • Отсутствующий ресурс больше не стирает мета-теги. При запросе SEO для несуществующего ресурса возвращалась полная форма с пустыми строками, а пустая строка для JS — команда удалить `` и canonical. Теперь такой запрос не отвечает ничем, и документ остаётся как был.
  • SEO-текст не очищался при снятии фильтров. SeoBuilder::build() возвращал ключ text только из ветки с подобранным шаблоном. При снятии всех фильтров срабатывал ранний возврат, а при комбинации без подходящего шаблона — buildDefault(), и обе эти ветки ключа не содержали. JS обновляет элемент только когда ключ пришёл, поэтому на странице оставался текст от предыдущей подборки. Теперь все ветки возвращают полную форму ответа — недостающий ключ больше не пропустить.

  • Поле «Шаблон текста» в SEO-шаблонах не сохранялось. Поле было проведено через весь стек — редактор его отправлял, TemplateController записывал при создании и обновлении, SeoBuilder читал, — но отсутствовало в карте полей модели xPDO, и колонки в таблице не существовало. xPDO молча отбрасывает поле, которого не знает: save() возвращал успех, в журнал ничего не писалось, значение исчезало. toArray() его тоже не отдавал, поэтому при повторном открытии редактор показывал пустое поле. Колонка добавляется миграцией при обновлении; на новых установках она создаётся из модели.

  • Блок «Выбрано» сбрасывался на дефолтные чанки при фильтрации. SelectedFilters.js содержал захардкоженную копию четырёх стандартных чанков и пересобирал блок из неё при каждом AJAX-ответе — поэтому кастомная разметка жила ровно до первого клика по фильтру, после чего заменялась стандартной. Без ошибок и без возможности отключить.

    Причина в том, что разметка была продублирована на двух языках — в чанках и в JS. Поэтому дублирование убрано: блок теперь рендерится на сервере теми же чанками, что и при первой загрузке, а JS просто вставляет полученную разметку — так же, как это давно работает для списка товаров.

    Логика рендера вынесена из сниппета в сервис SelectedRenderer, сниппет сохраняет свой конфиг (чанки и флаги) и выводит его хэш в data-mfilter-selected-hash, контроллер возвращает готовый HTML в поле selected AJAX-ответа.

    Обратная совместимость полная: если в вашем tplOuter нет атрибута data-mfilter-selected-hash, сервер не отдаёт selected, и работает прежний клиентский рендер. Чтобы включить — добавьте атрибут (в стандартном чанке он уже есть):

  • В блок «Выбрано» при AJAX попадали посторонние ключи. Сниппет при разборе запроса пропускал только настроенные фильтры, а AJAX-ветка рендерила всё, что прислал браузер — включая служебный _hidden, куда JS складывает неразобранные сегменты URL. Теперь ключи, которых нет в наборе фильтров страницы, чипами не становятся. Заодно добавлено экранирование data-key и data-group/data-label в чанках — рядом с ними |esc уже стоял на значениях.
  • Блок «Выбрано» мог отрендериться «непустым» без единого чипа. Проверка на пустоту стояла до отсева служебных ключей: если в фильтрах оставались только ключи, которых нет в наборе страницы, контейнер получал empty => false — без чипов, но с рабочей кнопкой сброса и без учёта hideWhenEmpty. Теперь пустым считается состояние, когда не применён ни один фильтр.

    Диапазон при этом чипом не показывается — снять «от 1000 до 5000» одним значением нельзя, а два чипа «1000» и «5000» вводили бы в заблуждение. Но он считается применённым фильтром: блок остаётся на месте с кнопкой сброса, а {$count} учитывает его как один фильтр — независимо от того, пришёл он как price (query-URL) или как price|min + price|max (SEO-URL, роутер отдаёт границы раздельно).

  • {$count} в tplOuter блока «Выбрано» расходился с числом чипов. Считался входной массив, а не отрисованные значения: диапазон давал 2, плюс в счёт входили пропущенные ключи. Теперь это число применённых фильтров — совпадает с $activeFiltersCount формы. С числом чипов оно не совпадает ни в одном режиме: чипов столько, сколько значений, и фильтр «Цвет: красный, синий» даёт две кнопки и при grouped=1 (внутри одной группы), и при grouped=0. Диапазон считается один раз и чипа не даёт вовсе.
  • Подписи значений в блоке «Выбрано» для категорий MS3 и производителей. Раньше человекочитаемое название подставлялось только для ключей parent и vendor_id — чипы категорий MS3 (ms3_category, ms3_categories) и производителей (vendor, vendors) показывали сырой ID. Теперь распознаются все эти ключи.
  • С параметром element каталог рендерился пустым. mFilter безусловно записывал в параметры элемента свои дефолты showHidden / showUnpublished / showDeleted = false, даже когда вызывающий их не задавал. msProducts объявляет showHidden => true намеренно — товары почти всегда hidemenu = 1, их прячут из навигации; получив наш false, он добавлял в запрос hidemenu = 0 и возвращал пустой список ID. Теперь флаги видимости пробрасываются только когда заданы явно, и элемент сохраняет свои дефолты. Выявлено на боевом каталоге.
  • При AJAX сбрасывались чанки, лимит и scope (native mode). После применения фильтра карточки рендерились дефолтным mfilter.row вместо кастомного чанка, размер страницы становился 20, а parents / depth / where терялись целиком. Причина: element mode сохранял конфиг вызова и восстанавливал его на AJAX, а native mode не сохранял ничего — FilterController::applyLegacy() добивал пробелы захардкоженными дефолтами. Теперь оба режима используют общее хранилище конфига, а Filter::ajaxApplyFilters() пробрасывает scope-параметры в Filter::apply(). Побочно это чинит where в native mode: до сих пор он применялся только на первом рендере, а после фильтрации отваливался.
  • Дублирование фильтров в URL и в блоке «Выбрано». Мультизначные фильтры (param_5[]) показывались дважды — один раз слагами из URL, второй раз значениями из формы — и накапливались в адресной строке. Четыре места в JS парсили фильтры, и только одно нормализовало суффикс []; теперь нормализация вынесена в общий хелпер. Дополнительно parseFromForm() заменяет, а не дополняет данные из URL (сервер уже отрендерил инпуты отмеченными с реальными значениями), а сторонние query-параметры (utm_*, gclid) больше не попадают в состояние фильтров и не превращаются в чипы. В query-режиме мультизначные фильтры теперь пишутся как key[]=a&key[]=b — иначе PHP собирал из них строку с единственным значением, и по прямой ссылке выбор терялся.
  • data-total и data-page-count протухали после AJAX. Атрибуты обёртки результатов — часть контракта разметки (SSR их пишет, JS читает при инициализации), но после фильтрации никто их не обновлял: на выборке 81 → 35 пагинация показывала верные 3 страницы, а атрибуты продолжали заявлять 81 и 7. Теперь синхронизируются на каждом обновлении. Для native mode в ответ добавлены pageCount и page — без них не обновлялось и внутреннее состояние, из-за чего «Показать ещё» считал страницы по первому рендеру.
  • Warmup мог вернуть пустой каталог даже после исправления сниппета. Авто-созданные конфиги прогрева хранят параметры вызова, но WarmupKeyBuilder не включает в хэш значения, равные дефолтам — поэтому набор с showHidden = false и набор без него дают одинаковый хэш. Конфиг находился по хэшу, его параметры никогда не обновлялись, прогрев запускал элемент со старыми параметрами, получал пустой список и кэшировал его под тем самым хэшем, который читает страница. Теперь параметры авто-конфигов обновляются при расхождении, пустой результат прогрева не кэшируется вовсе (пустой список почти всегда означает сломанный конфиг, а не пустой раздел), а миграция при обновлении вычищает устаревшие флаги из существующих авто-конфигов и сбрасывает связанный кэш baseIds.

Новые возможности

  • Индивидуальные SEO для подборок. Новая вкладка задаёт собственные title, h1, description и текст для конкретной комбинации фильтров — например, только для страницы одного цвета. Раньше такое можно было получить лишь сторонним плагином со своей таблицей.

    Заполненное поле перекрывает SEO-шаблон, незаполненное — оставляет его значение. Каждое поле проходит цепочку «индивидуальная запись → шаблон → значение по умолчанию» самостоятельно, поэтому чтобы поменять только заголовок, достаточно заполнить один H1: при правке шаблона такие страницы обновятся вместе со всеми.

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

    Запись привязана к значению фильтра, а не к URL. Переименование слага, смена mfilter.url_separator или режима mfilter.slug_parent_prefix меняют адрес страницы, но запись продолжает работать — в отличие от решений, которые ключуют такие данные по адресу.

    Список перечисляет подборки по одному значению одного фильтра — их пишут руками, и просматривать их списком осмысленно. Диапазоны и сочетания нескольких значений не перечисляются: между границами диапазона адресов бесконечно много, а сочетания растут мультипликативно.

    Запись снимает noindex, навешенный mfilter.seo_max_filters или mfilter.seo_max_values, и восстанавливает canonical на саму отфильтрованную страницу. Оговорка: подборку по одному значению эти настройки и так не закрывают, поэтому на страницах из списка снимать нечего — правило работает для записей, охватывающих сочетания нескольких значений. Настройка mfilter.seo_noindex_filtered сильнее в любом случае: если владелец закрыл все фильтрованные страницы, текст в поле этого не отменяет.

    Если значение исчезло из каталога, запись не удаляется — помечается в списке и остаётся: текст писали руками.

  • Чанк формы знает о состоянии фильтров. В tplOuter сниппета mFilterForm добавлены переменные $hasFilters, $activeFiltersCount и $appliedFilters — теперь кнопку сброса (бейдж, сводку) можно показывать только когда что-то реально выбрано:

    
      Сбросить ({$activeFiltersCount})
    

    Раньше штатного способа не было: плейсхолдер mfilter.activeCount выставляется после рендера чанка и внутри него недоступен, а mfilter.filters заполняется только при разборе SEO-URL — фильтры из обычной query-строки в него не попадали. Новые переменные собираются из обоих источников.

    Элемент с атрибутом data-mfilter-reset или классом mfilter-reset-btn скрывается и показывается автоматически при AJAX-фильтрации, вложенный [data-mfilter-reset-count] получает количество. Оба маркера равноправны; класс поддерживается и дальше, стандартный чанк перешёл на атрибут. Важно: кнопку нужно рендерить всегда и скрывать атрибутом hidden — обёрнутую в {if $hasFilters} кнопку JS не найдёт в DOM и показать не сможет.

    Счётчик считает диапазон за один фильтр — так же, как сервер: price|min и price|max схлопываются в price.

  • Разрешение конфликтов слагов и опциональный префикс родителя. Две категории с одинаковым названием в разных ветках (например «Axis» в «Печах» и в «Топках») давали один и тот же слаг axis. Второй INSERT падал на уникальном индексе uk_filter_slug_culture с Duplicate entry, запись не создавалась, и UrlBuilder подставлял в URL сырое значение — числовой ID категории вместо человекочитаемого адреса. Причина: SlugManager::save() проверял существование только по паре (filter_key, value), но не по (filter_key, slug).

    Теперь конфликт разрешается до записи. Поведением управляет новая настройка mfilter.slug_parent_prefix:

    Режим Поведение
    on_conflict (по умолчанию) Обычный слаг; при конфликте добавляется родитель — topki-axis. Существующие URL не меняются
    always Родитель добавляется всегда — pechi-axis, topki-axis. Меняет существующие URL, потребуются 301-редиректы
    never Только числовой суффикс — axis-2

    Префикс родителя доступен только там, где значение фильтра — ID ресурса. Раньше это решалось по имени ключа: два захардкоженных списка (SlugManager::getOrCreate() и логика префикса) должны были совпадать с supports() каждого типа — и разъехались. Из-за этого на MS3-каталогах слаги категорий писались в таблицу, но UrlBuilder всё равно подставлял в URL числовой ID, потому что ключей ms3_category/ms3_categories не было в списке.

    Теперь тип фильтра сам сообщает, с чем работает, — новый метод FilterTypeInterface::getValueKind():

    Вид значения Слаг для числового Префикс родителя Типы
    resource да да parents, ms3_categories
    entity да нет vendors
    scalar (по умолчанию) нет нет все остальные

    MS3CategoriesFilterType наследует resource от ParentsFilterType — новые типы-наследники получают корректное поведение автоматически. Для сторонних типов ничего не ломается: значение по умолчанию scalar объявлено в AbstractFilterType, от которого предписывает наследоваться документация.

  • Понятная ошибка при ручном вводе занятого слага. Раньше вкладка «Алиасы» отвечала безликой «Ошибкой сохранения» с кодом 500, а в лог падал Duplicate entry — было неясно, что слаг занят и каким значением. Теперь возвращается 409 с текстом «Слаг «axis» уже занят значением «124»». Автоподстановка суффикса при ручном вводе намеренно не делается: молча переименовывать введённое человеком хуже, чем отказать.

  • Фильтр и сортировка по источнику во вкладке «Алиасы». Добавлен выпадающий список «Все источники» рядом с фильтром по ключу и сортировка по колонке «Источник».

Внимание при обновлении: заголовки страниц с диапазоном изменятся

Страницы, отфильтрованные по диапазону, раньше не подбирали SEO-шаблон вовсе и генерировали заголовок по умолчанию. Теперь шаблон, настроенный на базовый ключ (price), к ним применяется — h1, title и description таких страниц изменятся, в том числе у уже проиндексированных.

Правила индексации изменились в одну сторону: диапазон теперь считается одним фильтром, а не двумя. Раньше страница «цена от–до плюс бренд» давала три ключа при дефолтном mfilter.seo_max_filters = 2 и закрывалась от индексации; теперь это два фильтра, и страница открыта. Служебный сегмент URL, который не удалось разобрать, тоже перестал считаться фильтром.

Вместе с noindex переезжает и canonical: у закрытой страницы он указывал на раздел без фильтров, у открытой указывает на неё саму. Для затронутых страниц это смена цели canonical — сам вид адресов при этом не менялся.

Подсчёт значений внутри фильтра по-прежнему идёт по исходным границам: свёрнутый диапазон выглядел бы двумя значениями и при mfilter.seo_max_values = 1 закрывал бы каждую такую страницу.

Внимание при обновлении: класс mfilter-pagination — на обёртке

Класс объявлен контрактом контейнера, в который вставляется {$pagination}, и снят с ` внутри стандартного чанка пагинации. Затрагивает сайты со своимtplOuter` без этого класса, полагавшиеся на класс из стандартного чанка: после обновления AJAX перестанет обновлять блок пагинации, ссылки продолжат работать обычной перезагрузкой. Лечится переносом класса на контейнер.

Стандартные чанки при обновлении не перезаписываются, поэтому правки mfilter.form и mfilter.pagination доходят только до новых установок. Существующим сайтам делать ничего не нужно — прежняя разметка поддержана.

Отдельно про блок «Выбрано»: исправление, из-за которого он сбрасывался на стандартные чанки на странице с одним диапазоном, опирается на атрибут data-mfilter-selected-hash в tplOuter. У сайта со своим чанком без этого атрибута дефект сохранится — атрибут нужно добавить вручную, как описано в разделе про серверный рендер блока.

Внимание при обновлении: изменятся URL категорий MS3 и производителей

До этой версии слаги для фильтров «Категории товаров (MS3)» (ms3_category, ms3_categories) и «Производители» (vendor, vendors, ms|vendor) не подставлялись в адрес: UrlBuilder видел числовое значение и отдавал его как есть. Адреса выглядели так:

/catalog/ms3_categories--61/
/catalog/vendor--17/

Теперь эти типы объявляют, что их значения — идентификаторы, слаги подхватываются, и те же страницы получают человекочитаемые адреса:

Последние обсуждения в сообществе MODX.pro