Создание блога на 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 = 'Get Your Free Instant SEO Audit Now'
    subtitle_1 = 'Improve your seo ranking with porto'
    subtitle_2 = 'Best SEO Features & Methodologies.'
}

Сайдбар (Sidebar)

Создайте файл assets/elements/chunks/blog/sidebar.tpl. В нем мы разместим категории и свежие посты.

Код чанка sidebar.tpl:

{* Определяем ID раздела блога. Если мы в статье, берем родителя. Если в разделе - текущий ID *}
{var $blog_parent = $_modx->resource.class_key == 'modDocument' ? $_modx->resource.parent : $_modx->resource.id}
{* Или жестко задайте ID, если сайдбар используется сквозняком: {var $blog_parent = 12} *}

<aside class="sidebar">
    <h5 class="font-weight-bold pt-4">Категории</h5>
    {'!pdoMenu' | snippet : [
        'parents' => $blog_parent,
        'outerClass' => 'nav nav-list flex-column mb-5',
        'rowClass' => 'nav-item',
        'tpl' => '@INLINE <li class="{$classnames}"><a class="nav-link" href="{$link}">{$menutitle}</a></li>'
    ]}

    <h5 class="font-weight-bold pt-4">Свежие записи</h5>
    <ul class="simple-post-list">
        {'!pdoResources' | snippet : [
            'parents' => $blog_parent,
            'limit' => 3,
            'sortby' => 'publishedon',
            'tpl' => 'file:chunks/blog/sidebar_post_item.tpl',
            'includeTVs' => 'image'
        ]}
    </ul>
</aside>

Код чанка 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): если у статьи нет картинки (?:), подставится дефолтное изображение. Проверьте путь до него.

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 для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.