Fenom на практике: как вывести карточки с TV, разбить список на страницы, разобрать MIGX и подготовить изображения. Разбираем 24 примера для повседневной работы с шаблонами.
При работе с MODX одни и те же задачи повторяются: собрать каталог, добавить обложку к статье, показать вопросы и ответы или найти ближайший старт курса. Обычно для этого уже есть всё необходимое — ресурсы, TV, чанки и сниппеты. В шаблоне остаётся связать данные с разметкой. Ниже разберём, как это сделать на Fenom.
Примеры рассчитаны на тех, кто уже работает с шаблонами MODX. Имена TV и чанков в примерах можно заменить своими. Перед использованием создайте нужные поля и чанки, привяжите TV к шаблонам и проверьте дополнения. ID ресурсов возьмите из своего сайта: числа из чужого проекта здесь не помогут.
Где работает Fenom и откуда приходят данные
Fenom — шаблонизатор, а не отдельная CMS. В рассматриваемых проектах он используется вместе с pdoTools. Синтаксис {$_modx->resource.pagetitle} работает там, где содержимое проходит через Fenom. Если фигурные скобки выводятся обычным текстом, проверьте парсер и настройки.
У pdoTools есть настройки обработки чанков и шаблонов: pdotools_fenom_default и pdotools_fenom_parser. Их поведение зависит от установленной версии; сверяйте его с документацией парсера. Не включайте доступ к произвольным PHP-функциям только ради примера: подготовку данных можно вынести в сниппет.
Частая причина одинаковых заголовков у всех карточек — обращение к открытой странице вместо строки выборки. $_modx->resource содержит данные текущей страницы; $pagetitle и $id внутри карточки — данные выбранного ресурса; $item.title в цикле — поле очередного элемента массива. Выбирайте источник по тому, что сейчас выводите.
Поля, условия и запасные значения
1. Заголовок с запасным значением
Для заголовка часто нужен простой запасной вариант: если редактор заполнил longtitle, берём его, иначе используем pagetitle. Запишем это условие:
{set $title = $_modx->resource.longtitle ?: $_modx->resource.pagetitle}
<h1>{$title | escape}</h1>
Оператор ?: выбирает запасное значение, если левое пусто. Он удобен для текста, но не для чисел, где ноль — допустимое значение. У H1 и HTML-тега title могут быть разные редакционные правила: не обязательно связывать их одним полем.
2. Условие вокруг необязательного блока
Если подзаголовок не заполнен, вместе с ним лучше убрать и контейнер. Иначе текст исчезнет, а отступ или рамка останутся. Проверим поле до открытия контейнера:
{if $_modx->resource.subtitle?}
<div class="wm-intro">
<p>{$_modx->resource.subtitle | escape}</p>
</div>
{/if}
При пустом поле не останется рамки или лишнего отступа. Проверка ? отвечает на вопрос о непустом значении. Она не проверяет существование файла, правильность URL или допустимость введённого текста.
3. Задана ли переменная
Для числовых значений бывает важно сохранить ноль. Проверка существования отличается от проверки непустоты:
{if $price is set}
<p>Стоимость: {$price | escape} ₽</p>
{/if}
is set проверяет, задана ли переменная. При этом она может содержать пустую строку: корректность цены проверяется при подготовке данных. О различии условий подробнее рассказано в статье о проверке переменных.
4. Текст и готовая разметка
Обычный текст и содержимое визуального редактора нельзя выводить одинаково. Для текстового описания используем экранирование, а редакторский HTML выводим как разметку:
<p>{$_modx->resource.description | escape}</p>
<div class="wm-text">{$_modx->resource.text1}</div>
Описание — обычный текст, поэтому экранируется. TV text1 здесь хранит контролируемый редакторский HTML: экранирование превратило бы теги в видимые символы. Пользовательский ввод сначала очищают на сервере. text1 нужно создать в вашем проекте — это не стандартное поле MODX.
Наследование шаблонов и параметры чанков
5. Общий каркас и дочерний шаблон
Создайте базовый шаблон wm-base: в нём будут общие шапка, подвал и место для содержимого страницы.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>{$_modx->resource.pagetitle | escape}</title>
</head>
<body>
{include 'wm-header'}
<main>{block 'main'}{/block}</main>
{include 'wm-footer'}
</body>
</html>
Создайте чанки wm-header и wm-footer. Это минимальный каркас: стили, скрипты и полные метаданные подключаются в основе вашего сайта. Дочерний шаблон заменяет содержимое именованного блока:
{extends 'template:wm-base'}
{block 'main'}
<h1>{$_modx->resource.pagetitle | escape}</h1>
<div class="wm-text">{$_modx->resource.content}</div>
{/block}
Префикс template: сообщает, что родитель — шаблон MODX. Имена блока в основе и потомке должны совпадать. Общую шапку меняют в одном месте, не обходя каждый шаблон страницы.
6. Передать параметры в чанк
Карточка может понадобиться на главной, в подборке и в конце статьи. Вынесите разметку в чанк, а значения передавайте при вызове:
{include 'wm-card'
title=$_modx->resource.pagetitle
description=$_modx->resource.description
link=$_modx->makeUrl($_modx->resource.id)
}
Чанк wm-card работает с параметрами, не привязываясь к конкретной странице:
<article class="wm-card">
<h3><a href="{$link | escape}">{$title | escape}</a></h3>
{if $description?}<p>{$description | escape}</p>{/if}
</article>
Обратите внимание на имена переменных: здесь мы сами задаём параметры чанка, а в карточку pdoResources приходят поля строки выборки. Поэтому один вызов нельзя заменить другим без проверки чанка.
Списки ресурсов и карточки с TV
7. Дочерние страницы через pdoResources
Начнём с небольшого списка: выведем дочерние страницы текущего раздела. Для этого вызовем pdoResources:
{'pdoResources' | snippet : [
'parents' => $_modx->resource.id,
'depth' => 0,
'tpl' => 'wm-resource-card',
'limit' => 12,
'sortby' => 'menuindex',
'sortdir' => 'ASC'
]}
parents задаёт раздел, depth — глубину обхода, tpl — чанк одной карточки. limit здесь ограничивает весь блок двенадцатью записями. Это ещё не постраничный каталог: для большого раздела нужна пагинация. Чанк карточки создадим в следующем примере.
8. Подключить TV и согласовать имена
Для обложки и подзаголовка добавьте TV к выборке. Эти поля должны быть привязаны к шаблонам дочерних ресурсов:
{'pdoResources' | snippet : [
'parents' => $_modx->resource.id,
'depth' => 0,
'tpl' => 'wm-resource-card',
'includeTVs' => 'preview_img,subtitle',
'prepareTVs' => 1,
'tvPrefix' => '',
'limit' => 12,
'sortby' => 'menuindex',
'sortdir' => 'ASC'
]}
Создайте чанк wm-resource-card. В нём используются данные строки, а не открытого раздела:
<article class="wm-card">
{if $preview_img?}
<img src="{$preview_img | escape}" alt="" loading="lazy">
{/if}
{set $title = $menutitle ?: $pagetitle}
<h3><a href="{$_modx->makeUrl($id) | escape}">{$title | escape}</a></h3>
{if $subtitle?}<p>{$subtitle | escape}</p>{/if}
</article>
includeTVs добавляет поля в выборку. prepareTVs подготавливает значения с учётом типа TV; это не универсальный декодер MIGX. processTVs запускает обработку вывода TV и требуется только когда она нужна. Пустой tvPrefix даёт переменную $preview_img. Если префикс tv. сохранён, используется $_pls['tv.preview_img']. Согласуйте вызов с чанком. Контракт параметров описан в документации pdoResources.
9. Получить массив вместо HTML
Иногда одного чанка карточки мало: например, первый элемент нужно оформить иначе или между группами вставить заголовки. Получим ресурсы массивом и переберём их в шаблоне:
{set $items = 'pdoResources' | snippet : [
'parents' => $_modx->resource.id,
'depth' => 0,
'return' => 'data',
'limit' => 12,
'sortby' => 'menuindex',
'sortdir' => 'ASC'
]}
{if $items is array}
<ul>
{foreach $items as $item}
<li><a href="{$_modx->makeUrl($item.id) | escape}">
{$item.pagetitle | escape}
</a></li>
{/foreach}
</ul>
{/if}
return => 'data' возвращает массив вместо готовой разметки. Его можно перебрать, но нельзя просто вывести как HTML. Для одинаковых карточек обычно достаточно tpl. Если записей нет и пустой ul не нужен, проверяйте количество элементов до открытия списка.
Пагинация и связанные материалы
10. Список с pdoPage
Выведем статьи по 16 записей на страницу:
{'!pdoPage' | snippet : [
'element' => 'pdoResources',
'parents' => $_modx->resource.id,
'depth' => 0,
'tpl' => 'wm-resource-card',
'includeTVs' => 'preview_img,subtitle',
'prepareTVs' => 1,
'tvPrefix' => '',
'limit' => 16,
'pageVarKey' => 'page',
'pageNavVar' => 'page.nav',
'sortby' => 'publishedon',
'sortdir' => 'DESC'
]}
{'page.nav' | placeholder}
pdoPage оборачивает выборку и создаёт навигацию. Плейсхолдер читается после вызова. Проверьте первую, вторую и последнюю страницы. Для двух независимых списков задайте разные pageVarKey и имена служебных плейсхолдеров. Их назначение смотрите в документации pdoPage.
Знак ! задаёт некэшируемый MODX-вызов — это важно для результата, зависящего от запроса. Внутренний кэш дополнения — отдельный механизм: знак не отменяет все уровни кэширования. Если список состоит из разных типов ресурсов, добавьте фильтр по вашему шаблону.
11. Похожие материалы без текущей страницы
В конце статьи можно показать другие материалы того же раздела. Текущую статью исключим из подборки:
{set $related = 'pdoResources' | snippet : [
'parents' => $_modx->resource.parent,
'depth' => 0,
'where' => ['id:!=' => $_modx->resource.id],
'tpl' => 'wm-resource-card',
'limit' => 6,
'sortby' => 'menuindex',
'sortdir' => 'ASC'
]}
{if $related?}
<section>
<h2>Ещё в этом разделе</h2>
{$related}
</section>
{/if}
Весь блок исчезает, если соседних материалов нет. id:!= исключает текущий ресурс. Если в разделе мало материалов, можно отдельным условием расширить выборку до родительского раздела. Такой переход имеет смысл только при понятной структуре каталога.
Ссылки, настройки и хлебные крошки
12. URL ресурса вместо ручной сборки
Адрес ресурса можно получить через makeUrl или модификатор url. Метод строит адрес по ID:
<a href="{$_modx->makeUrl($_modx->resource.parent) | escape}">
Вернуться в раздел
</a>
{set $absoluteUrl = $_modx->makeUrl($_modx->resource.id, '', '', 'full')}
Вариант full нужен для абсолютного адреса. На странице верхнего уровня проверьте, куда должна вести ссылка возврата: её родитель может быть равен нулю. Канонический URL фильтра или постраничного списка требует отдельной логики, а не только этого вызова.
13. Общие контакты в ресурсе настроек
Общие контакты можно хранить в отдельном ресурсе настроек. $settingsId — ID вашей страницы настроек, переданный из основы:
{set $phone = $settingsId | resource : 'phone1'}
{if $phone?}
<p>Телефон: {$phone | escape}</p>
{/if}
Поле меняют один раз и используют в шапке, подвале и контактах. Не путайте его с системной настройкой: $_modx->config.site_url приходит из конфигурации, phone1 здесь — TV другого ресурса. Для tel: подготовьте нормализованный номер отдельно от оформленного текста.
14. Хлебные крошки из структуры
Хлебные крошки не нужно собирать вручную: pdoCrumbs построит цепочку по родителям ресурса. Вот вариант с простой разметкой:
<nav aria-label="Хлебные крошки">
{'pdoCrumbs' | snippet : [
'showHome' => 1,
'showCurrent' => 1,
'tplWrapper' => '@INLINE <ol>{$output}</ol>',
'tplHome' => '@INLINE <li><a href="{$link | escape}">Главная</a></li>',
'tpl' => '@INLINE <li><a href="{$link | escape}">{$pagetitle | escape}</a></li>',
'tplCurrent' => '@INLINE <li aria-current="page">{$pagetitle | escape}</li>'
]}
</nav>
Последний пункт показывает текущую страницу без ссылки на саму себя. Если цепочка неверная, проверьте родителей ресурсов: крошки отражают структуру MODX. Длинные @INLINE-шаблоны удобнее вынести в именованные чанки.
MIGX: JSON, массив и рендер через сниппет
MIGX позволяет редактировать повторяемые строки: преимущества, вопросы, слайды, расписание. Для примера вопросов настройте TV faq с текстовыми полями question и answer. Его значение — JSON-массив:
[{"question":"Как заказать?","answer":"Оставьте заявку."}]
15. Разобрать JSON и вывести цикл
JSON можно разобрать прямо в шаблоне. Перед циклом проверим, что получили массив:
{set $faqs = json_decode($_modx->resource.faq, true)}
{if $faqs is array}
{foreach $faqs as $faq}
<details>
<summary>{$faq.question | escape}</summary>
<p>{$faq.answer | escape}</p>
</details>
{/foreach}
{/if}
Второй аргумент true возвращает ассоциативный массив. Пример предполагает доступность json_decode в вашей конфигурации Fenom. Если функция недоступна, декодируйте данные в сниппете. Некорректный JSON не должен попасть в цикл. Ответ здесь — простой текст; редакторский HTML требует другого контракта.
16. MIGX через getImageList
Установите MIGX через управление пакетами MODX. В его состав входит getImageList: несмотря на название, он выводит не только изображения, но и вопросы, преимущества и другие строки. Создайте TV faq с полями question и answer, привяжите его к шаблону и заполните. Вызов для текущей страницы:
{'getImageList' | snippet : [
'tvname' => 'faq',
'tpl' => 'wm-faq-item',
'limit' => 0
]}
tvname — имя MIGX-поля, tpl — чанк одной строки. Чанк создадим ниже. Нулевой limit выводит все строки; для большого набора задайте ограничение. Чтобы получить строки другого ресурса, передайте его ID в docid.
17. Чанк строки для getImageList
Создайте чанк wm-faq-item. Для переносимого примера используем обычные плейсхолдеры MODX: их обработка не требует включения Fenom внутри чанка.
<details>
<summary>[[+question:htmlent]]</summary>
<p>[[+answer:htmlent]]</p>
</details>
Названия плейсхолдеров совпадают с полями MIGX. Фильтр htmlent экранирует текст для HTML. Здесь ответы содержат обычный текст, а не разметку визуального редактора. Сам вызов сниппета остаётся в Fenom-шаблоне, а чанк строки обрабатывается парсером MODX.
{'getImageList' | snippet : [
'tvname' => 'faq',
'tpl' => 'wm-faq-item',
'limit' => 3
]}
В этом вызове выводятся первые три строки. Остальные остаются в TV и появятся при увеличении или снятии лимита.
18. Не добавлять базовый путь дважды
В MIGX может храниться только имя файла, без пути к медиатеке. В этом случае к имени добавляют базовый URL. При прямом разборе JSON это выглядит так:
{set $imageUrl = $imagesBaseUrl ~ $item.image}
<img src="{$imageUrl | escape}" alt="{$item.title | escape}" loading="lazy">
$imagesBaseUrl задаёт ваш шаблон: это URL базы, а не путь на диске. Фрагмент подходит, если $item.image — относительное имя без базы. Пользовательский сниппет может уже добавлять базу медиатеки или отдельно обрабатывать абсолютные адреса. Сначала проверьте результат, чтобы не получить assets/images/assets/images/photo.jpg.
Изображения и адаптивные размеры
19. Производное изображение через pThumb
Нужен установленный pThumb. Для обложки с пропорциями 4:3:
{if $_modx->resource.preview_img?}
{set $src = $_modx->resource.preview_img | pthumb : 'w=800&h=600&zc=1&q=80'}
<img src="{$src | escape}" alt=""
width="800" height="600" loading="lazy">
{/if}
zc=1 разрешает обрезку. Для схем и произведений, у которых важны края, выберите сохранение кадра. Пустой alt подходит декоративной обложке рядом с заголовком; содержательное изображение требует описания. loading="lazy" используйте ниже первого экрана. Подробнее — в статье о TV-изображениях.
20. Две ширины через pThumb
Подготовим две версии изображения через установленный pThumb. Браузер выберет подходящую из srcset:
{if $image?}
{set $small = $image | pthumb : 'w=640&q=80'}
{set $large = $image | pthumb : 'w=1280&q=80'}
<img src="{$small | escape}"
srcset="{$small | escape} 640w, {$large | escape} 1280w"
sizes="(max-width: 680px) 100vw, 640px"
alt="{$title | escape}" loading="lazy">
{/if}
$image и $title передаются в чанк. sizes описывает ширину картинки в макете. Здесь исходное изображение должно быть не меньше 1280 пикселей по ширине: дескрипторы 640w и 1280w должны совпадать с фактическими ширинами файлов. Проверьте результат обработки и добавьте width и height по известным пропорциям.
Расписание и формы числительных
21. Ближайшая будущая строка расписания
Чтобы показать ближайший старт курса, перебираем расписание, пропускаем прошедшие даты и останавливаемся на первой будущей. В нашем примере дата хранится в числовом поле start_ts как Unix-время:
{set $next = []}
{foreach $schedule as $row}
{if $row.start_ts < $now}
{continue}
{/if}
{set $next = $row}
{break}
{/foreach}
{if $next?}
<p>Ближайший старт: {$next.start_label | escape}</p>
{else}
<p>Дату следующего старта уточните у организатора.</p>
{/if}
Массив $schedule, Unix-время $now и строку start_label готовит сниппет. Это адаптация алгоритма, не вызов готового дополнения. Строки должны быть заранее отсортированы по дате, иначе первая будущая запись не обязательно ближайшая. Правила часового пояса и границы даты должны быть общими для всей системы записи.
22. Три формы русского числительного
Для правильной подписи количества нужны последние две цифры числа. Входное $count — целое неотрицательное количество:
{set $n100 = $count % 100}
{set $n10 = $count % 10}
{if $n10 == 1 && $n100 != 11}
{set $word = 'работа'}
{elseif $n10 >= 2 && $n10 <= 4 && ($n100 < 10 || $n100 >= 20)}
{set $word = 'работы'}
{else}
{set $word = 'работ'}
{/if}
<p>{$count} {$word}</p>
Проверьте 0, 1, 2, 5, 11, 14, 21, 22 и 25. Не загружайте весь большой каталог только ради count. У пагинированной выборки используйте общий счётчик, у отдельной статистики — соответствующий запрос в сниппете.
23. Показать секцию после получения HTML
Сначала получите строки, затем решите, нужен ли весь блок. Это избавит страницу от пустого заголовка «Вопросы и ответы»:
{set $rows = 'getImageList' | snippet : [
'tvname' => 'faq',
'tpl' => 'wm-faq-item',
'limit' => 0
]}
{if $rows?}
<section aria-labelledby="faq-title">
<h2 id="faq-title">Частые вопросы</h2>
{$rows}
</section>
{/if}
Само присваивание результата не показывает: важно вывести $rows внутри секции. Здесь строка уже содержит HTML чанка, поэтому её целиком не экранируют. Текстовые поля экранируются в чанке строки. При нескольких FAQ-блоках на странице используйте уникальные ID заголовков.
Как проверять код и что не стоит копировать
24. Проверить контракт данных
Если список не выводится, начните с результата сниппета: он вообще вернул массив? Для проверки добавьте во временный тестовый шаблон небольшой вызов:
{set $items = 'pdoResources' | snippet : [
'parents' => $_modx->resource.id,
'depth' => 0,
'return' => 'data',
'limit' => 3
]}
{if $items is array}
<p>Записей в пробной выборке: {$items | count}</p>
{else}
<p>Проверьте результат выборки и настройки парсера.</p>
{/if}
Лимит в три записи нужен только для проверки. Полученное количество не равно числу всех дочерних ресурсов. Этот фрагмент показывает тип результата, не выводя содержимое базы; после проверки уберите его из шаблона.
При переносе примера посмотрите, сколько данных он обработает. limit => 0 подходит для нескольких категорий, но большой каталог целиком займёт память. Запрос внутри foreach выполнится для каждой карточки. Для каждого дополнения проверьте параметры по документации установленной версии.
- Проверьте назначенный шаблон и обработку Fenom.
- Проверьте привязку TV, заполненность и ожидаемый формат.
- Разделите поля открытой страницы и строки карточки; согласуйте tvPrefix.
- Проверьте имена чанков, наличие сниппетов и параметры вашей версии.
- Проверьте пустые поля, отсутствие строк и количество выше лимита.
- Откройте ссылки и производные изображения: пути не должны дублировать базу медиатеки.
- Для пагинации проверьте вторую страницу, для расписания — границу даты, для числительных — 11–14.
- После правок проверьте кэш, журнал MODX, консоль и сетевые ошибки браузера.
Для первого применения возьмите один раздел своего сайта: подключите карточку с TV и выведите её через pdoResources. Проверьте заполненную карточку и карточку без обложки, а затем пустой раздел. Если записей больше, чем помещается на странице, добавьте pdoPage и перейдите на вторую страницу. Так вы проверите весь сценарий, прежде чем переносить его в остальные разделы.
Документация и дальнейшее чтение
API сверяется с документацией: парсер pdoTools, pdoResources, pdoPage, Fenom. Для вывода MIGX см. документацию getImageList. Основы структуры объяснены отдельно в статье о шаблонах, чанках и TV.