Интеграция Bootstrap-шаблона в MODX через PageBlocks Free и Fenom

Курс

Продолжаем интегрировать нашу верстку. В этом уроке мы сделаем полностью управляемыми блоки главной страницы с помощью PageBlocks Free. Мы не будем упрощать верстку, а сохраним все анимации, SVG-разделители и параллакс-эффекты.

Предполагается, что вы уже знакомы с PageBlocks Free или как минимум прочитали его документацию. В этом уроке чисто практика.

Алгоритм интеграции или как съесть слона по частям

Сейчас код вашей главной страницы (home.tpl) выглядит как бесконечная «простыня» HTML-кода на 600+ строк.

Работать с таким — мучение. Мы нарежем его на 8 логических блоков.

Процесс интеграции выглядит так:

  1. Анализ и нарезка. Мы берем исходный HTML-код секции (например, первого экрана), вырезаем его из home.tpl и переносим в отдельный чанк (например, pb.hero).
  2. Конфигурация блока. В компоненте PageBlocks создаем новый блок, привязываем к нему этот чанк и создаем поля под контент (заголовки, картинки, списки).
  3. Магия Fenom. Возвращаемся в чанк и заменяем статический текст на переменные Fenom (например, <h1>Title</h1> меняем на <h1>{$title}</h1>).
  4. Повторение. Проделываем это для всех 8 блоков.
  5. Сборка. В шаблоне главной страницы оставляем всего один вызов: {'pageblocks' | snippet}.

В итоге мы получим пустой шаблон с 4 мя строками, который будет наполняться контентом динамически.

{extends 'file:templates/base.tpl'}
{block 'content'}
    {'pageblocks' | snippet}
{/block}

Практика: собираем динамический лендинг (Porto SEO-2)

Если вы хотите в дальнейшем иметь доступ к созданным чанкам через IDE, рекомендую предварительно установить  специальный плагин: StaticFilesPlus (доступен в modstore).

Блок 1: Hero Section (Первый экран)

Блок 1: Hero Section (Первый экран)

Этот блок содержит много декоративных элементов (div.custom-circle), которые менеджеру редактировать не нужно. Мы вынесем в переменные только тексты, кнопку и картинку справа.

Идем в Пакеты — PageBlocks и создаем наш первый блок «Приветствие» (чанк pb.hero).

Добавляем поля:

  • title (Тип: Текст) — Заголовок H1.
  • subtitle (Тип: Текст) — Подзаголовок.
  • button (Тип: Кнопка) — Кнопка призыва.
  • image (Тип: Изображение) — Фото справа.

Содержимое чанка pb.hero

Вставляем HTML. Декоративные круги оставляем «хардкодом» (они часть дизайна), а контент заменяем переменными.

<section class="section custom-bg-color-light-1 border-0 pt-5 m-0">
    <div class="container position-relative z-index-1 pt-5 mt-5">
        
        {* --- Декоративные круги (Оставляем как есть) --- *}
        <div class="custom-circle custom-circle-wrapper custom-circle-big custom-circle-pos-1 appear-animation" data-appear-animation="expandInWithBlur" data-appear-animation-delay="900" data-appear-animation-duration="2s">
            <div class="bg-color-tertiary rounded-circle w-100 h-100" data-plugin-float-element data-plugin-options="{ 'startPos': 'bottom', 'speed': 0.5, 'transition': true, 'transitionDuration': 1000 }"></div>
        </div>
        {* ... (здесь остальные div.custom-circle, я сократил для удобства чтения) ... *}
        <div class="custom-circle custom-circle-medium custom-circle-pos-8 appear-animation" data-appear-animation="expandInWithBlur" data-appear-animation-delay="1350" data-appear-animation-duration="2s">
            <div class="custom-bg-color-grey-2 rounded-circle w-100 h-100" data-plugin-float-element data-plugin-options="{ 'startPos': 'bottom', 'speed': 0.5, 'transition': true, 'transitionDuration': 500 }"></div>
        </div>

        <div class="row align-items-center pt-4">
            <div class="col-md-6 pb-5 mb-md-5">
                <div class="spacer" style="height: 110px;"></div>                
                {* Заголовок *}
                <h1 class="text-color-dark font-weight-bold text-10 mb-3 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="300">{$title}</h1>                
                {* Подзаголовок *}
                {if $subtitle}<p class="custom-text-color-grey-1 text-4 mb-4 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="450">{$subtitle}</p>{/if}                
                {* Кнопка *}
                {if $button.published}<a href="{$button.href?:($button.resource|resource:'uri')}" class="btn btn-gradient btn-rounded font-weight-semibold px-5 py-3 text-3 mb-md-5 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="600">{$button.caption}</a>{/if}

                <div class="spacer d-none d-md-block" style="height: 310px;"></div>
            </div>
            
            <div class="col-md-6 pb-5">
                {* Изображение *}
                <img src="{$image.url}" class="img-fluid position-relative z-index-1 pb-5 mb-5 ms-5 top-10 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="750" data-appear-animation-duration="1500ms" alt="{$image.title}">
            </div>
        </div>
    </div>
</section>

Примечание для этого и последующих блоков: везде где видите if — это не обязательные поля, а где его нет — обязательные.

Блок 2: Features Grid (Сетка преимуществ)

Блок 2: Features Grid (Сетка преимуществ)

Здесь главная сложность — сохранить структуру карточек слева. В HTML они разбиты на две колонки:

  1. col-md-6: содержит 1-ю и 2-ю карточку.
  2. col-md-6: содержит 3-ю карточку (которая смещена вниз).

Мы реализуем это через один TableList, но в чанке с помощью Fenom разобьем массив карточек на части.

Создаем таблицу cards со следующими полями:

  • icon (Изображение) — SVG иконка.
  • title (Текст) — Заголовок карточки.
  • text (Текстовая область) — Текст карточки.
  • link (Текст) — Ссылка.
  • color (Select) — Цвет акцента. Варианты: tertiary, secondary, primary (чтобы менять цвет кнопки READ MORE).

Не забываем создавать «Столбцы таблицы».

Создаем блок «Сетка преимуществ». Чанк: pb.features_grid. со следующими полями:

  1. cards (Таблица) — Карточки слева (примечание: добавляем ровно 3 штуки).
  2. title (Текст) — Заголовок справа (Capture Leads…).
  3. subtitle (Текст) — Надзаголовок (THE EASIEST WAY).
  4. lead (Текстовая область) — Жирный текст вступления.
  5. description (Текстовый редактор) — Основной текст.
  6. button (Кнопка) — Кнопка (OUR SERVICES).

Содержимое чанк pb.seo_features_grid:

<section class="section position-relative bg-color-light border-0 m-0">
    {* SVG Background *}
    <svg class="custom-section-curved-top-1" width="100%" height="600px" xmlns="http://www.w3.org/2000/svg">
        <path id="svg_1" d="..." fill="#f7f8f9"/>
        <path id="svg_2" d="..." fill="#fbfcfc"/>
        <path id="svg_3" d="..." fill="#ffffff"/>
    </svg>
    
    <div class="container position-relative custom-negative-margin-1 z-index-3 pb-lg-5 mb-lg-3">
        {* Декоративные круги (оставляем как есть, они создают красоту) *}
        <div class="custom-circle custom-circle-medium custom-circle-pos-9 d-none d-md-block">
            <div class="bg-color-secondary rounded-circle w-100 h-100" data-plugin-float-element data-plugin-options="{ 'startPos': 'top', 'speed': 0.3, 'transition': true, 'transitionDuration': 1000 }"></div>
        </div>
        {* ... остальные круги ... *}

        <div class="row align-items-center">
            
            {* ЛЕВАЯ ЧАСТЬ: КАРТОЧКИ *}
            <div class="col-lg-7 pe-lg-5">
                <div class="row align-items-center">
                    
                    {* Первая колонка с карточками (1 и 2) *}
                    <div class="col-md-6">
                        {if $cards[0]}
                        <div class="card position-relative border-0 custom-box-shadow-1 custom-border-radius-1 mb-4 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="850">
                            <div class="custom-dots-rect-1" style="background-image: url(assets/img/demos/seo-2/dots-group.png);"></div>
                            <div class="card-body text-center pt-5">
                                <img src="{$cards[0].icon.url}" class="img-fluid mb-4 pb-2" width="80" height="80" alt="" />
                                <h4 class="text-color-dark font-weight-semibold mb-2">{$cards[0].title}</h4>
                                <p>{$cards[0].text}</p>
                                <a href="{$cards[0].link}" class="text-color-{$cards[0].color} font-weight-bold">READ MORE +</a>
                            </div>
                        </div>
                        {/if}

                        {if $cards[1]}
                        <div class="card border-0 custom-box-shadow-1 custom-border-radius-1 mb-4">
                            <div class="card-body text-center pt-5">
                                <img src="{$cards[1].icon.url}" class="img-fluid mb-4 pb-2" width="80" height="80" alt="" />
                                <h4 class="text-color-dark font-weight-semibold mb-2">{$cards[1].title}</h4>
                                <p>{$cards[1].text}</p>
                                <a href="{$cards[1].link}" class="text-color-{$cards[1].color} font-weight-bold">READ MORE +</a>
                            </div>
                        </div>
                        {/if}
                    </div>

                    {* Вторая колонка с карточкой (3) *}
                    <div class="col-md-6">
                        {if $cards[2]}
                        <div class="card position-relative border-0 custom-box-shadow-1 custom-border-radius-1 mb-4 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="1000">
                            <div class="custom-dots-rect-2" style="background-image: url(assets/img/demos/seo-2/dots-group-2.png);"></div>
                            <div class="card-body text-center pt-5">
                                <img src="{$cards[2].icon.url}" class="img-fluid mb-4 pb-2" width="70" height="70" alt="" />
                                <h4 class="text-color-dark font-weight-semibold mb-2">{$cards[2].title}</h4>
                                <p>{$cards[2].text}</p>
                                <a href="{$cards[2].link}" class="text-color-{$cards[2].color} font-weight-bold">READ MORE +</a>
                            </div>
                        </div>
                        {/if}
                    </div>

                </div>
            </div>

            {* ПРАВАЯ ЧАСТЬ: ТЕКСТ *}
            <div class="col-lg-5 pt-lg-5 ps-lg-4 mt-lg-5">
                <h2 class="text-color-dark font-weight-semibold text-6 line-height-3 pt-5 mt-5 mb-0 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="300">{$title}</h2>
                {if $subtitle}<span class="d-block mb-3 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="450">{$subtitle}</span>{/if}
                {if $lead}<p class="lead appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="600">{$lead}</p>{/if}
                <div class="mb-4 pb-2 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="750">{$description}</div>
                {if $button.published}<a href="{$button.href?:($button.resource|resource:'uri')}" class="btn btn-gradient btn-rounded font-weight-semibold px-5 py-3 text-3 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="900">{$button.caption}</a>{/if}
             </div>
        </div>
    </div>
</section>

Блок 3: About & Features (О нас + Фичи)

Блок 3: About & Features (Логотипы + Текст + Иконки)

Это сложный блок. Он начинается с SVG-волны, потом идут логотипы партнеров, а ниже — текст и «фичи» (иконка + текст).

Первым делом создадим 2 таблицы:

  1. logos (Логотипы), внутри: image (Изображение).
  2. features (Список преимуществ с иконками), внутри: icon_class (Текст, например fas fa-puzzle-piece), title (Текст), text (Текстовая область), color (Select: bg-color-secondary или bg-color-tertiary).

Создаем блок «О нас + Фичи». Чанк: pb.about.

Вкладка «Поля»:

  1. logos (Таблица) — Логотипы.
  2. title (Текст) — Заголовок секции.
  3. subtitle (Текст) — Маленький текст над заголовком (THE EASIEST WAY).
  4. description (RichText) — Основной текст описания.
  5. features (Таблица) — Список преимуществ с иконками.
  6. image (Изображение) — Картинка справа.

2. Чанк pb.about

<section class="section custom-bg-color-light-2 position-relative border-0 pt-4 m-0">
    {* SVG Волна (Оставляем в коде, так как она часть дизайна блока) *}
    <svg class="custom-section-curved-top-2" width="100%" height="298" xmlns="http://www.w3.org/2000/svg">
        <path id="svg_3" d="..." fill="#eff1f3"/> 
        {* ... код пути SVG сокращен ... *}
    </svg>

    <div class="container position-relative z-index-1 mt-3 mb-5">
        {* Декоративные круги *}
        <div class="custom-circle custom-circle-small custom-circle-pos-14">
            <div class="custom-bg-color-grey-2 rounded-circle w-100 h-100" data-plugin-float-element data-plugin-options="{ 'startPos': 'top', 'speed': 1, 'horizontal': true, 'transition': true, 'transitionDuration': 1000 }"></div>
        </div>
        {* ... остальные круги ... *}

        {* --- Логотипы партнеров --- *}
        {if $logos}
        <div class="row text-center align-items-center pb-4 pt-lg-4 mb-3 mb-lg-0 appear-animation" data-appear-animation="fadeInLeftShorter" data-appear-animation-delay="300">
            {foreach $logos as $logo}
            <div class="col-md-4 col-lg-2 mb-4 mb-lg-0">
                <img src="{$logo.image.url}" class="img-fluid" alt="{$logo.image.title}" style="max-width: 130px;" />
            </div>
            {/foreach}
        </div>
        {/if}

        <div class="row pb-2 mb-4"><div class="col"><hr></div></div>

        <div class="row">
            <div class="col-lg-6 mb-5 mb-lg-0 appear-animation" data-appear-animation="fadeInRightShorter"
                 data-appear-animation-delay="450">
                <h2 class="text-color-dark font-weight-semibold text-6 line-height-3 mb-0 pe-5 me-5">{$title}</h2>
                {if $subtitle}<span class="d-block mb-3">{$subtitle}</span>{/if}
                {if $description}
                    <div class="lead pe-5 mb-4 pb-2">{$description}</div>{/if}
                {foreach $features as $item}
                    <div class="feature-box">
                        <div class="feature-box-icon custom-feature-box-icon-size-1 {$item.color} top-0">
                            <i class="{$item.icon_class} position-relative left-1"></i>
                        </div>
                        <div class="feature-box-info mb-4 pb-3">
                            <h4 class="font-weight-bold line-height-3 custom-font-size-1 mb-1">{$item.title}</h4>
                            <p class="mb-0">{$item.text}</p>
                        </div>
                    </div>
                {/foreach}
            </div>

            <div class="col-lg-6 text-center text-lg-start">
                <img src="{$image.url}" class="img-fluid appear-animation" data-appear-animation="fadeInRightShorter"
                     data-appear-animation-delay="600" alt="{$image.title}"/>
            </div>
        </div>
    </div>
</section>

Блок 4: Services Grid (Сетка услуг)

Блок 4: Services Grid (Сетка услуг)

Довольно интересный блок. Обратите внимание на верстку: карточки разбиты на 3 колонки (col-md-7 col-lg-4). В каждой колонке по 2 карточки. Чтобы реализовать это динамически, нам нужно разбить единый массив данных на 3 части.

Создаем таблицу services (Карточки услуг), со следующим набором полей:

  • icon (Изображение),
  • title (Текст),
  • text (Textarea),
  • link (Текст/Список ресурсов).

Создаем блок «Сетка услуг». Чанк: pb.services.

Вкладка «Поля»:

  1. title (Текст) — Заголовок (Our Services).
  2. subtitle (Текст) — Надзаголовок (WHAT WE DO).
  3. description (Textarea) — Описание по центру.
  4. services (Таблица) — Карточки услуг.
    • Внутри: icon (Изображение), title (Текст), text (Textarea), link (Текст/Ссылка).
  5. button (Кнопка) — Кнопка внизу («Get a Quote»).

Чанк pb.seo_services

Здесь мы используем мощь Fenom. Нам нужно вывести 3 колонки. Мы можем использовать модификатор array_chunk, чтобы разбить массив всех услуг на группы по 2 штуки.

<section class="section bg-color-light position-relative border-0 pt-0 m-0">
    {* SVG Curved Top 3 *}
    <svg class="custom-section-curved-top-3" width="100%" height="298" xmlns="http://www.w3.org/2000/svg">
        <path id="svg_2" fill="#FFF" d="..." />
    </svg>

    <div class="container position-relative z-index-1 pb-5 mb-5">
        {* Декоративные элементы пропущены для краткости, но они должны быть тут *}
        
        <div class="row justify-content-center mb-4">
            <div class="col-8 text-center">
                <div class="overflow-hidden mb-0">
                    <span class="d-block line-height-1 mb-0 appear-animation" data-appear-animation="maskUp" data-appear-animation-delay="200">{$subtitle}</span>
                </div>
                <div class="overflow-hidden mb-2">
                    <h2 class="font-weight-bold text-color-quaternary text-7 mb-0 appear-animation" data-appear-animation="maskUp" data-appear-animation-delay="350">{$title}</h2>
                </div>
                <p class="appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="500">{$description}</p>
            </div>
        </div>

        {* --- СЕТКА КАРТОЧЕК --- *}
        <div class="row justify-content-center pb-2 mb-4">
            {* Разбиваем массив услуг на чанки по 2 элемента *}
            {var $chunks = $services | array_chunk : 2}

            {foreach $chunks as $col_index => $col_services}
                {* Для второй колонки добавляем отступ pt-lg-4 mt-lg-5, как в верстке *}
                <div class="col-md-7 col-lg-4 {if $col_index == 1}pt-lg-4 mt-lg-5{/if} mb-4 mb-lg-0">
                    
                    {foreach $col_services as $service}
                        <div class="card border-0 custom-box-shadow-1 custom-border-radius-1 mb-4 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="400" data-plugin-options="{ 'accY': -100 }">
                            
                            {* Если нужно добавить декоративные точки только первой карточке, можно добавить условие $service.first *}
                            {if $service@first && $col_index == 0}
                                <div class="custom-dots-rect-3" style="background-image: url(assets/img/demos/seo-2/dots-group-3.png);"></div>
                            {/if}

                            <div class="card-body text-center p-5">
                                {if $service.icon}
                                    <img src="{$service.icon.url}" class="img-fluid mb-4 mt-3 pb-3" width="55" alt="" />
                                {/if}
                                <h4 class="text-color-dark font-weight-semibold mb-3">{$service.title}</h4>
                                <p>{$service.text}</p>
                                <a href="{$service.link}" class="text-color-tertiary font-weight-bold">READ MORE +</a>
                            </div>
                        </div>
                    {/foreach}

                </div>
            {/foreach}
        </div>

        {* Кнопка внизу *}
        {if $button.published}
        <div class="row mb-4">
            <div class="col text-center">
                <div class="overflow-hidden">
                    <a href="{$button.href?:($button.resource|resource:'uri')}" class="btn btn-secondary btn-outline btn-rounded font-weight-bold px-5 py-3 text-3 appear-animation" data-appear-animation="maskUp" data-appear-animation-delay="250">
                        {$button.caption}
                    </a>
                </div>
            </div>
        </div>
        {/if}
        
    </div>
</section>

Теперь менеджер может менять текст «Our Services», добавлять новые иконки или менять порядок блоков местами, а «дорогая» верстка Porto SEO-2 останется в целости и сохранности.

Блок 5: SEO Audit (Форма захвата)

Это темная секция с формой. В реальности форму нужно оживлять через FetchIt или FormIt, но в рамках PageBlocks мы сделаем редактируемыми заголовки и тексты, а саму форму оставим как HTML-каркас (или обертку для вызова сниппета).

Создаем блок «SEO Форма». Чанк: pb.seo_audit.

Поля:

  1. title (Текст) — Заголовок (Get Your Free Instant…).
  2. subtitle_1 (Текст) — Мелкий текст под заголовком.
  3. subtitle_2 (Текст) — Текст под заголовком.

Чанк pb.seo_audit

<section class="section section-height-4 bg-color-quaternary position-relative border-0 pt-5 m-0">
    {* SVG Background *}
    <svg class="custom-section-curved-top-4" width="100%" height="298" xmlns="http://www.w3.org/2000/svg">
        <path id="svg_2" fill="#171940" d="..." /> 
    </svg>

    <div class="container">
        <div class="row justify-content-center mb-3">
            <div class="col-md-8 col-lg-6 text-center">
                <div class="overflow-hidden mb-2">
                    <h2 class="font-weight-bold text-color-light text-7 line-height-2 mb-0 appear-animation" data-appear-animation="maskUp" data-appear-animation-delay="250">
                        {$title}
                    </h2>
                </div>
                <div class="overflow-hidden mb-1">
                    <p class="lead custom-text-color-grey-2 mb-0 appear-animation" data-appear-animation="maskUp" data-appear-animation-delay="400">
                        {$subtitle_1}
                    </p>
                </div>
                <div class="overflow-hidden mb-3">
                    <p class="custom-text-color-grey-2 mb-0 appear-animation" data-appear-animation="maskUp" data-appear-animation-delay="550">
                        {$subtitle_2}
                    </p>
                </div>
            </div>
        </div>
        
        <div class="row">
            <div class="col">
                {* Здесь можно вставить вызов FetchIt или оставить верстку *}
                <form class="custom-form-style-1 custom-form-simple-validation form-errors-light" action="/" method="POST">
                    <div class="row mb-4 appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="700">
                        <div class="form-group col-md-6 pe-md-2">
                            <input type="text" class="form-control" name="url" placeholder="Enter URL" required />
                        </div>
                        <div class="form-group col-md-6 ps-md-2">
                            <input type="email" class="form-control" name="email" placeholder="Enter E-mail Address" required />
                        </div>
                    </div>
                    <div class="row justify-content-center appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="850">
                        <div class="form-group col-auto mb-0">
                            <button type="submit" class="btn btn-gradient btn-rounded font-weight-bold px-5 py-3 text-3">
                                CHECK NOW
                            </button>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
</section>

Форму сделаем в одном из следующих уроков.

Блок 6: Social Proof (Статистика и отзывы)

Блок 6: Social Proof (Статистика и отзывы)

Это «тяжелый» блок, объединяющий три логические части: счетчики (круги слева), текстовое описание с прогресс-барами (справа) и слайдер отзывов (внизу). Чтобы не дробить фон с декоративными кругами, сделаем это одним блоком.

Первым делом как всегда создаем таблицы:

  1. counters (Круглые счетчики), внутри: number (Число), label (Текст), color (Select: bg-color-secondary, bg-color-tertiary, bg-color-quaternary).
  2. bars (Прогресс-бары), внутри: label (Название), percent (Число от 0 до 100).
  3. reviews (Слайды отзывов), внутри: text (Textarea), author (Текст), role (Текст), photo (Изображение).

Далее создаем блок «Статистика и Отзывы», чанк: pb.stats.

Поля:

  1. counters (Таблица) — Круглые счетчики.
  2. title (Текст) — Заголовок справа.
  3. subtitle (Текст) — Надзаголовок.
  4. description (Textarea) — Описание.
  5. bars (Таблица) — Прогресс-бары.
  6. reviews_title (Текст) — Заголовок блока отзывов.
  7. reviews_text (Textarea) — Описание блока отзывов.
  8. reviews (Таблица) — Слайды отзывов.

Чанк pb.seo_stats

<div class="container position-relative pb-lg-5 mb-5">
    
    {* --- Декоративные круги (сокращено) --- *}
    <div class="custom-circle custom-circle-extra-small custom-circle-pos-24 appear-animation" data-appear-animation="expandIn" data-appear-animation-delay="200">
        <div class="custom-bg-color-grey-2 rounded-circle w-100 h-100" data-plugin-float-element data-plugin-options="{ 'startPos': 'bottom', 'speed': 0.3, 'transition': true, 'transitionDuration': 2000 }"></div>
    </div>
    {* ... еще 20 кругов ... *}
    <div class="custom-dots-rect-4" style="background-image: url(assets/img/demos/seo-2/dots-group.png);"></div>

    <div class="row py-5 mt-5 mb-lg-3">
        {* ЛЕВАЯ КОЛОНКА: СЧЕТЧИКИ *}
        <div class="col-lg-6 mb-5 mb-lg-0">
            <div class="custom-circles-group-1 counters pe-5 me-5">
                 {* Еще декоративные круги фона счетчиков... *}
                <div class="custom-circle custom-circle-big custom-circle-pos-31 bg-color-secondary"></div>
                
                {* Вывод динамических счетчиков *}
                {foreach $counters as $idx => $counter}
                    {* Классы circle-1, circle-2 зависят от порядка, можно использовать index+1 *}
                    <div class="circle-{$idx + 1} {$counter.color}">
                        <div class="counter">
                            <strong class="text-color-light text-10 line-height-1" data-to="{$counter.number}" data-append="+">0</strong>
                            <label class="text-color-light text-4 mb-0">{$counter.label}</label>
                        </div>
                    </div>
                {/foreach}
            </div>
        </div>

        {* ПРАВАЯ КОЛОНКА: ТЕКСТ И БАРЫ *}
        <div class="col-lg-6">
            <h2 class="text-color-dark font-weight-semibold text-6 line-height-3 mb-0 pe-5 me-5">{$title}</h2>
            <span class="d-block mb-3 pb-2">{$subtitle}</span>
            <p class="lead font-weight-normal pe-5 mb-4">{$description}</p>
            
            <div class="progress-bars custom-progress-bars-style-1 mt-4">
                {foreach $bars as $bar}
                <div class="progress-label">
                    <span class="d-block text-3 mb-1">{$bar.label}</span>
                </div>
                <div class="progress mb-3">
                    <span class="progress-bar-tooltip">{$bar.percent}%</span>
                    <div class="progress-bar progress-bar-quaternary" data-appear-progress-animation="{$bar.percent}%"></div>
                </div>
                {/foreach}
            </div>
        </div>
    </div>

    <div class="row"><div class="col"><hr></div></div>

    {* НИЖНЯЯ ЧАСТЬ: ОТЗЫВЫ *}
    <div class="row mt-5 mb-5">
        <div class="col-lg-4 pe-lg-0">
            <h2 class="text-color-dark font-weight-semibold text-6 line-height-3 mb-3">{$reviews_title}</h2>
            <p>{$reviews_text}</p>
        </div>
        <div class="col-lg-8 ps-lg-4">
            <div class="owl-carousel custom-carousel-style-1 custom-carousel-dots-style-1" data-plugin-options="{ 'responsive': { '0': { 'items': 1 }, '979': { 'items': 2 } }, 'margin': 0, 'loop': true, 'dots': true, 'nav': false, 'autoplay': true }">
                
                {foreach $reviews as $review}
                <div>
                    <div class="testimonial testimonial-style-3 custom-testimonial-style-1">
                        <blockquote>
                            <p class="mb-0">{$review.text}</p>
                        </blockquote>
                        <div class="testimonial-author">
                            <div class="testimonial-author-thumbnail">
                                <img src="{$review.photo.url}" class="img-fluid rounded-circle" alt="">
                            </div>
                            <p><strong class="font-weight-semibold text-4 mb-1">{$review.author}</strong><span class="text-2">{$review.role}</span></p>
                        </div>
                    </div>
                </div>
                {/foreach}

            </div>
        </div>
    </div>
</div>

Блок 7: Pricing Plans (Тарифы)

Блок 7 Pricing Plans (Тарифы)

Здесь важно реализовать логику выделения «Популярного» тарифа (Professional Plan), так как у него другая верстка (цвета, отступы). Мы решим это простым переключателем Yes/No.

Создаем сначала таблицу plans (Карточки тарифов), со следующими полями:

  • title (Текст).
  • desc (Текст) — Короткое описание под заголовком.
  • is_featured (List Yes/No) — Это популярный тариф?
  • features (RichText) — Список <ul><li>...</li></ul>.
  • button_text (Текст).
  • link (Текст).

Создаем блок «Тарифы». Чанк: pb.pricing.

Поля:

  1. title (Текст) — Заголовок (Pricing Plans).
  2. subtitle (Текст) — Надзаголовок (PLANS TABLE).
  3. description (Textarea) — Описание.
  4. plans (Таблица) — Карточки тарифов.

Чанк pb.seo_pricing

<section class="section position-relative custom-bg-color-light-2 border-0 pt-4 m-0">
    <svg class="custom-section-curved-top-5" width="100%" height="284" xmlns="http://www.w3.org/2000/svg">
         <path id="svg_2" fill="#eff1f3" d="..." />
    </svg>
    
    <div class="container position-relative z-index-1 pb-lg-4 mb-lg-5">
        {* Декоративные круги... *}

        <div class="row justify-content-center mb-3">
            <div class="col-md-10 col-lg-8 text-center">
                <div class="overflow-hidden">
                    <span class="d-block appear-animation" data-appear-animation="maskUp" data-appear-animation-delay="250">{$subtitle}</span>
                </div>
                <div class="overflow-hidden mb-2">
                    <h2 class="font-weight-bold text-color-quaternary text-7 mb-0 appear-animation" data-appear-animation="maskUp" data-appear-animation-delay="400">{$title}</h2>
                </div>
                <p class="appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="550">{$description}</p>
            </div>
        </div>

        <div class="row pricing-table custom-pricing-table-style-1 justify-content-center mb-4">
            
            {foreach $plans as $plan}
                <div class="col-md-8 col-lg-4">
                    {if $plan.is_featured == 1}
                        {* --- FEATURED PLAN (Центральный/Выделенный) --- *}
                        <div class="plan plan-featured bg-color-quaternary text-center appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="400">
                            <span class="plan-popular-tag text-color-light font-weight-bold">POPULAR</span>
                            <div class="plan-header bg-color-quaternary pt-5 px-4">
                                <h4 class="text-color-light font-weight-bold text-5 mt-4 mb-2">{$plan.title}</h4>
                                <p class="text-color-light font-weight-light opacity-6 px-2 mb-0">{$plan.desc}</p>
                            </div>
                            {* Для RichText списка нужно стилизовать li, можно добавить класс-обертку *}
                            <div class="plan-features px-4 text-color-light">
                                {$plan.features}
                            </div>
                            <div class="plan-footer pt-3 pb-5">
                                <a href="{$plan.link}" class="btn btn-light-2 btn-outline btn-rounded font-weight-semibold px-5 py-3 text-3 mb-4">{$plan.button_text}</a>
                            </div>
                        </div>
                    {else}
                        {* --- OBYCHNYE PLANS --- *}
                        <div class="plan text-center appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="550">
                            {* Точки добавляем только первому тарифу для красоты, или по условию *}
                            {if $plan@first}
                                <div class="custom-dots-rect-5" style="background-image: url(assets/img/demos/seo-2/dots-group.png);"></div>
                            {/if}
                            
                            <div class="plan-header bg-color-light pt-5 px-4">
                                <h4 class="text-color-dark font-weight-bold text-color-quaternary text-5 mt-4 mb-2">{$plan.title}</h4>
                                <p class="px-2 mb-0">{$plan.desc}</p>
                            </div>
                            <div class="plan-features px-4">
                                {$plan.features}
                            </div>
                            <div class="plan-footer pt-3 pb-5">
                                <a href="{$plan.link}" class="btn btn-secondary btn-outline btn-rounded font-weight-semibold px-5 py-3 text-3 mb-4">{$plan.button_text}</a>
                            </div>
                        </div>
                    {/if}
                </div>
            {/foreach}
            
        </div>
    </div>
</section>

Отлично, завершаем нашу практическую работу 8-м блоком: «Recent Blog Posts».

Этот блок интересен тем, что мы не будем заставлять менеджера вручную набивать заголовки статей. Вместо этого мы используем мощь PageBlocks как обертки для вызова сниппета pdoResources. Менеджер просто выберет заголовок секции, а статьи подтянутся автоматически из раздела «Блог».

Блок 8: Recent Blog Posts (Последние новости)

Здесь мы комбинируем статический контент (заголовок, описание, кнопка «Все новости») с динамическим выводом последних ресурсов.

Создаем блок «Новости на главной». Чанк: pb.blog.

Вкладка «Поля»:

  1. title (Текст) — Заголовок (Recent Blog Posts).
  2. subtitle (Текст) — Надзаголовок (NEWS AND INFO).
  3. description (Textarea) — Описание под заголовком.
  4. parent (Resource List) — ID родителя, откуда брать новости (например, раздел Блог).
  5. limit (Number) — Сколько новостей выводить (по умолчанию 2).
  6. button (Кнопка) — Текст кнопки внизу (VIEW BLOG).

Чанк pb.seo_blog

<section class="our-blog section bg-color-light position-relative border-0 m-0">
    {* SVG Background *}
    <svg class="custom-section-curved-top-6" width="100%" height="600px" xmlns="http://www.w3.org/2000/svg">
         <path id="svg_1" fill="#f7f8f9" d="..." />
         <path id="svg_2" fill="#fbfcfc" d="..." />
         <path id="svg_3" fill="#ffffff" d="..." />
    </svg>

    <div class="container position-relative z-index-1 pb-5 mb-lg-5">
        {* ЗАГОЛОВОК СЕКЦИИ *}
        <div class="row justify-content-center mb-4">
            <div class="col-8 text-center">
                <span>{$subtitle}</span>
                <h2 class="font-weight-bold line-height-3 text-7 pb-2 mb-2">{$title}</h2>
                <p>{$description}</p>
            </div>
        </div>

        {* СПИСОК СТАТЕЙ (Вызов pdoResources) *}
        <div class="row mb-3 pb-5">
            <div class="col">
                <div class="row">
                    {'!pdoResources' | snippet : [
                        'parents' => $parent ?: 0,
                        'limit' => $limit ?: 2,
                        'tpl' => 'tpl.seo_blog_post',
                        'includeTVs' => 'image',
                        'tvPrefix' => '',
                        'sortby' => 'publishedon',
                        'sortdir' => 'DESC'
                    ]}
                </div>
            </div>
        </div>

        {* КНОПКА ВНИЗУ *}
        {if $button.published}
        <div class="row justify-content-center mb-5">
            <div class="col-auto">
                <a href="{$button.href?:($button.resource|resource:'uri')}" class="btn btn-gradient btn-rounded font-weight-semibold px-5 py-3 text-3">
                    {$button.caption}
                </a>
            </div>
        </div>
        {/if}

        {* Декоративные круги... *}
        <div class="custom-circle custom-circle-big custom-circle-pos-47 appear-animation" data-appear-animation="expandInWithBlur" data-appear-animation-delay="200" data-appear-animation-duration="2s">
            <div class="bg-color-quaternary rounded-circle w-100 h-100" data-plugin-float-element data-plugin-options="{ 'startPos': 'bottom', 'speed': 1, 'transition': true, 'transitionDuration': 3000 }"></div>
        </div>
        {* ... остальные круги ... *}
    </div>
</section>

Мы используем pdoResources для выборки статей. Обратите внимание, что мы передаем ID родителя из поля {$parent}.

Чанк для одной статьи tpl.seo_blog_post

Создаем отдельный обычный чанк (не в PageBlocks, а в Элементах), который будет оформлять каждую статью.

<div class="col-lg-6 mb-4 mb-lg-0">
    <article>
        <div class="card border-0 border-radius-0 box-shadow-1">
            <div class="card-body p-4 z-index-1">
                <a href="{$uri}">
                    {* Выводим картинку из TV или заглушку *}
                    <img class="card-img-top border-radius-0" src="{$image ?: 'assets/img/demos/seo-2/blog/blog-1.jpg'}" alt="{$pagetitle}" style="height: 240px; object-fit: cover;">
                </a>
                
                <p class="text-uppercase text-1 mb-3 pt-1 text-color-default">
                    <time pubdate datetime="{$publishedon | date : 'Y-m-d'}">
                        {$publishedon | date : 'd M Y'}
                    </time> 
                    <span class="opacity-3 d-inline-block px-2">|</span> 
                    {$createdby | user : 'fullname'}
                </p>

                <div class="card-body p-0">
                    <h4 class="card-title mb-3 text-5 font-weight-semibold">
                        <a class="text-color-dark" href="{$uri}">{$pagetitle}</a>
                    </h4>
                    <p class="card-text mb-3">{$introtext | truncate : 100}</p>
                    <a href="{$uri}" class="font-weight-bold text-uppercase text-decoration-none d-block mt-3">Read More +</a>
                </div>
            </div>
        </div>
    </article>
</div>

Как это видит менеджер: заполнение контентом

После того как вы (как разработчик) создали все конфигурации и настроили чанки, начинается магия для контент-менеджера или клиента.

  1. Зайдите в ресурс «Главная страница».
  2. Перейдите на вкладку PageBlocks.
  3. Нажмите кнопку «Добавить блок». Вы увидите список наших 8 красивых блоков.
    Добавление блоков
  4. Добавляйте их в нужном порядке: сначала Hero, потом Сетку преимуществ, затем Тарифы и т.д.
  5. Заполняйте поля: пишите заголовки, загружайте картинки, добавляйте пункты в списки преимуществ.

Вы можете менять блоки местами простым перетаскиванием (Drag & Drop). Хотите, чтобы «Отзывы» были выше «Тарифов»? Просто перетащите их мышкой и сохраните. Верстка при этом не сломается, так как каждый блок изолирован в своем чанке.

Заключение

Мы проделали большую работу. Из статичного HTML-файла, в котором страшно было менять даже запятую, мы сделали мощный конструктор лендингов.

Что мы получили в итоге:

  • Гибкость: клиент сам управляет структурой страницы.
  • Чистоту кода: нет дублирования, всё разложено по полочкам (чанкам).
  • Сохранность дизайна: сложная SVG-графика и скрипты анимации спрятаны «под капот», клиент редактирует только смысловой контент.

Используя связку MODX 3 + PageBlocks Free + Fenom, вы можете собирать сайты любой сложности быстрее, чем на Tilda, но с полным контролем над кодом и без ежемесячных платежей.

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

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