Создание блога на MODX Revo: pdoTools, нативные шаблоны и Fenom

Создание блога на MODX Курс

Раздел блога отличается от лендингов: здесь важна структура, дата публикации и удобная навигация. Мы будем использовать pdoPage для вывода списка статей и Fenom для сборки шаблонов из файлов.

Что нам понадобится:

  1. pdoTools — базовый набор сниппетов для вывода ресурсов.
  2. Шаблоны: blog.tpl (список) и blog_post.tpl (страница статьи).
  3. Файловые чанки: все компоненты будем хранить в папке 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>

Разбор важных моментов:

  1. pdoRescale: в коде я добавил фильтр pdoRescale : '50x50'. Это полезно, если вы используете компонент pThumb или phpThumbOn. Он автоматически нарежет маленькую превьюшку, чтобы не грузить полноразмерное фото в сайдбар. Если компонент не установлен, просто оставьте src="{$_pls['tv.image']}".
  2. object-fit: cover: этот css стиль гарантирует, что даже если картинки статей имеют разные пропорции, в кружочке (или квадратике) сайдбара они будут смотреться аккуратно, без искажений.
  3. Заглушка (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)

Самая важная часть. Здесь мы выводим контент, информацию об авторе и кнопки «Поделиться».

Особенности реализации:

  1. Контент берем напрямую из [[*content]] (в Fenom: {$_modx->resource.content}).
  2. Фон заголовка (SVG) оставляем в коде, чтобы сохранить уникальный дизайн Porto.
  3. Данные автора берем динамически из профиля пользователя, создавшего ресурс.

Код шаблона 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}

Шаблон детальной страницы спроектирован так, чтобы быть автономным и динамичным. Мы не заполняем данные об авторе или дате вручную в контенте — всё подтягивается автоматически.

Ключевые фишки шаблона:

  1. Динамический профиль автора: в блоке post-author мы не пишем имя вручную. Используя модификатор | user : 'fullname', MODX берет данные пользователя, который создал ресурс. Фотография автора (photo) также подтягивается из профиля. Это значит, что если автор сменит аватарку в админке, она обновится во всех его статьях.
  2. Защита от отсутствия контента: в коде предусмотрены проверки. Например, блок с главной картинкой (post-image) выводится только через условие {if $_modx->resource.image}. Если контент-менеджер забыл загрузить обложку, пустой блок не сломает верстку — он просто не появится.
  3. Smart-заглушки: в сайдбаре и блоке автора мы используем оператор Elvis (?:). Конструкция src="{$photo ?: 'default.jpg'}" работает так: «Поставь фото автора, но если его нет — поставь дефолтную картинку». Это сохраняет визуальную целостность дизайна.
  4. Разделение логики и дизайна: сложные декоративные элементы (например, SVG-волна под шапкой) прописаны прямо в шаблоне (или вынесены в чанк), а сам текст статьи выводится одной строкой {$_modx->resource.content}. Это позволяет менять дизайн страницы глобально, не редактируя каждую статью отдельно.

Преимущества такого подхода

  1. Чистота: PageBlocks используется только там, где нужен конструктор (Главная, Лендинги услуг). Функциональные разделы (Блог, Новости, Каталог) работают на нативном MODX.
  2. Скорость: меньше запросов к базе данных (не нужно грузить модель PageBlocks).
  3. Понятность: разработчику сразу видно в шаблоне, где вызывается pdoPage, и легко поправить параметры (например, limit), не заходя в админку в настройки блока.

Таким образом, PageBlocks у нас остается только на:

  1. Главной странице (Home).
  2. Списке услуг (Services) — хотя и здесь можно переделать на pdoResources + include, если структура жесткая.
  3. Контактах (Contacts).
  4. Детальной услуге (Service Detail).

А разделы Блога (список и статья) работают на чистых шаблонах. Это очень грамотное архитектурное разделение.

Оцените статью
MODX 3
Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.