pbStudio: Создаём сайт с PageBlocks – настройка и главная страница

В этом уроке мы установим MODX, настроим необходимые компоненты и системные параметры для работы с PageBlocks. Затем создадим главную страницу и добавим на неё два блока. Это часть серии уроков, в которых мы пошагово создадим полноценный сайт для студии с использованием PageBlocks.



Демо сайта.

Навигация по урокам:

Настройка


1. Устанавливаем MODX — следуйте официальной инструкции.

2. Устанавливаем компоненты:
  • PageBlocks – основной компонент (без него никуда)
  • ColorPicker – инструмент для выбора цветов
  • pThumb – оптимизация изображений
  • TinyMCE Rich Text Editor –мощный текстовый редактор
3. Настраиваем системные параметры:
  • pageblocks_development_mode – Developer (режим разработчика)
  • pageblocks_routing – Full API (полный контроль маршрутизации)
  • pageblocks_parser – pbFenom (шаблонизатор Fenom)
4. Настройка сервера:
  • Для Apache: переименуйте ht.access в .htaccess
  • Для Nginx: настройте соответствующие правила
5. Опционально: в разделе «Типы содержимого» (Содержимое → Типы содержимого) можно удалить расширение .html для чистых URL

На этом базовая настройка завершена.

Главная страница


1. Создаем наш первый маршрут в core/App/routes/web.php:
use PageBlocks\App\Http\Controllers\ResourceController;

Route::get('/{alias?}', [ResourceController::class, 'index']);
Этот маршрут обрабатывает:
  • / — главная страница
  • some_alias — другие страницы.
2. Создаем контроллер ResourceController (core/App/Http/Controllers/ResourceController.php):
<?php
namespace PageBlocks\App\Http\Controllers;

class ResourceController extends Controller
{
    public $classKey = \modResource::class;

    public function index(string $alias = '')
    {
        // Условия для поиска ресурса
        $where = [
            'published' => 1, // ресурс должен быть опубликованный
            'deleted' => 0, // не удален
        ];
        
        // Если $alias пустой, значит это главная страница, иначе ищем ресурс по алиасу.
        if (empty($alias)) {
            $where['id'] = $this->modx->getOption('site_start', null, 1, true);
        } else {
            $where['alias'] = $alias;
        }
        
        // Если ресурс не найден то отдаем 404 ошибку
        if (!$resource = $this->modx->getObject($this->classKey, $where)) {
            abort();
        }
        
        // Можем сделать просто так:
        // $this->modx->resource = $resource; // для того чтобы в шаблоне и у сниппетов был доступ к ресурсу
        
        // но мы сделаем так, для того, чтобы поля созданные через PageBlocks тоже учитывались
        $this->modx->resource = updateResource($resource);
        
        // Возвращаем базовый шаблон
        return response()->view('templates/base');
    }
}

3. Создаем базовый шаблон core/App/elements/templates/base.tpl:
<!doctype html>
<html lang="en">
<head>
    {block 'head'}
        {insert 'chunks/head.tpl'}
    {/block}
</head>
<body>
    {block 'header'}
        {insert 'chunks/header.tpl'}
    {/block}
    
    {*Выводим блоки*}
    {block 'content'}
	<div class="container">
            {'!pbBlocks'|snippet: [
                'fileElements' => 1,
            ]}
	</div>
    {/block}

    {block 'footer'}
        {insert 'chunks/footer.tpl'}
    {/block}
    
    {block 'modal'}
        {insert 'chunks/modal.tpl'}
    {/block}
</ body>
</html>
Это единственный шаблон сайта, который будет выводить уникальные блоки для каждой страницы.

Примеры готовых чанков:
4. Добавляем блоки в метод getBlocks в core/App/Models/Resource.php.

Блок Slider

Block::make('Slider') // название блока
    // chunk можем не указывать, ведь make задает не только название блока, 
    // но и название чанка в нижнем регистре.
    // ->chunk('slider') // явно указываем имя чанка
    ->fields([
        Field::make('Slider')
        ->type('table')
        ->fields([
            Field::make('Background')
                ->type('image')
                ->sourcePath('/assets/images/')
                ->required(),
            
            Field::make('Title')
                ->required(),
            
            Field::make('Description')
                ->type('textarea'),
        ])
        ->columns([
            Column::make('Background')
                ->width(100)
                ->render('image'),
            
            Column::make('Title')
        ])
    ])

В результате получаем блок с таблицей:


и форму для слайда:


Теперь для отображения блока нам нужно создать чанк core/App/elements/chunks/slider.tpl:
Верстку возьмем отсюда
<div id="pbSlider" class="carousel slide">
    <div class="carousel-indicators">
        {foreach $slider as $idx => $slide}
            <button type="button"
                    data-bs-target="#pbSlider"
                    data-bs-slide-to="{$idx}"
                    class="{!$idx ? 'active' : ''}"
                    aria-current="{!$idx ? 'true' : 'false'}"
                    aria-label="{$slide.title}"></button>
        {/foreach}
    </div>
    <div class="carousel-inner">
        {foreach $slider as $idx => $slide}
            <div class="carousel-item{!$idx ? ' active' : ''}">
                <img src="{$slide.background.url|pthumb:'w=1296&h=500&zc=1&f=webp'}"
                     class="d-block w-100 img-fluid object-fit-cover"
                     width="1296"
                     height="500"
                     style="min-height: 240px;"
                     alt="{$slide.title}">
                <div class="carousel-caption mx-auto top-50 start-50 translate-middle w-100" style="max-width:650px;">
                    <h1>{$slide.title}</h1>
                    {if $slide.description}
                        <p class="d-none d-md-block">{$slide.description}</p>
                    {/if}
                </div>
            </div>
        {/foreach}
    </div>
    <button class="carousel-control-prev" type="button" data-bs-target="#pbSlider" data-bs-slide="prev">
        <span class="carousel-control-prev-icon" aria-hidden="true"></span>
        <span class="visually-hidden">Previous</span>
    </button>
    <button class="carousel-control-next" type="button" data-bs-target="#pbSlider" data-bs-slide="next">
        <span class="carousel-control-next-icon" aria-hidden="true"></span>
        <span class="visually-hidden">Next</span>
    </button>
</div>

Вот и все, наш первый блок готов. Результат можно увидеть на сайте или на первой картинке в статье.

Блок Services

Этот блок немного сложнее, так как нам нужно для каждого элемента задать:
  • Фон блока
  • Цвет текста
  • Расположение текста
  • Ссылку
  • Картинку


Настройка:
Block::make('Services')
    ->fields([
        // Создаем поле с типом таблица
        Field::make('Services')
            ->type('table')
            ->fields([
                // Фон блока
                Field::make('Background')
                    ->type('colorpicker')
                    ->width(50)
                    ->required(),
                
                // Цвет текста
                Field::make('Color')
                    ->type('colorpicker')
                    ->width(50)
                    ->default('#ffffff')
                    ->required(),
                
                // Тип блока: Image или Text. Определяет набор отображаемых полей.
                Field::make('Type')
                    ->type('select')
                    ->options([
                        'image' => 'Image',
                        'text' => 'Text',
                    ])
                    ->required(),
                
                // Картинка (только для типа Image)
                Field::make('Image')
                    ->type('image')
                    ->sourcePath('/assets/images/')
                    ->required()
                    ->hidden('type', '!=', 'image'),
                
                // Заголовок (только для типа Text)
                Field::make('Title')
                    ->required()
                    ->hidden('type', '!=', 'text'),
                
                // Позиция текста (только для типа Text)
                Field::make('Position')
                    ->type('select')
                    ->values([
                        '' => 'Center',
                        'mt-auto' => 'Bottom'
                    ])
                    ->required()
                    ->hidden('type', '!=', 'text'),
                
                // Описание (только для типа Text)
                Field::make('Description')
                    ->type('richtext')
                    ->hidden('type', '!=', 'text'),
                
                // ID ресурса для ссылки, прячем поле, если тип не выбран
                Field::make('resource_id')
                    ->label('Resource')
                    ->type('resourcelist')
                    ->hidden('type', '=', ''),
            ])
            // Настройка колонок для таблицы
            ->columns([
                Column::make('Background')
                    ->render('color'),
                
                Column::make('Color')
                    ->width(100)
                    ->render('color'),
                
                Column::make('Title')
                    ->default('---')
            ])
    ]),

Описание полей:
  • Background: Выбор цвета фона блока с помощью colorpicker. Обязательное поле.
  • Color: Выбор цвета текста с помощью colorpicker. По умолчанию #ffffff (белый). Обязательное поле.
  • Type: Выбор типа блока:
    • Image: Отображает только изображение.
    • Text: Отображает заголовок, описание и позволяет выбрать позицию текста. Обязательное поле. В зависимости от выбранного типа, отображаются соответствующие поля.
  • Image: Загрузка изображения. Отображается только если Type равен Image. Указывает путь к папке с изображениями (sourcePath).
  • Title: Заголовок текста. Отображается только если Type равен Text. Обязательное поле.
  • Position: Выбор позиции текста (центр или низ). Используется для вертикального выравнивания текста. Отображается только если Type равен Text. Обязательное поле.
  • Description: Описание блока. Отображается только если Type равен Text. Использует редактор richtext для форматирования текста.
  • Resource (resource_id): Выбор ресурса (страницы) для создания ссылки. Отображается если выбран какой-то Type.
Результат:


Давайте улучшим конфигурацию колонок таблицы для блока Services:
->columns([
    Column::make('Colors')
        // Группируем фон и цвет текста в одну колонку
        ->group([
            Column::make('Background')
                ->render('color')
                ->withLabel(), // Добавляем заголовок, который равен имени колонки (Background)
            
            Column::make('Color')
                ->render('color')
                ->withLabel('Color text'), // Добавляем кастомный заголовок
        ]),
    Column::make('Title / Image')
        // Добавляем условие, показываем заголовок или картинку
        ->renderIf([
            Column::make('Title'),
            Column::make('Image')->render('image')
        ])
    ])

Результат:


Создаем чанк coreApp/elements/chunks/services.tpl
<section class="services d-flex flex-wrap">
    {foreach $services as $item}
        <div class="service-item col-12 col-xl-6"
             style="background-color: {$item.background};color:{$item.color?:'#fff'}">
            {if $item.type === 'text'}
                <h2 class="{$item.position}">{$item.title}</h2>
                {if $item.description}
                    <div class="mb-0 {$item.position}">{$item.description}</div>
                {/if}
            {else}
                <img loading="lazy"
                     src="{$item.image.url}"
                     class="img-fluid mx-auto"
                     width="236"
                     height="236"
                     alt="{$item.image.title}">
            {/if}
            {if $item.resource_id}
                <a href="{$item.resource_id|url}"
                   class="service-link text-decoration-none d-flex align-items-center gap-3 position-absolute bottom-0 end-0">
                    {if $item.type === 'image'}
                        <span>{$item.resource_id|resource:'pagetitle'}</span>
                    {/if}
                    <svg fill="{$item.color}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
                        <!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
                        <path d="M502.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L402.7 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l370.7 0-73.4 73.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l128-128z"/>
                    </svg>
                </a>
            {/if}
        </div>
    {/foreach}
</section>

Вот и всё. Главная страница готова.
Все, кто дочитал до конца, ставьте лайк! Встретимся в следующем уроке, где продолжим создавать наш сайт.
Aleksandr Huz
16 апреля 2025, 12:45
modx.pro
3
3 311
+11
Поблагодарить автора Отправить деньги

Комментарии: 6

Сергей Сергеевич
16 апреля 2025, 19:53
+2
Инструкция! Вот чего не хватало, спасибо!!!
Зачем нужен пункт 1 и 2? Ведь это решается стандартными полями ресурса в MODX?
    Aleksandr Huz
    16 апреля 2025, 21:10
    0
    Зачем нужен пункт 1 и 2? Ведь это решается стандартными полями ресурса в MODX?
    Для полного контроля над страницами и логикой отображения.
    Александр Туниеков
    17 апреля 2025, 20:57
    0
    Block::make('Services')
        ->fields([
            // Создаем поле с типом таблица
            Field::make('Services')
                ->type('table')
                ->fields([
    Учить еще один язык програмирования, который типа облегчает контроль над html и css. Как говориться плох тот разработчик который не написал свой фрейворк.
    PageBlocks может и не плох но как же уже достал это зоопарк фреймворков :-)
      Aleksandr Huz
      17 апреля 2025, 22:00
      +1
      PageBlocks может и не плох но как же уже достал это зоопарк фреймворков :-)
      PageBlocks имеет 2 режима разработки:
      • Режим менеджера — можно делать тоже самое, только клацать в админке
      • Режим разработчика — управлять всем через файлы, но нужно немного поучиться, зато получаем чистый и управляемый код, который можно хранить на гитхабе
      Как говорится, плох тот разработчик, который не учится новому!
        Александр Туниеков
        18 апреля 2025, 01:38
        0
        Как говорится, плох тот разработчик, который не учится новому!
        Новое так быстро выходит что не успеваешь написать программу как подоспел новый фреймворк и надо переписывать прогу уже с новым фреймворком.
        П.С. Хочестя в тикеты при сохранении комментария добавить кнопку «проверить на ошибки» и при нажатии на нее проверить комментарий на ошибки в нейросети сохранить проверенный вариант. Но пока еще не настолько хочется чтоб взять и сделать :-)
          Aleksandr Huz
          18 апреля 2025, 08:40
          +1
          Хочестя в тикеты при сохранении комментария добавить кнопку «проверить на ошибки» и при нажатии на нее проверить комментарий на ошибки в нейросети сохранить проверенный вариант.
          Хорошая идея. Запишу себе и реализую, когда буду делать уроки по созданию блога на PageBlocks.
      Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
      6