MODX Revolution - платформа, которую часто незаслуженно обходят стороной. Все рвутся на WordPress с WooCommerce или собирают тяжёлые конструкции на OpenCart. А зря. MODX дает уникальную свободу: вы не привязаны к структуре “категория -> товар”. Вы строите логику сами. Мы пройдем путь от установки «движка» до настройки SEO-результатов, добавим оформление заказов, UI-советы и склептим полноценный магазин.
MiniShop3 против устаревших решений
Забудьте про Shopkeeper. Этот сниппет был хорош для MODX Evolution, но на Revo он превращает сайт в тыкву. Он не дружит с pdoTools, а его архитектура для корзины вызывает когнитивный диссонанс у современного разработчика. Ваш выбор - MiniShop3 (или MiniShop2 на старых проектах).
Почему именно он? MiniShop3 использует pdoTools для выборки данных. Это значит, что скорость работы каталога зависит не от кол-ва товаров, а от того, как вы настроили кэширование. Плагин работает через сниппеты: msProducts для вывода списка, msProduct для карточки, msCart для корзины и msOrder для оформления. Вся логика разбита на понятные куски, которые можно переопределять через чанки.
Установка через стандартный Package Manager. Качаете pdoTools (база всего), затем сам MiniShop3. Не игнорируйте требования к PHP - версия 8.1 и выше. Если ваш хостинг тянет 7.4, пора писать письмо техподдержке или менять провайдера. После установки в дереве ресурсов появится новый тип документа - «Продукт». Сразу идите в настройки компонента и укажите ID страниц: корзины, оформления, личного кабинета. Если этого не сделать, корзина будет редиректить на главную.
Настройка ЧПУ и pdoTools: Фундамент урлов
MODX без ЧПУ просто «index.php?id=123». Идем в Системные настройки. Ищем friendly_urls - включаем. friendly_alias_path - включаем. use_alias_path - включаем. Теперь у нас есть ЧПУ вида /catalog/sektor-gaza-benzopila. Для транслитерации кириллицы в латиницу поставьте дополнение Translit - иначе будут кракозябры.
Для вывода товаров мы используем вызов [[!pdoPage? &element=`msProducts` ...]]. Восклицательный знак (!) критически важен на этапе разработки - он отключает кэш сниппета, чтобы вы сразу видели правки. В боевом режиме, когда всё настроено, кэширование лучше включить обратно. Старайтесь избегать вложенных вызовов некешированных сниппетов убивает производительность. Если у вас меню вызывается через Wayfinder без кэша, а внутри него лежит счетчик корзины, сервер будет пересобирать это при каждом клике.
Структура каталога строится через ресурсы. Создаете родительскую страницу «Каталог», а внутри - дочерние ресурсы типа «Категория». Товары вешаете на категории. Не плодите уровень вложенности глубже 3-х уровней (Каталог -> Подкатегория -> Товар). Гугл любит короткие урлы.
Сниппеты и чанки: Рубикон разделения логики
Главная ошибка новичков - валить весь код в один шаблон. Шаблон (Template) должен содержать только каркас: <html><head><body>.... Внутри тела вызываем чанки через [[$header]], [[$footer]]. А вывод товаров - через вызов сниппета, который обращается к чанку-обертке.
Сниппет msProducts отправляет запрос в БД, получает массив товаров и для каждого товара применяет чанк, указанный в параметре &tpl. Пример вызова для каталога:
[[!pdoPage?
&element=`msProducts`
&parents=`2`
&limit=`12`
&tpl=`tpl.Product`
&includeThumbs=`small`
&sortby=`{"menuindex":"ASC", "publishedon":"DESC"}`
&ajaxMode=`default`
]]
А вот так выглядит чанк tpl.Product (Fenom синтаксис):
<div class="product-card">
<a href="[[~[[+id]]]]" class="product-link">
<img src="[[+tv.product_image:phpthumbof=`w=300&h=300&zc=1`]]" alt="[[+pagetitle]]" loading="lazy">
<h3>[[+pagetitle]]</h3>
<div class="price">[[+price]] ₽</div>
</a>
<button class="add-to-cart" data-id="[[+id]]">В корзину</button>
</div>
Обратите внимание на phpthumbof - мы не храним миллион копий изображений, а генерируем превью на лету (желательно с кэшированием). [[~[[+id]]]] - стандартный MODX тег для генерации ссылки на ресурс с учетом ЧПУ.
Картинки-заглушки: Пока нет товаров - покажите хоть что-то
Когда вы только настраиваете каталог, товаров нет, а дизайн уже верстается. В чанке tpl.Product для вывода изображения используйте тернарный оператор. Если [[+tv.product_image]] пуст - подставляйте путь к заглушке.
<img src="[[+tv.product_image:default=`/assets/images/no-image.png`]]" alt="[[+pagetitle]]">
Для ms2Gallery, если галерея пуста, проверяйте [[+ms2g.0.url]]. Удобно создать отдельную системную настройку site_default_image и вызывать её через [[++site_default_image]].
<img src="[[+ms2g.0.url:default=`[[++site_default_image]]`]]" alt="[[+pagetitle]]">
- Заглушка должна быть визуально нейтральной - серый фон, силуэт товара или коробка, вот пример идеальной фото заглушки. Не ставьте туда надпись «Нет фото» вызывает недоверие. Лучше просто иконку. Размер заглушки должен соответствовать пропорциям реальных изображений (обычно 1:1), чтобы верстка не плыла.
- Для галереи товара используйте системную настройку
ms2gallery_placeholders_tpl. Укажите имя чанка, который рендерит каждое изображение. Внутри чанка тоже проверяйте: если[[+url]]пуст - не выводите ничего, либо показывайте заглушку. Это чище, чем городить условия в самом шаблоне ресурса. - Заглушки пригодятся и в корзине. Если товар удален, а сессия еще помнит его ID - в
msCartможет прилететь пустая картинка. В чанке корзиныtpl.msCart.rowтоже проверяйтеimageчерезdefault. Итог: ни одной битой ссылки на изображение на всем сайте. Это и SEO-метрики не роняет, и пользователя не раздражает.

TV-параметры: Характеристики и фильтрация
В MODX есть Template Variables (TV). Вы создаете поле «Производитель», «Вес», «Цвет». В карточке товара выводите [[+tv.proizvoditel]]. Это основа. Но для интернет-магазина этого мало - нужно, чтобы по этим полям можно было фильтровать каталог.
Стандартный msProducts с фильтрацией справляется плохо. Тут на сцену выходит mFilter2 (часть экосистемы pdoTools). Этот сниппет строит форму фильтра на основе TV и полей ресурсов. Он анализирует текущую выборку и выводит только те значения фильтров, которые реально есть в наличии.
Настройка фильтрации требует указания в параметрах &filters= . Вы пишите: &filters={"tv:proizvoditel":"like","tv:ves":"number"}`. mFilter2 перехватывает AJAX-запросы и подгружает товары без перезагрузки страницы. Важно: для TV-полей, по которым планируете фильтровать, в настройках самого TV поставьте галочку «Использовать в поиске» и укажите тип индекса. Без этого запросы будут тупить.
Заставляем msOrder работать правильно
Страница оформления - самое слабое место в любом самодельном магазине. Тут теряются клиенты, валится сессия и происходят чудеса с суммой заказа. MiniShop3 предоставляет сниппет msOrder, который берет на себя логику. Вы вызываете его на отдельной странице (например, /checkout) с обязательным восклицательным знаком: {'!msOrder' | snippet : ['tpl' => 'tpl.msOrder']} .
Вызов должен быть строго некэшированным. Если вы забудете восклицательный знак или случайно закэшируете страницу через pdoTools, пользователь увидит пустую корзину или чужие данные. Плейсхолдер $isCartEmpty в чанке формы позволяет показать сообщение «Корзина пуста» и ссылку на каталог, пока заказ не оформлен.
В чанке tpl.msOrder доступны массивы: $deliveries (способы доставки), $payments (способы оплаты), $form (значения полей формы), $order (стоимость товаров и доставки). Важный нюанс - каждый способ доставки содержит массив payments с ID доступных оплат. JavaScript на фронте автоматически фильтрует вторую группу при смене первой. Не пытайтесь захардкодить эту связь - используйте готовые события.
В системных настройках MiniShop3 обязательно укажите ID страниц через параметры ms3_cart_page_id, ms3_order_page_id, ms3_order_success_page_id. Если этого не сделать, после оформления пользователя кинет на главную без всякого «спасибо». Страница успеха должна содержать вызов msGetOrder, который вытаскивает детали только что созданного заказа из сессии.
Обязательные поля. Для каждого способа доставки в админке можно настроить правила валидации в JSON-формате. Например, для курьерской доставки требуйте полный адрес, а для самовывоза - только телефон. Ключи валидации совпадают с атрибутами name полей формы. Если поле обязательное, добавьте "email": "required|email". Для чекбокса согласия - "agree": "accepted".
Пример правил для доставки «Курьер»:
{
"first_name": "required",
"last_name": "required",
"phone": "required|regex:/^\\+?[0-9]{10,12}$/",
"email": "required|email",
"city": "required",
"street": "required",
"agree": "accepted"
}
Валидация работает в два этапа. Сначала при каждом изменении поля через ms3.order.setField сервер проверяет его изолированно. Затем при отправке формы - весь комплект. Ошибки автоматически подставляются в контейнеры .invalid-feedback рядом с полями. Добавляйте эти блоки в чанк заранее, иначе пользователь не увидит, что пошло не так.
Кастомные поля. Если вам нужно сохранить поле, которого нет в стандартной модели msOrder (например, «Номер пропуска» или «Предпочитаемое время»), используйте плагин на событие msOnBeforeCreateOrder. Заберите значение из $_POST или из свойств адреса и запишите в properties заказа. Поле при этом должно быть объявлено в правилах валидации доставки, иначе сервер его проигнорирует.
Пример кода для сохранения кастомного поля:
switch ($modx->event->name) {
case 'msOnBeforeCreateOrder':
$address = $msOrder->Address;
if ($address) {
$properties = $msOrder->get('properties') ?: [];
$properties['pass_number'] = $address->get('properties')['pass_number'] ?? '';
$msOrder->set('properties', $properties);
}
break;
}
JavaScript API: Управляем оформлением без перезагрузки
MiniShop3 предоставляет глобальный объект ms3.order с методами для управления формой. ms3.order.setDelivery(id) и ms3.order.setPayment(id) обновляют способы доставки и оплаты, автоматически пересчитывая стоимость. ms3.order.setField(key, value) валидирует и сохраняет значение поля.
События - ваш главный инструмент для расширения функционала. Подписывайтесь на ms3:order:before-submit, чтобы провести дополнительную проверку (например, согласие с офертой через кастомный чекбокс). Если проверка не пройдена - вызовите e.preventDefault().
Событие ms3:order:success срабатывает после создания заказа. В e.detail лежит order_id и redirect - URL страницы «Спасибо». Вы можете перехватить редирект и, скажем, отправить данные в CRM перед переходом.
document.addEventListener('ms3:order:success', (e) => {
// Отправляем заказ в Google Analytics или CRM
gtag('event', 'purchase', {
transaction_id: e.detail.order_id,
value: e.detail.order_cost
});
// Стандартный редирект всё равно сработает
});
При ошибке валидации срабатывает ms3:order:error, и в e.detail.errors прилетает объект с проблемными полями. Используйте его для кастомного отображения ошибок, если стандартной подсветки недостаточно.
UI-советы: Не отпугивайте клиента на последнем шаге
Прогресс-бар. Никто не любит длинные формы. Разбейте оформление на шаги: «Корзина» → «Данные» → «Доставка/Оплата» → «Подтверждение». Но не заставляйте пользователя жать «Далее» пять раз - все поля лучше разместить на одной странице, визуально сгруппировав в блоки. Адаптивная сетка Bootstrap 5, которую использует MiniShop3 по умолчанию, сама раскладывает блоки в две колонки на десктопе и в одну на мобильных.
Микротекст и подсказки. Каждое обязательное поле должно иметь звездочку. Рядом с полем «Телефон» укажите формат - «+7 999 123-45-67». Пустые догадки пользователя - главная причина брошенных корзин. Поле «Индекс» лучше сделать необязательным или добавить подсказку «Для ускорения доставки». Курьерские службы и так определят город по адресу.
Прятки. Не показывайте пользователю способы доставки, которые недоступны для его города. Лучше отфильтровать их на бэкенде, чем выводить серые радиокнопки. То же самое с оплатами - не показывайте «Наложенный платеж», если доставка - курьером по городу, где эта опция не работает. MiniShop3 через payments в каждой доставке позволяет это сделать.
Кнопка оформления. Она должна быть крупной, контрастной и всегда видна. Если форма длинная, продублируйте кнопку внизу и в боковой панели с итоговой суммой. Итоговая панель должна показывать: стоимость товаров, стоимость доставки (0, если не выбрана), итог. Без этого клиент не нажмет «Оформить».
Модальное окно после добавления. Когда пользователь нажал «В корзину» в каталоге, не молчите. Покажите всплывающее окно с кнопками «Продолжить покупки» и «Оформить заказ». Это снижает тревожность - человек видит, что товар точно добавился. Для MiniShop3 используется колбэк ms3:cart:add, внутри которого вы рендерите модалку.
Пример реализации простого модального окна:
document.addEventListener('ms3:cart:add', (e) => {
const modalHtml = `
<div class="cart-modal-overlay">
<div class="cart-modal">
<p>Товар добавлен в корзину</p>
<button class="continue-btn">Продолжить</button>
<button class="checkout-btn">Оформить</button>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
document.querySelector('.checkout-btn').onclick = () => {
window.location.href = '/checkout';
};
});
Валидация в реальном времени. Не ждите, пока пользователь нажмет «Оформить», чтобы подсветить ошибки. Вешайте обработчик на blur полей и вызывайте ms3.order.setField. Сервер вернет ошибку мгновенно. Поле с неверным email подсветится до отправки формы профессионально и удобно.
Корзина и оформление: Заставляем считать
Для работы корзины на странице нужно разместить вызов [[!msCart]]. Он выведет таблицу с добавленными товарами. Но стандартная верстка там... спартанская. Придется править чанк tpl.msCart. Добавьте туда кнопки изменения количества товара (data-action="change"), удаления и иконки.
Сниппет msOrder отвечает за форму оформления. Он умеет валидировать поля (через FormIt на бэкенде), считать доставку и комиссии. Чтобы добавить поле «ИНН» или «Комментарий», не лезьте в сниппет. Просто добавьте поле в форму и обработайте через плагин, подписавшись на событие msOnBeforeCreateOrder. В коде плагина заберите значение из $_POST и запишите в $order->set('properties', $data).
Для динамического обновления цены корзины (например, доставка зависит от суммы) используйте JS-колбэки. MiniShop3 генерирует события на странице. Вешаете обработчик на изменение радиокнопок способа доставки и отправляете AJAX-запрос на обновление сниппета.
SEO: Микроразметка и 301 редирект
Поисковики любят MODX за скорость, но требуют ручной настройки. Первое - установите SeoSuite или MinifyX для управления мета-тегами. В шаблоне ресурса пропишите:
<title>[[*seodefault=`[[*pagetitle]]`]]</title> <meta name="description" content="[[*description:default=`[[*pagetitle]] - купить в магазине ...`]]">
Для товаров обязательно заполняйте longtitle заголовок в сниппете выдачи. Используйте Schema.org (JSON-LD). В чанк карточки товара добавьте скрипт с микроразметкой, подставляя значения из плейсхолдеров: [[+price]], [[+article]], [[+image]]. Гугл проверяет это строго.
Что делать с удаленными товарами? Если товар снят с продажи, не удаляйте его физически - снимите галочку «Опубликовано». В файле .htaccess пропишите 301 редирект со старого URL на похожий товар или категорию. Пример:
RewriteRule ^catalog/staraya-model.html$ /catalog/novaya-model.html [R=301,L]
Периодически проверяйте битые ссылки через curl -I. Если сервер отдает 404, поисковик быстро «забудет» вес страницы.
Оптимизация и кэширование: Выжимаем 100 баллов Pagespeed
MODX Revo славится тяжелой админкой, но фронт может летать. Используйте PageSpeed (платный, но окупается). Этот сниппет сжимает HTML, склеивает CSS/JS, переводит картинки в WebP на лету и подставляет loading="lazy" для изображений. Вызов ставится в самом низу чанка footer:
[[!PageSpeed?
&minify=`html`
&convert=`static`
&quality=`85`
]]
Настройте кэширование на сервере (Redis или хотя бы Memcached). В системных настройках MODX выберите cache_handler как xPDOFileCache (по умолчанию) или более продвинутый вариант. Обязательно включите кэширование для ресурсов и сниппетов. Для pdoTools используйте встроенный кэш выборок - он хранит результат SQL-запроса в файлах, не трогая БД при каждом хите.
С composer не дружите? А зря. Установка MiniShop3 через Composer с авто-загрузкой классов ускоряет работу за счет опкэширования PHP. Но для новичка это сложно - ставьте через Package Manager.
Безопасность и финальные штрихи
Закройте доступ к папке core через .htaccess (она обычно и так закрыта, но проверьте). Удалите файл setup/ после установки. Обновляйте MODX и пакеты вовремя - в старых версиях находят дыры.
Не забудьте про robots.txt и sitemap.xml. Для генерации карты сайта используйте GoogleSitemap - он умеет обходить все ресурсы, включая товары MiniShop3.
Самодельный магазин на MODX Revo не хобби, а полноценный инструмент бизнеса. Да, это требует больше кода, чем типовые решения. Но вы получаете полный контроль над HTML, отсутствие «магических» обновлений, ломающих верстку, и скорость загрузки, которую не стыдно показать аудитору. Действуйте.
