Раздел блога отличается от лендингов: здесь важна структура, дата публикации и удобная навигация. Мы будем использовать pdoPage для вывода списка статей и Fenom для сборки шаблонов из файлов.
- Что нам понадобится:
- 1. Подготовка общих элементов
- Универсальная шапка (Header)
- Форма
- Сайдбар (Sidebar)
- Информация о блоге
- Поиск по блогу
- Последние публикации
- Последние комментарии
- Категории
- 2. Лента новостей (blog.tpl)
- 1. Чанк карточки статьи
- 2. Основная разметка шаблона (blog.tpl)
- 3. Страница статьи (blog_post.tpl)
- Преимущества такого подхода
Что нам понадобится:
- pdoTools — базовый набор сниппетов для вывода ресурсов.
- Шаблоны:
blog.tpl(список) иblog_post.tpl(страница статьи). - Файловые чанки: все компоненты будем хранить в папке
assets/elements/chunks/.
1. Подготовка общих элементов
Универсальная шапка (Header)
В обоих шаблонах нам нужна шапка с хлебными крошками. Вместо того чтобы дублировать код, вызываем файловый чанк.
В самом верху файлов blog.tpl и blog_post.tpl добавьте:
{include 'file:chunks/pageblocks/pb.page_header.tpl'
title = $_modx->resource.pagetitle
show_breadcrumbs = 1
}
Здесь мы используем тот же чанк, что и в блоках, но вызываем его вручную.
Форма
Так же в обоих шаблонах, перед футером у нас есть форма заказа SEO аудита, так же используем уже готовую
{include 'file:chunks/pageblocks/pb.footer_form.tpl'
title = 'Получите бесплатный SEO-аудит'
subtitle_1 = 'Улучшите свой SEO-рейтинг'
subtitle_2 = 'Лучшие SEO-функции и методологии.'
}
Сайдбар (Sidebar)
Создайте файл assets/elements/chunks/blog/sidebar.tpl. Вставьте в него весь код сайтбара <aside class="sidebar">прочий код</div>.
Разберём оригинальную структуру Porto блок за блоком.
Информация о блоге
{set $about_blog = 'cc_about_blog' | config}
{if $about_blog}
<div class="px-3 mb-4">
<h3 class="text-color-quaternary text-capitalize font-weight-bold text-5 m-0 mb-3">О блоге</h3>
<p class="m-0">{$about_blog}</p>
</div>
<div class="py-1 clearfix">
<hr class="my-2">
</div>
{/if}
Системная настройка для «О блоге»:
- Ключ:
cc_about_blog - Значение:
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc viverra lorem ipsum erat orci.
Поиск по блогу
Пока пропускаем — реализуем его в одном из следующих уроков.
Последние публикации
<div class="px-3 mt-4">
{* Определяем ID раздела блога. Если мы в статье, берем родителя. Если в разделе - текущий ID *}
{var $blog_parent = $_modx->resource.class_key == 'modDocument' ? $_modx->resource.parent : $_modx->resource.id}
{* Или жестко задайте ID, если сайдбар используется сквозняком: {var $blog_parent = 12} *}
<h3 class="text-color-quaternary text-capitalize font-weight-bold text-5 m-0 mb-3">Последние публикации</h3>
<div class="pb-2 mb-1">
{'!pdoResources' | snippet : [
'parents' => $blog_parent,
'limit' => 3,
'sortby' => 'publishedon',
'tpl' => '@FILE chunks/blog/sidebar_post_item.tpl',
]}
</div>
</div>
Код чанка sidebar_post_item.tpl:
<a href="{$uri}" class="text-color-default text-uppercase text-1 mb-0 d-block text-decoration-none">
{$publishedon | date_format : "%d %b %Y"}
<span class="opacity-3 d-inline-block px-2">|</span>
{* Комментарии временно захардкожены *}
3 Comments
<span class="opacity-3 d-inline-block px-2">|</span>
{$createdby | user : 'fullname'}
</a>
<a href="{$uri}" class="text-color-dark text-hover-primary font-weight-bold text-3 d-block pb-3 line-height-4">
{$pagetitle}
</a>
Поле количество комментариев пока оставили нетронутым — внедрим потом (как добавим систему комментирования).
Скудненько как то он выглядит, можно его модифицировать к примеру вот так:
<div class="px-3 mt-4">
<h3 class="text-color-quaternary text-capitalize font-weight-bold text-5 m-0 mb-3">Последние публикации</h3>
<ul class="nav pb-2 mb-1">
{'!pdoResources' | snippet : [
'parents' => $blog_parent,
'limit' => 3,
'sortby' => 'publishedon',
'sortdir' => 'DESC',
'tpl' => '@FILE chunks/blog/sidebar_post_item.tpl',
'includeTVs' => 'image',
'tvPrefix' => ''
]}
</ul>
</div>
где sidebar_post_item.tpl:
<li>
<div class="post-image">
<div class="img-thumbnail img-thumbnail-no-borders d-block">
<a href="{$uri}">
<img src="{$_pls['tv.image'] | pdoRescale : '50x50' ?: 'assets/img/blog/default-thumb.jpg'}"
width="50"
height="50"
alt="{$pagetitle}"
style="object-fit: cover;">
</a>
</div>
</div>
<div class="post-info">
<a href="{$uri}" class="text-color-dark font-weight-semibold text-2 line-height-1">
{$pagetitle}
</a>
<div class="post-meta">
{$publishedon | date_format : "%d %b %Y"}
</div>
</div>
</li>
Разбор важных моментов:
pdoRescale: в коде я добавил фильтрpdoRescale : '50x50'. Это полезно, если вы используете компонентpThumbилиphpThumbOn. Он автоматически нарежет маленькую превьюшку, чтобы не грузить полноразмерное фото в сайдбар. Если компонент не установлен, просто оставьтеsrc="{$_pls['tv.image']}".object-fit: cover: этот css стиль гарантирует, что даже если картинки статей имеют разные пропорции, в кружочке (или квадратике) сайдбара они будут смотреться аккуратно, без искажений.- Заглушка (Placeholder): если у статьи нет картинки (
?:), подставится дефолтное изображение. Проверьте путь до него.
Последние комментарии
Пока пропускаем, так как системы комментирования у нас пока нет.
Категории
<div class="px-3 mt-4">
<h3 class="text-color-quaternary text-capitalize font-weight-bold text-5 m-0">Категории</h3>
{'!pdoMenu' | snippet : [
'parents' => $blog_parent,
'level' => 2,
'showHidden' => 0,
'countChildren' => 1,
'hideSubMenus' => 1, {* Скрыть категории без статей *}
'outerClass' => 'nav nav-list flex-column mt-2 mb-0 p-relative right-9',
'rowClass' => 'nav-item',
'hereClass' => 'active',
'tpl' => '@INLINE <li class="nav-item"><a class="nav-link bg-transparent border-0 {$classnames}" href="{$link}">{$menutitle} ({$children})</a></li>',
'tplParentRow' => '@INLINE <li class="nav-item"><a class="nav-link bg-transparent border-0 {$classnames}" href="{$link}">{$menutitle} ({$children})</a><ul>{$wrapper}</ul></li>'
]}
</div>
Конечный код sidebar.tpl:
{set $about_blog = 'cc_about_blog' | config}
{* Определяем ID раздела блога. Если мы в статье, берем родителя. Если в разделе - текущий ID *}
{var $blog_parent = $_modx->resource.class_key == 'modDocument' ? $_modx->resource.parent : $_modx->resource.id}
{* Или жестко задайте ID, если сайдбар используется сквозняком: {var $blog_parent = 12} *}
<aside class="sidebar">
{if $about_blog}
<div class="px-3 mb-4">
<h3 class="text-color-quaternary text-capitalize font-weight-bold text-5 m-0 mb-3">О блоге</h3>
<p class="m-0">{$about_blog}</p>
</div>
<div class="py-1 clearfix">
<hr class="my-2" />
</div>
{/if}
<div class="px-3 mt-4">
<form action="page-search-results.html" method="get">
<div class="input-group mb-3 pb-1">
<input
class="form-control box-shadow-none text-1 border-0 bg-color-grey"
placeholder="Search..."
name="s"
id="s"
type="text" />
<button type="submit" class="btn bg-color-grey text-1 p-2"><i class="fas fa-search m-2"></i></button>
</div>
</form>
</div>
<div class="py-1 clearfix">
<hr class="my-2" />
</div>
<div class="px-3 mt-4">
<h3 class="text-color-quaternary text-capitalize font-weight-bold text-5 m-0 mb-3">Последние публикации</h3>
<div class="pb-2 mb-1">
{'!pdoResources' | snippet : [
'parents' => $blog_parent,
'limit' => 3,
'sortby' => 'publishedon',
'tpl' => '@FILE chunks/blog/sidebar_post_item.tpl'
]}
</div>
</div>
<div class="py-1 clearfix">
<hr class="my-2" />
</div>
<div class="px-3 mt-4">
<h3 class="text-color-quaternary text-capitalize font-weight-bold text-5 m-0 mb-3">Последние комментарии</h3>
<div class="pb-2 mb-1">
<a href="#" class="text-color-default text-2 mb-0 d-block text-decoration-none line-height-4"
>Admin on <strong class="text-color-dark text-hover-primary text-4">Vivamus sollicitudin</strong>
<span class="text-uppercase text-1 d-block pt-1 pb-3">10 Jan 2025</span></a
>
<a href="#" class="text-color-default text-2 mb-0 d-block text-decoration-none line-height-4"
>John Doe on <strong class="text-color-dark text-hover-primary text-4">Lorem ipsum dolor</strong>
<span class="text-uppercase text-1 d-block pt-1 pb-3">10 Jan 2025</span></a
>
<a href="#" class="text-color-default text-2 mb-0 d-block text-decoration-none line-height-4"
>Admin on <strong class="text-color-dark text-hover-primary text-4">Consectetur adipiscing</strong>
<span class="text-uppercase text-1 d-block pt-1 pb-3">10 Jan 2025</span></a
>
</div>
</div>
<div class="py-1 clearfix">
<hr class="my-2" />
</div>
<div class="px-3 mt-4">
<h3 class="text-color-quaternary text-capitalize font-weight-bold text-5 m-0">Категории</h3>
{'!pdoMenu' | snippet : [
'parents' => $blog_parent,
'level' => 2,
'showHidden' => 0,
'countChildren' => 1,
'hideSubMenus' => 1, {* Скрыть категории без статей *}
'outerClass' => 'nav nav-list flex-column mt-2 mb-0 p-relative right-9',
'rowClass' => 'nav-item',
'hereClass' => 'active',
'tpl' => '@INLINE
<li class="nav-item">
<a class="nav-link bg-transparent border-0 {$classnames}" href="{$link}">{$menutitle} ({$children})</a>
</li>
', 'tplParentRow' => '@INLINE
<li class="nav-item">
<a class="nav-link bg-transparent border-0 {$classnames}" href="{$link}">{$menutitle} ({$children})</a>
<ul>
{$wrapper}
</ul>
</li>
' ]}
</div>
</aside>
2. Лента новостей (blog.tpl)
Для страницы списка статей мы будем использовать сниппет pdoPage, который автоматически разбивает статьи на страницы.
1. Чанк карточки статьи
Создайте файл assets/elements/chunks/blog/blog_item.tpl. Это HTML-код одного анонса в ленте.
<article class="mb-5">
<div class="card border-0 border-radius-0 box-shadow-1">
<div class="card-body p-4 z-index-1">
<a href="{$uri}">
<img class="card-img-top border-radius-0 mb-4" src="{$_pls['tv.image'] ?: 'assets/img/blog/default.jpg'}" alt="{$pagetitle}">
</a>
<p class="text-uppercase text-1 mb-3 text-color-default">
<time datetime="{$publishedon | date : 'Y-m-d'}">{$publishedon | date : 'd M Y'}</time>
{* Если category_name не задан в плейсхолдерах, используем pagetitle родителя *}
<span class="opacity-3 mx-1">|</span> {$_pls['category_name'] ?: ($_pls['parent'] | resource : 'pagetitle')}
</p>
<div class="card-body p-0">
<h4 class="card-title mb-3 text-5 font-weight-bold"><a class="text-color-dark text-color-hover-primary" href="{$uri}">{$pagetitle}</a></h4>
<p class="card-text mb-4">{$introtext | truncate : 150 : '...'}</p>
<a href="{$uri}" class="font-weight-bold text-uppercase text-decoration-none text-3">Читать далее</a>
</div>
</div>
</div>
</article>
2. Основная разметка шаблона (blog.tpl)
Теперь соберем все вместе.
{extends 'file:templates/base.tpl'}
{block 'content'}
{include 'file:chunks/pageblocks/pb.page_header.tpl'
title = $_modx->resource.pagetitle
show_breadcrumbs = 1
}
<section class="section bg-color-light position-relative border-0 pt-3 m-0">
<svg class="custom-page-header-curved-top-1" width="100%" height="700" xmlns="http://www.w3.org/2000/svg">
<path transform="rotate(-3.1329219341278076 1459.172607421877,783.5322875976566) " d="m-12.54488,445.11701c0,0 2.16796,-1.48437 6.92379,-3.91356c4.75584,-2.42918 12.09956,-5.80319 22.45107,-9.58247c20.70303,-7.55856 53.43725,-16.7382 101.56202,-23.22255c48.12477,-6.48434 111.6401,-10.27339 193.90533,-7.05074c41.13262,1.61132 88.20271,5.91306 140.3802,12.50726c230.96006,32.89734 314.60609,102.57281 635.26547,59.88645c320.65938,-42.68635 452.47762,-118.72154 843.58759,3.72964c391.10997,122.45118 553.23416,-82.15958 698.49814,-47.66481c-76.25064,69.23438 407.49874,281.32592 331.2481,350.5603c-168.91731,29.52009 85.02254,247.61162 -83.89478,277.13171c84.07062,348.27313 -2948.95065,-242.40222 -2928.39024,-287.84045" stroke-width="0" stroke="#000" fill="#FFF" id="svg_2"/>
</svg>
<div class="container">
<div class="row">
<div class="col-lg-9 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="1600">
{'!pdoPage' | snippet : [
'element' => 'pdoResources',
'parents' => $_modx->resource.id,
'limit' => 5,
'tpl' => '@FILE chunks/blog/blog_item.tpl',
'includeTVs' => 'image',
'tvPrefix' => '',
'sortby' => 'publishedon',
'sortdir' => 'DESC',
'tplPageWrapper' => '@INLINE <ul class="custom-pagination-style-1 pagination pagination-rounded pagination-md justify-content-center">{$prev}{$pages}{$next}</ul>',
'tplPage' => '@INLINE <li class="page-item"><a class="page-link" href="{$href}">{$pageNum}</a></li>',
'tplPageActive' => '@INLINE <li class="page-item active"><a class="page-link" href="{$href}">{$pageNum}</a></li>',
'tplPagePrev' => '@INLINE <li class="page-item"><a class="page-link" href="{$href}"><i class="fas fa-angle-left"></i></a></li>',
'tplPageNext' => '@INLINE <li class="page-item"><a class="page-link" href="{$href}"><i class="fas fa-angle-right"></i></a></li>',
'tplPagePrevEmpty' => '',
'tplPageNextEmpty' => ''
]}
{$_modx->getPlaceholder('page.nav')}
</div>
<div class="col-lg-3 pt-4 pt-lg-0 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="1800">
{include 'file:chunks/blog/sidebar.tpl'}
</div>
</div>
</div>
</section>
{include 'file:chunks/pageblocks/pb.footer_form.tpl'
title = 'Get Your Free Instant SEO Audit Now'
subtitle_1 = 'Improve your seo ranking with porto'
subtitle_2 = 'Best SEO Features & Methodologies.'
}
{/block}
3. Страница статьи (blog_post.tpl)
Самая важная часть. Здесь мы выводим контент, информацию об авторе и кнопки «Поделиться».
Особенности реализации:
- Контент берем напрямую из
[[*content]](в Fenom:{$_modx->resource.content}). - Фон заголовка (SVG) оставляем в коде, чтобы сохранить уникальный дизайн Porto.
- Данные автора берем динамически из профиля пользователя, создавшего ресурс.
Код шаблона blog_post.tpl:
{extends 'file:templates/base.tpl'}
{block 'content'}
{include 'file:chunks/pageblocks/pb.page_header.tpl'
title = $_modx->resource.pagetitle
show_breadcrumbs = 1
}
<section class="section bg-color-light position-relative border-0 pt-3 m-0">
<svg class="custom-page-header-curved-top-1" width="100%" height="700" xmlns="http://www.w3.org/2000/svg">
<path transform="rotate(-3.1329219341278076 1459.172607421877,783.5322875976566) " d="m-12.54488,445.11701c0,0 2.16796,-1.48437 6.92379,-3.91356c4.75584,-2.42918 12.09956,-5.80319 22.45107,-9.58247c20.70303,-7.55856 53.43725,-16.7382 101.56202,-23.22255c48.12477,-6.48434 111.6401,-10.27339 193.90533,-7.05074c41.13262,1.61132 88.20271,5.91306 140.3802,12.50726c230.96006,32.89734 314.60609,102.57281 635.26547,59.88645c320.65938,-42.68635 452.47762,-118.72154 843.58759,3.72964c391.10997,122.45118 553.23416,-82.15958 698.49814,-47.66481c-76.25064,69.23438 407.49874,281.32592 331.2481,350.5603c-168.91731,29.52009 85.02254,247.61162 -83.89478,277.13171c84.07062,348.27313 -2948.95065,-242.40222 -2928.39024,-287.84045" stroke-width="0" stroke="#000" fill="#FFF" id="svg_2"/>
</svg>
<div class="container">
<div class="row">
<div class="col-lg-9 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="1600">
<article>
<div class="card border-0 border-radius-0 mb-5 box-shadow-1">
<div class="card-body p-4 z-index-1">
{* Мета-данные *}
<p class="text-uppercase text-1 mb-3 text-color-default">
<time pubdate datetime="{$_modx->resource.publishedon | date : 'Y-m-d'}">
{$_modx->resource.publishedon | date : 'd M Y'}
</time>
<span class="opacity-3 d-inline-block px-2">|</span>
{$_modx->resource.createdby | user : 'fullname'}
{* Если подключите комментарии например от (easyComm), раскомментируйте строку ниже *}
{* <span class="opacity-3 d-inline-block px-2">|</span> <span id="comment-count-placeholder">0</span> Comments *}
</p>
{* Изображение *}
{if $_modx->resource.image}
<div class="post-image pb-4">
<img class="card-img-top border-radius-0" src="{$_modx->resource.image}" alt="{$_modx->resource.pagetitle}">
</div>
{/if}
<div class="card-body p-0">
{$_modx->resource.content}
<div class="a2a_kit a2a_kit_size_32 a2a_default_style mt-4">
<a class="a2a_dd" href="https://www.addtoany.com/share"></a>
<a class="a2a_button_facebook"></a>
<a class="a2a_button_x"></a>
<a class="a2a_button_copy_link"></a>
</div>
<script async src="https://static.addtoany.com/menu/page.js"></script>
<hr class="my-5">
{* БЛОК АВТОРА *}
<div class="post-block post-author">
<h3 class="text-color-secondary text-capitalize font-weight-bold text-5 m-0 mb-3">Автор</h3>
<div class="img-thumbnail img-thumbnail-no-borders d-block pb-3">
{var $photo = $_modx->resource.createdby | user : 'photo'}
<a href="{$_modx->makeUrl(6)}"> {* 6 - ID страницы 'Наша команда' или профиля *}
<img src="{$photo ?: 'assets/img/avatars/default.jpg'}" class="rounded-circle" alt="{$_modx->resource.createdby | user : 'fullname'}" style="width: 80px; height: 80px; object-fit: cover;">
</a>
</div>
<p>
<strong class="name">
<a href="{$_modx->makeUrl(6)}" class="text-4 text-dark pb-2 pt-2 d-block">
{$_modx->resource.createdby | user : 'fullname'}
</a>
</strong>
</p>
{* Поле address часто пустое, можно использовать extended.job_title или другое поле *}
<p>{$_modx->resource.createdby | user : 'address'}</p>
</div>
<hr class="my-5">
{* Комментарии *}
{include 'file:chunks/blog/post_comments.tpl'}
</div>
</div>
</div>
</article>
</div>
<div class="col-lg-3 pt-4 pt-lg-0 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="1800">
{include 'file:chunks/blog/sidebar.tpl'}
</div>
</div>
</div>
</section>
{include 'file:chunks/pageblocks/pb.footer_form.tpl'
title = 'Get Your Free Instant SEO Audit Now'
subtitle_1 = 'Improve your seo ranking with porto'
subtitle_2 = 'Best SEO Features & Methodologies.'
}
{/block}
Шаблон детальной страницы спроектирован так, чтобы быть автономным и динамичным. Мы не заполняем данные об авторе или дате вручную в контенте — всё подтягивается автоматически.
Ключевые фишки шаблона:
- Динамический профиль автора: в блоке
post-authorмы не пишем имя вручную. Используя модификатор| user : 'fullname', MODX берет данные пользователя, который создал ресурс. Фотография автора (photo) также подтягивается из профиля. Это значит, что если автор сменит аватарку в админке, она обновится во всех его статьях. - Защита от отсутствия контента: в коде предусмотрены проверки. Например, блок с главной картинкой (
post-image) выводится только через условие{if $_modx->resource.image}. Если контент-менеджер забыл загрузить обложку, пустой блок не сломает верстку — он просто не появится. - Smart-заглушки: в сайдбаре и блоке автора мы используем оператор Elvis (
?:). Конструкцияsrc="{$photo ?: 'default.jpg'}"работает так: «Поставь фото автора, но если его нет — поставь дефолтную картинку». Это сохраняет визуальную целостность дизайна. - Разделение логики и дизайна: сложные декоративные элементы (например, SVG-волна под шапкой) прописаны прямо в шаблоне (или вынесены в чанк), а сам текст статьи выводится одной строкой
{$_modx->resource.content}. Это позволяет менять дизайн страницы глобально, не редактируя каждую статью отдельно.
Преимущества такого подхода
- Чистота: PageBlocks используется только там, где нужен конструктор (Главная, Лендинги услуг). Функциональные разделы (Блог, Новости, Каталог) работают на нативном MODX.
- Скорость: меньше запросов к базе данных (не нужно грузить модель PageBlocks).
- Понятность: разработчику сразу видно в шаблоне, где вызывается
pdoPage, и легко поправить параметры (например,limit), не заходя в админку в настройки блока.
Таким образом, PageBlocks у нас остается только на:
- Главной странице (Home).
- Списке услуг (Services) — хотя и здесь можно переделать на pdoResources + include, если структура жесткая.
- Контактах (Contacts).
- Детальной услуге (Service Detail).
А разделы Блога (список и статья) работают на чистых шаблонах. Это очень грамотное архитектурное разделение.







