Skip to content
mFilter
mFilter
Фасетная фильтрация для MODX 3 с поддержкой SEO URL
  1. Компоненты
  2. mFilter
  3. Фронтенд
  4. Плейсхолдеры

Плейсхолдеры

Справочник по всем плейсхолдерам и переменным, которые mFilter выставляет в MODX и чанки.

Синтаксис

MODX-плейсхолдер mfilter.something доступен из шаблона тремя способами:

  • MODX-тег: [[+mfilter.something]]
  • Fenom (pdoTools), через массив $_pls: {$_pls['mfilter.something']}
  • Fenom, явным вызовом: {$_modx->getPlaceholder('mfilter.something')}

Внимание

Записи вида {$mfilter.something} (dot-notation через переменную $mfilter) не работают — такой переменной в Fenom-scope нет. Используйте $_pls['mfilter.something'].

$_pls в шаблоне и в чанке — это разные массивы

Одна и та же запись {$_pls['ключ']} читает разные данные в зависимости от того, где стоит.

ГдеЧто лежит в $_plsКак читать плейсхолдер mfilter.hash
Шаблон ресурсаглобальные плейсхолдеры MODX{$_pls['mfilter.hash']}
Чанк, отрисованный через getChunk (tplOuter, tpl, tplPagination и прочие)только данные, переданные этому чанку{$hash} — как переменная

Причина в pdoTools: парсер шаблона передаёт Fenom массив $modx->placeholders, а getChunk() — массив свойств чанка, и в обоих случаях кладёт его копию в $_pls. Глобальные плейсхолдеры в чанк не подмешиваются.

Отсюда две типовые ошибки:

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

Внутри чанка глобальный плейсхолдер всё же доступен MODX-тегом [[+mfilter.sort]] — но переменная чанка быстрее и надёжнее, если она есть. Списки переменных для каждого чанка — в секциях ниже.

Не дублируйте обёртку результатов в шаблоне

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

Глобальные плейсхолдеры

Устанавливаются плагином mfilter при OnHandleRequest для страницы каталога с активными фильтрами, и/или сниппетом mFilter во время рендера. Доступны в шаблоне ресурса — через $_pls или MODX-тегом; внутри чанков pdoTools — только MODX-тегом, см. выше.

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

fenom
{set $mfilterOutput = '!mFilter' | snippet : ['element' => 'msProducts', 'parents' => $_modx->resource.id]}

<h1>{$_pls['mfilter.seo.h1'] ?: $_modx->resource.pagetitle}</h1>
...
<div class="catalog-content">{$mfilterOutput}</div>

Активные фильтры и URL

ПлейсхолдерТипОписание
mfilter.filtersarrayАктивные фильтры: ['brand' => ['apple'], 'color' => ['red']]
mfilter.activeFiltersarrayСиноним mfilter.filters
mfilter.base_uristringURI страницы каталога без сегмента фильтров (/catalog/)
mfilter.filter_uristringСегмент фильтров, добавляемый после base_uri (brand--apple/color--red/)
mfilter.baseIdsarrayID товаров текущей выборки (устанавливается сниппетом mFilter)

Сортировка и пагинация

ПлейсхолдерТипОписание
mfilter.sortstringТекущая сортировка: "pagetitle-asc", "price-desc"
mfilter.sortBystringТолько поле: "pagetitle", "price"
mfilter.sortDirstringТолько направление: "asc" / "desc"
mfilter.limitintТекущий лимит на странице
mfilter.defaultLimitintДефолтный лимит из настроек (для сравнения)
mfilter.pageintТекущая страница
mfilter.tplstringАктивный шаблон карточки ("tpl1", "tpl2")

SEO

Устанавливаются, когда есть хотя бы один активный фильтр. На нефильтрованных страницах — пустые (noindexfalse).

ПлейсхолдерТипОписание
mfilter.seo.titlestringSEO title из SEO Templates
mfilter.seo.h1stringSEO H1
mfilter.seo.descriptionstringSEO meta description
mfilter.seo.canonicalstringCanonical URL (при noindex указывает на страницу без фильтров)
mfilter.seo.noindexboolФлаг noindex (см. Системные настройки)
mfilter.seo.textstringПроизвольный SEO-текст из SEO Templates

Прочее

ПлейсхолдерТипОписание
mfilter.hashstringХэш конфигурации вызова mFilter; по нему AJAX воспроизводит тот же вызов
mfilter.resultsstringHTML отрендеренных карточек (только при &toPlaceholders=1 в сниппете mFilter)
mfilter.paginationstringHTML пагинации (только при &toPlaceholders=1)

toPlaceholders=1

Использование &toPlaceholders=1 в вызове сниппета mFilter разбивает вывод на mfilter.results и mfilter.pagination без оборачивающего tplOuter-чанка. Теряются data-mfilter-results, data-page-count и вся SSR-инициализация — фронтенд-JS не сможет её подхватить. Используйте обычный вывод сниппета и оборачивающий чанк tplOuter.

Использование в шаблоне ресурса

SEO-теги в <head>

html
<title>{$_pls['mfilter.seo.title'] ?: $_modx->resource.pagetitle}</title>
<meta name="description" content="{$_pls['mfilter.seo.description'] ?: $_modx->resource.description}">

{if $_pls['mfilter.seo.canonical']}
    <link rel="canonical" href="{$_pls['mfilter.seo.canonical']}">
{/if}

{if $_pls['mfilter.seo.noindex']}
    <meta name="robots" content="noindex, follow">
{/if}

Логика фолбэков: если фильтров нет, mfilter.seo.* пусты, и ?: возвращает стандартное значение ресурса.

Почему noindex, follow, а не noindex, nofollow

JS-часть при AJAX-обновлении фильтров всегда выставляет content="noindex, follow" — это лучше для SEO, чем nofollow (link equity со страницы каталога с фильтрами продолжает распределяться по товарам). Чтобы SSR-разметка не рассогласовывалась с AJAX-состоянием, в шаблоне тоже указывайте follow.

H1 и SEO-текст в теле страницы

Обязательные маркеры для AJAX

<h1> и контейнер SEO-текста нужно явно пометить атрибутом или классом — иначе JS не найдёт их при AJAX-фильтрации и они обновятся только после F5. У страницы может быть несколько <h1> (hero, sidebar), поэтому просто «первый h1» не подходит.

  • <h1> — атрибут data-mfilter-h1 или класс mfilter-h1
  • SEO-текст — атрибут data-mfilter-seo-text или класс mfilter-seo-text
html
<h1 data-mfilter-h1>{$_pls['mfilter.seo.h1'] ?: $_modx->resource.pagetitle}</h1>

<div data-mfilter-seo-text{if !$_pls['mfilter.seo.text']} style="display:none"{/if}>
    {$_pls['mfilter.seo.text']}
</div>

При AJAX-фильтрации JS сам управляет display контейнера SEO-текста (скрывает при пустом значении, показывает при заполненном), поэтому inline-display:none при пустом SSR-значении — только чтобы контейнер не мелькал до первой фильтрации.

Хлебные крошки с фильтрами

html
<nav class="breadcrumbs">
    <a href="/">Главная</a>
    /
    <a href="{$_pls['mfilter.base_uri'] ?: $_modx->makeUrl($_modx->resource.id)}">
        {$_modx->resource.pagetitle}
    </a>
    {if $_pls['mfilter.seo.h1']}
        / <span>{$_pls['mfilter.seo.h1']}</span>
    {/if}
</nav>

Проверка «есть ли активные фильтры»

Отдельного плейсхолдера hasFilters нет — проверяйте сам массив:

html
{if $_pls['mfilter.filters']}
    <a href="{$_pls['mfilter.base_uri']}" class="reset-all">Сбросить все фильтры</a>
{/if}

Условная сортировка/лимит в UI-контролах

html
<select data-mfilter-sort>
    <option value="pagetitle-asc" {if $_pls['mfilter.sort'] == 'pagetitle-asc'}selected{/if}>А-Я</option>
    <option value="price-asc" {if $_pls['mfilter.sort'] == 'price-asc'}selected{/if}>Сначала дешевле</option>
</select>

<select data-mfilter-limit>
    <option value="12" {if $_pls['mfilter.limit'] == 12}selected{/if}>12</option>
    <option value="24" {if $_pls['mfilter.limit'] == 24}selected{/if}>24</option>
</select>

Сортировку можно вывести и кнопками — значение указывается прямо в атрибуте:

html
<button data-mfilter-sort="price-asc">Сначала дешёвые</button>
<button data-mfilter-sort="price-desc">Сначала дорогие</button>
<a href="#" data-mfilter-sort="pagetitle-asc">По названию</a>

JS обрабатывает <select> по событию change, а <button> и <a> — по клику, с отменой перехода.

Подсветку активной кнопки JS не делает — её ставит вёрстка. В шаблоне сравнивайте значение атрибута с плейсхолдером mfilter.sort, в чанке tplOuter — с переменной $currentSort; формат поле-направление у обоих одинаковый.

Переменные внутри чанков

При рендере своих чанков pdoTools передаёт им данные напрямую как Fenom-переменные (не через $_pls).

Чанки mFilter (карточки товаров)

tplOuter (обёртка результатов):

ПеременнаяОписание
$rowsHTML всех карточек товаров
$paginationHTML пагинации
$totalКоличество найденных товаров
$pageТекущая страница
$pageCountВсего страниц
$limitТоваров на странице
$hashХэш конфигурации вызова; выводится в data-mfilter-hash
$currentSortТекущая сортировка в формате поле-направление (price-asc) — для подсветки активного пункта в селекте или кнопках
$currentTplТекущий алиас вида из &tpls (tpl1, tpl2) — для подсветки переключателя сетка/список
$filterKeysКлючи фильтров страницы через запятую; выводится в data-mfilter-keys
$filtersПрименённые фильтры массивом, в исходном виде: на SEO-адресе диапазон приходит двумя ключами (price|min, price|max). Свёрнутый набор есть у формы — $appliedFilters

tpl1, tpl2 и другие (карточка одного товара) — обычные pdoResources/msProducts-переменные ресурса:

html
<div class="product-card" data-id="{$id}">
    <img src="{$image}" alt="{$pagetitle}">
    <h3>{$pagetitle}</h3>
    <div class="price">{$price | number:0} ₽</div>
</div>

Чанки mFilterForm (форма фильтров)

tplOuter:

ПеременнаяОписание
$filtersHTML всех фильтров, склеенных вместе
$resourceIdID текущего ресурса каталога
$formAttrsГотовая строка атрибутов тега <form>
$formId, $formClassЗначения параметров &formId и &formClass
$ajax, $ajaxModeВключён ли AJAX и его режим
$autoSubmitВключена ли автоотправка формы
$debugProfilerВключён ли профилировщик — служебная, для отладки
$hasFiltersПрименён ли хотя бы один фильтр (bool)
$activeFiltersCountСколько фильтров применено; диапазон считается за один
$appliedFiltersПрименённые фильтры массивом, диапазоны свёрнуты в один ключ

Кнопка сброса

$hasFilters и $activeFiltersCount нужны, чтобы отрисовать кнопку сброса со счётчиком — см. mFilterForm.

&tpl — обёртка одного фильтра (по умолчанию чанк mfilter.filter):

ПеременнаяОписание
$keyКлюч фильтра (vendor, color, price…)
$labelНазвание фильтра
$typeТип: default, number, boolean, parents, ms3_categories, colors, vendors, date
$itemsHTML значений фильтра
$itemCountСколько значений отрисовано
$activeCountСколько значений этого фильтра выбрано
$multipleМножественный выбор (checkbox vs radio)
$collapsedСвёрнут ли блок; сейчас всегда false

tplItem (одно значение — checkbox/radio):

ПеременнаяОписание
$keyКлюч фильтра
$valueЗначение
$slugСлаг для URL
$labelОтображаемый текст
$countКоличество товаров
$activeЗначение выбрано (bool)
$disabledЗначение недоступно (нет товаров при текущих остальных фильтрах)
$multipleМножественный выбор (checkbox vs radio)

tplBoolean (переключатель да/нет):

ПеременнаяОписание
$key, $value, $label, $count, $activeКак в tplItem

tplColor (цветовой свотч):

ПеременнаяОписание
$key, $value, $label, $activeКак в tplItem
$hexHEX-код цвета; пустая строка, если у значения он не задан

tplSlider (range-фильтр):

ПеременнаяОписание
$keyКлюч фильтра
$labelНазвание фильтра — то же значение, что в tplFilter
$min, $maxДоступный диапазон в текущей выборке
$minValue, $maxValueВыбранные пользователем значения
$stepШаг
$prefix, $suffixПрефикс/суффикс единицы измерения

Чанки mFilterSelected (блок «Выбрано»)

tplOuter:

ПеременнаяОписание
$itemsHTML всех выбранных значений
$resetHTML кнопки сброса, отрисованной через tplReset
$total, $countСколько фильтров применено; два имени одного значения. Диапазон считается за один и чипа не даёт
$emptyНет применённых фильтров (bool)
$hiddenБлок следует скрыть — при $empty и включённом &hideWhenEmpty
$hashХэш конфигурации блока; выводится в data-mfilter-selected-hash
$filterLabelsJSON-карта «ключ → название» для инициализации JS

tplGroup (группа значений одного фильтра):

ПеременнаяОписание
$key, $labelКлюч и название фильтра
$itemsHTML значений внутри группы
$countСколько значений в группе
$showLabelВыводить ли название фильтра — из параметра &showLabels

tplItem (одна chip):

ПеременнаяОписание
$keyКлюч фильтра
$valueЗначение (машиночитаемое)
$valueLabelОтображаемый текст значения
$labelСиноним $valueLabel
$filterLabelНазвание фильтра, которому принадлежит значение

tplReset (кнопка «сбросить всё»):

ПеременнаяОписание
$textПодпись кнопки — из &resetText или лексикона
$urlАдрес страницы без фильтров

Стандартный чанк рисует <button>, который обрабатывает JS. $url нужен, если сброс делается ссылкой — тогда он работает и без JS:

fenom
<a href="{$url}" class="mfilter-selected-reset" data-mfilter-reset-selected>{$text}</a>

Использование в JavaScript

Есть два пути.

Через window.mFilter (клиентский API)

javascript
const instance = window.mFilter.getInstance();

instance.state.filters;      // текущие фильтры
instance.state.sort;         // сортировка
instance.state.limit;        // лимит
instance.setFilter('brand', ['apple']);
instance.submit();

Полный список методов — в JS API.

Через события

javascript
document.addEventListener('mfilter:success', function (e) {
    console.log(e.detail.filters, e.detail.total, e.detail.seo);
});

e.detail.seo содержит те же данные, что попадают в mfilter.seo.* плейсхолдеры при SSR. Полный список событий — в Events.

Примеры

Пустые результаты

html
{if $total == 0}
    <div class="empty-results">
        <p>По вашему запросу ничего не найдено.</p>
        {if $_pls['mfilter.filters']}
            <p>Попробуйте <a href="{$_pls['mfilter.base_uri']}">сбросить фильтры</a>.</p>
        {/if}
    </div>
{/if}

$total — из tplOuter чанка mFilter. mfilter.filters — глобальный плейсхолдер.

Условная канонизация

Если у фильтрованной страницы noindex, canonical указывает на исходный ресурс (без фильтров). Иначе — на текущий URL.

html
{if $_pls['mfilter.seo.canonical']}
    <link rel="canonical" href="{$_pls['mfilter.seo.canonical']}">
{else}
    <link rel="canonical" href="{$_modx->makeUrl($_modx->resource.id, '', '', 'full')}">
{/if}

Логирование фильтрации в аналитику

html
<script>
document.addEventListener('mfilter:success', function (e) {
    if (typeof gtag !== 'undefined') {
        gtag('event', 'filter_apply', {
            filter_count: Object.keys(e.detail.filters).length,
            total: e.detail.total,
        });
    }
});
</script>