
SEO шаблоны
SEO-шаблон — правило, по которому собираются title, H1, description и текст для подборки с применённым фильтром. Одно правило работает на всех значениях фильтра: SEO-шаблон для ключа made_in даёт заголовок и для «Германии», и для любой другой страны.
Создание SEO-шаблона
mFilter → SEO шаблоны → Добавить.
| Поле | Что задаёт |
|---|---|
| Название | Только для списка в админке |
| Страница | SEO-шаблон действует на этой странице. Пустое поле — глобальный SEO-шаблон, на всех страницах |
| Ключи фильтров | Ключи, при которых SEO-шаблон срабатывает: made_in, color. Вводятся по одному, через Enter. Подсказки берутся из алиасов, ключ без алиасов — например, диапазон цены — впишите вручную |
| Шаблон H1 | Заголовок страницы |
| Шаблон Title | Тег <title> |
| Шаблон Description | Мета-описание |
| Шаблон текста | SEO-текст под выдачей |
| Приоритет | Разрешает ничью между SEO-шаблонами. Меньше — раньше. От 0 до 100. При равном приоритете порядок не гарантирован — задавайте разные |
| Активен | Выключенный SEO-шаблон не участвует в подборе |
Незаполненное поле подобранного SEO-шаблона выводится пустым, а не значением по умолчанию. Заполняйте H1, Title и Description у каждого шаблона.
SEO-шаблон без ключей фильтров сохранить можно, но он не сработает никогда. «SEO-шаблона на всё» не существует: роль запасного варианта играет поведение без SEO-шаблона.
Как подбирается SEO-шаблон
SEO-шаблоны делятся на два вида. С заполненным полем «Страница» шаблон страничный — он срабатывает только на ней. С пустым — глобальный, годится для любой страницы каталога; в списке такие помечены меткой «Глобальный».
Ключи выбранных фильтров сравниваются с полем «Ключи фильтров» в четыре прохода. Первый подошедший SEO-шаблон и применяется:
- SEO-шаблоны этой страницы, у которых набор ключей совпал точно;
- SEO-шаблоны этой страницы, у которых совпал хотя бы один ключ;
- глобальные SEO-шаблоны с точным совпадением;
- глобальные SEO-шаблоны с совпадением хотя бы одного ключа.
Порядок проходов важнее приоритета: страничный SEO-шаблон обойдёт глобальный, а точное совпадение — частичное. Приоритет решает только, какой из SEO-шаблонов взять, когда в одном проходе подошли несколько.
Из-за частичного совпадения SEO-шаблон для made_in применится и к странице, где выбраны made_in и color. Чтобы у такой пары был свой заголовок, заведите SEO-шаблон с обоими ключами и той же привязкой к странице — точное совпадение выиграет у частичного. Глобальный шаблон на пару не обойдёт страничный шаблон для made_in: страничные проходы идут раньше.
Если SEO-шаблон не подобран
Заголовки собираются из названия страницы и выбранных значений:
- H1 — «Название страницы - Значения»;
- Title — то же плюс « | Название сайта»;
- Description — описание страницы плюс те же значения.
Значения подставляются подписями: раздел каталога — пунктом меню, а если он пуст — заголовком, производитель MiniShop3 — именем, числовое значение TV — подписью из индекса.
Плейсхолдеры
Значения фильтров
Значение подставляется по ключу фильтра, без префикса. Именно по ключу, а не по полю, из которого фильтр берёт данные: у фильтра {"brand": {"field": "product_brand"}} в SEO-шаблоне пишется {$brand}. Доступны только ключи из латиницы, цифр и _: ключ с - или | в SEO-шаблоне не адресуется.
| Плейсхолдер | Что подставит |
|---|---|
{$made_in} | Все выбранные значения через запятую |
{$made_in_first} | Только первое значение |
{$made_in_count} | Сколько значений выбрано |
{$vendor_id_raw} | Значение до подстановки названия: у производителей, разделов и TV со списком ресурсов — идентификатор. Нужен для словоформ, заведённых на идентификатор: {$parent_raw.gen}. Диапазон и здесь одной строкой — «1000 — 5000» |
Диапазон приходит одним значением, а не двумя границами: {$price} даст «1000 — 5000», а при одной границе — «от 1000» или «до 5000». Слова и тире берутся из лексикона mfl_seo_range_from, mfl_seo_range_to, mfl_seo_range_separator. Диапазон дат сворачивается так же, без числового форматирования.
Где фильтр хранит идентификатор, подставляется название — то же, что в заголовке без SEO-шаблона, а у TV — подпись значения:
| Фильтр | Что хранится | Что подставится |
|---|---|---|
Производитель MiniShop3, {$vendor_id} | 12 | «Acme» |
| Раздел каталога | Идентификатор раздела | Название раздела |
| Категория товаров | Идентификатор категории | Название категории |
| TV со списком ресурсов | Идентификатор страницы | Название страницы |
| Другой TV, где значение — идентификатор | Идентификатор | Подпись из поля «Возможные значения», например из привязки @SELECT name, id FROM … |
| TV, у значения которого нет подписи | Идентификатор | Сам идентификатор |
Поля страницы и сайта
| Плейсхолдер | Что подставит |
|---|---|
{$pagetitle}, {$longtitle}, {$menutitle}, {$introtext}, {$id}, {$alias}… | Любое поле ресурса под своим именем |
{$<имя TV>} | Значение TV этой страницы — обработанное, с параметрами вывода. Только TV, привязанные к шаблону страницы |
{$site_name} | Название сайта — единственная системная настройка, доступная в SEO-шаблоне |
Запись через точку
SEO-шаблоны, написанные по прежней версии документации, обращались к полям через точку: {$resource.pagetitle}, {$filters.vendor_id}. Такая запись работает и даёт то же, что {$pagetitle} и {$vendor_id}. Уровень вложенности — один: {$filters.vendor_id.gen} останется в заголовке как текст, падеж пишется как {$vendor_id.gen}. {$resource} и {$filters} целиком подставляются пустым местом.
Имена берутся в таком порядке: сначала поля ресурса, затем TV, затем значения фильтров. Последние записываются поверх, поэтому ключ фильтра, совпадающий с именем поля или TV — description, parent, id, — перекроет его.
Числа найденных товаров в SEO-шаблоне нет
Плейсхолдера с числом найденных товаров в SEO-шаблоне нет. Число выставляет сниппет mFilter в плейсхолдер mfilter.total (имя меняется параметром &totalVar), а выводит чанк обёртки — строкой «Найдено: N». В шаблоне ресурса читайте его ниже вызова сниппета через {$_modx->getPlaceholder('mfilter.total')}: через $_pls оно не видно, см. Плейсхолдеры.
Модификаторы
Модификатор пишется через точку после ключа: {$made_in.gen}.
Падежи
| Модификатор | Падеж |
|---|---|
.nom | Именительный |
.gen | Родительный |
.dat | Дательный |
.acc | Винительный |
.ins | Творительный |
.pre | Предложный |
Множественное число — те же имена с суффиксом: .gen_plural, .dat_plural и так далее.
Формы берутся из Словоформ. Если слова там нет, подставится исходное значение — без ошибки и без записи в лог.
У производителей, разделов и категорий товаров склоняется название: словоформа нужна на «Acme», а не на 12. Пакетная генерация таких словоформ не создаст — она берёт слова из значений алиасов, а у этих фильтров там идентификаторы. Заведите их вручную.
Склоняется одно значение. Если выбрано несколько, {$color} — это строка «красный, синий», словоформы для неё нет, и {$color.gen} выведет её в именительном падеже. Для падежа берите первое значение: {$color_first.gen}.
| Запись | Что выведет |
|---|---|
{$color} | «красный, синий» |
{$color.gen} | «красный, синий» — падеж не применился |
{$color_first.gen} | «красного» |
Обработка текста
| Модификатор | Что делает |
|---|---|
.lower, .upper | Нижний и верхний регистр |
.ucfirst, .lcfirst | Первая буква заглавная или строчная |
.strip | Убирает HTML-теги |
.trim | Убирает пробелы по краям |
.escape | Экранирует кавычки и угловые скобки |
Неизвестный модификатор ошибки не вызывает — значение подставится как есть. Поэтому опечатка в {$made_in.gne} выглядит как «падеж не применился».
Условия
Блок {if $ключ}…{/if} выводит содержимое, только когда значение непустое. Пустыми считаются и 0, и "0" — это касается логических фильтров и {$ключ_count}. В условии пишется только имя, без модификатора: {if $color.gen} останется в тексте как есть.
{$pagetitle} {if $color}цвета {$color.gen}{/if}Ветка {else} выводится, когда значение пустое. Она нужна SEO-шаблону, который подобран по частичному совпадению ключей: SEO-шаблон для color и made_in применится и к странице, где выбрана только страна.
{$pagetitle} {if $color}цвета {$color.gen}{else}всех цветов{/if}Внутри блока работают те же плейсхолдеры и модификаторы, блоки можно вкладывать друг в друга. Незакрытый блок или второй {else} в одном блоке выводятся как текст — так ошибку видно в заголовке.
Пробелы
Перед сборкой парсер заменяет подряд идущие пробелы и переносы строк одним пробелом, а края всего текста обрезает. Пробелы внутри ветки остаются как написаны: {$pagetitle}{if $color} — {$color}{/if} даст «Диваны — синий». Разметку многострочного SEO-текста это не портит, но пустые строки между абзацами исчезнут — разделяйте абзацы тегами.
Примеры SEO-шаблонов
Страна производства
Ключи фильтров: made_in
Title: Товары из {$made_in.gen} — купить в {$site_name}
H1: Товары из {$made_in.gen}
Description: Товары из {$made_in.gen} в наличии. Доставка по всей России.Страна и цвет
Ключи фильтров: made_in, color
Title: Товары из {$made_in.gen} {$color.gen} цвета | {$site_name}
H1: Товары из {$made_in.gen}, {$color.gen} цветаДиапазон цен
Ключи фильтров: price
Title: {$pagetitle} {$price} рублей
H1: {$pagetitle}, {$price}Индексация
Поля «не индексировать» у SEO-шаблона нет: страницы закрываются системными настройками.
| Настройка | По умолчанию | Что делает |
|---|---|---|
mfilter.seo_noindex_filtered | Нет | Закрывает все отфильтрованные страницы разом |
mfilter.seo_max_filters | 2 | Закрывает страницу, где выбрано больше указанного числа фильтров. 0 — без ограничения |
mfilter.seo_max_values | 1 | Закрывает страницу, где у одного фильтра выбрано больше указанного числа значений. 0 — без ограничения |
Диапазон считается одним фильтром, хотя в адрес попадает двумя границами. Значения же считаются внутри каждого ключа отдельно, а у границы значение одно — поэтому seo_max_values диапазон не закрывает даже при лимите в единицу.
Индивидуальная запись снимает закрытие: страница, для которой её написали, индексируется, даже если попала под лимиты. Настройка mfilter.seo_noindex_filtered сильнее — она закрывает страницу и при заполненной записи.
Canonical
Адрес вычисляется, в SEO-шаблоне его не задают.
- Открытая для индексации страница получает canonical на саму себя — отфильтрованный адрес без номера страницы, сортировки и лимита;
- закрытая — canonical на страницу без фильтров.
Адрес всегда абсолютный.
Программный доступ
$mfilter = $modx->services->get('mfilter');
$seoBuilder = $mfilter->getSeoBuilder();
// Значения настоящие: у производителя — id; диапазон — двумя ключами
$filters = ['made_in' => ['Германия'], 'price|min' => ['1000'], 'price|max' => ['5000']];
$seoData = $seoBuilder->build($resourceId, $filters);
// [
// 'h1' => 'Товары из Германии',
// 'title' => 'Товары из Германии — купить в Название сайта',
// 'description' => 'Товары из Германии в наличии...',
// 'text' => '<p>SEO текст...</p>',
// 'canonical' => 'https://site.ru/catalog/made_in--germaniya/',
// 'noindex' => false
// ]Для несуществующего ресурса build() вернёт пустой массив.
| Метод | Что делает |
|---|---|
shouldNoindex(array $filters): bool | Попадает ли набор фильтров под лимиты индексации |
getCanonical(int $resourceId, array $filters): string | Canonical для набора фильтров |
clearCache(): void | Сбрасывает кэш загруженных SEO-шаблонов |
Вывод на фронтенде
Плейсхолдеры mfilter.seo.* ставит сниппет mFilter — всегда, а без фильтров в них заголовок, описание и адрес самого ресурса. Читайте их в шаблоне ресурса ниже вызова сниппета через $_modx->getPlaceholder(): через $_pls видны только значения, которые плагин считает заранее на SEO-адресе. Пример для <head> и <body>, где вызов mFilter стоит в начале шаблона, — в Плейсхолдерах.
Маркеры для AJAX
Атрибуты data-mfilter-h1 и data-mfilter-seo-text (или классы h1.mfilter-h1 и .mfilter-seo-text) обязательны: без них JS не найдёт элементы при AJAX-фильтрации, и они обновятся только после F5. Класс mfilter-h1 срабатывает только на теге <h1> — на другом теге нужен атрибут. Меты title, description, canonical, robots в <head> JS обновляет по стандартным селекторам — маркеры не нужны.
Если страница открыта для индексации, скрипт после фильтрации переписывает любой <meta name="robots"> с noindex на index, follow — и выставленный вручную тоже. Учитывайте это, если закрываете раздел своим robots.
После обновления JS вызывает событие mfilter:seoUpdate — его можно слушать, если на странице есть свои элементы с SEO-данными:
document.addEventListener('mfilter:seoUpdate', (e) => {
console.log(e.detail.seoData.h1, e.detail.seoData.title);
});Когда SEO-шаблон не нужен
SEO-шаблон описывает правило для всех значений сразу. Если текст нужен под одну конкретную подборку — свой заголовок и описание для определённого бренда, — правило заводить не обязательно: такая страница настраивается поштучно на вкладке Индивидуальные SEO.
Одно другому не мешает. Заполненные поля индивидуальной записи перекрывают SEO-шаблон, незаполненные берутся из него: правило закрывает массу страниц, а руками пишется то, ради чего стоит сесть за текст.

