Загрузка
Все статьи

WordPress Gutenberg: custom-блоки без JavaScript — как создать блок через PHP

Все гайды по Gutenberg-блокам заставляют писать React и собирать через wp-scripts. Но что если вы backend-разработчик и не хотите тащить node_modules в проект? В WordPress 5.9+ появилась возможность регистрировать серверные блоки полностью на PHP. Разбираю этот подход.

Привет! Меня зовут Артём, я бэкенд-разработчик. Когда в WordPress 5.0 появился Gutenberg, я был в ужасе — «что значит, мне нужен React для создания блока в WordPress?!». Пять лет спустя ситуация улучшилась, но большинство туториалов всё ещё начинают с npx @wordpress/create-block и требуют настройку сборки через webpack.

Однако в WordPress 5.9 появилась возможность создавать так называемые «server-side blocks» — блоки, которые полностью рендерятся на PHP, без единой строки JavaScript. Для backend-разработчика это настоящий глоток свежего воздуха.

Что такое серверный блок?

Серверный блок (Server-Side Rendered Block) — это блок, чей HTML генерируется на сервере при сохранении поста и при рендере страницы. В редакторе он отображается как статичный HTML (не интерактивный), а в базе данных хранится в виде сериализованного block comment.

Это значит: вы пишете PHP-функцию, которая принимает атрибуты блока и возвращает HTML. WordPress оборачивает результат в <!-- wp:my-plugin/awesome-block --> и сохраняет в post_content.

Минимальный пример

Вот полный код серверного блока, который выводит карточку с заголовком и описанием:

// Регистрация блока
add_action('init', function() {
    register_block_type('my-plugin/info-card', [
        'api_version'     => 2,
        'attributes'      => [
            'title' => [
                'type'    => 'string',
                'default' => 'Информационная карточка',
            ],
            'description' => [
                'type'    => 'string',
                'default' => '',
            ],
            'style' => [
                'type'    => 'string',
                'default' => 'default',
            ],
        ],
        'render_callback' => 'render_info_card_block',
    ]);
});

function render_info_card_block($attributes) {
    $title = esc_html($attributes['title']);
    $description = esc_html($attributes['description']);
    $style = sanitize_html_class($attributes['style']);
    
    ob_start();
    ?>
    <div class="info-card info-card--<?php echo $style; ?>">
        <div class="info-card__header">
            <h3><?php echo $title; ?></h3>
        </div>
        <div class="info-card__body">
            <p><?php echo $description; ?></p>
        </div>
    </div>
    <?php
    return ob_get_clean();
}

Всё. Без webpack, без React, без package.json. Один вызов register_block_type с render_callback, и блок работает.

Как это выглядит в редакторе?

Когда пользователь добавляет ваш серверный блок в редакторе, Gutenberg вызывает render_callback через REST API (эндпоинт /wp-json/wp/v2/block-renderer/my-plugin/info-card). Ответ — HTML, который отображается в редакторе как «превью» блока. Пользователь может менять атрибуты через инспектор (sidebar), и при каждом изменении Gutenberg делает новый запрос к REST API.

Это означает, что render_callback выполняется при каждом изменении атрибутов в редакторе. Если ваш callback делает тяжёлые запросы к БД или внешним API, редактор начнёт тормозить. Решение — кэшировать результат:

function render_info_card_block($attributes) {
    $cache_key = 'info_card_' . md5(serialize($attributes));
    $cached = get_transient($cache_key);
    
    if ($cached !== false) {
        return $cached;
    }
    
    // Тяжёлая логика
    $data = fetch_expensive_data($attributes);
    
    ob_start();
    // ... рендеринг ...
    $html = ob_get_clean();
    
    set_transient($cache_key, $html, HOUR_IN_SECONDS);
    return $html;
}

Динамические данные: блок с выбором из БД

Один из реальных кейсов: блок «Последние отзывы», который выводит 3 последних одобренных отзыва из кастомного post type. Мы используем rest_api_init для добавления опций в инспектор блока:

register_block_type('my-plugin/recent-reviews', [
    'api_version'     => 2,
    'attributes'      => [
        'count' => [
            'type'    => 'number',
            'default' => 3,
            'minimum' => 1,
            'maximum' => 20,
        ],
        'category' => [
            'type'    => 'string',
            'default' => 'all',
        ],
    ],
    'render_callback' => 'render_recent_reviews_block',
]);

function render_recent_reviews_block($attributes) {
    $count = (int) $attributes['count'];
    $category = sanitize_key($attributes['category']);
    
    $args = [
        'post_type'      => 'review',
        'posts_per_page' => $count,
        'post_status'    => 'publish',
    ];
    
    if ($category !== 'all') {
        $args['tax_query'] = [[
            'taxonomy' => 'review_category',
            'field'    => 'slug',
            'terms'    => $category,
        ]];
    }
    
    $reviews = get_posts($args);
    
    if (empty($reviews)) {
        return '<p>' . esc_html__('Отзывов пока нет.', 'my-plugin') . '</p>';
    }
    
    ob_start();
    echo '<div class="recent-reviews">';
    foreach ($reviews as $review) {
        echo '<div class="review-card">';
        echo '<h4>' . esc_html(get_the_title($review)) . '</h4>';
        echo '<p>' . esc_html(wp_trim_words($review->post_content, 30)) . '</p>';
        echo '<span class="review-author">' . esc_html(get_post_meta($review->ID, 'author_name', true)) . '</span>';
        echo '</div>';
    }
    echo '</div>';
    
    return ob_get_clean();
}

Обратите внимание: мы используем get_posts вместо WP_Query. Разница в производительности небольшая, но get_posts не меняет глобальные переменные WordPress ($wp_query, $post), что делает его безопаснее для использования внутри render_callback.

Стили: enqueue_block_style

WordPress 5.9 добавил функцию enqueue_block_style, которая подключает CSS только когда блок реально используется на странице. Это лучше, чем enqueue_style на wp_enqueue_scripts, который загружает стили на всех страницах.

add_action('init', function() {
    register_block_type('my-plugin/info-card', [ /* ... */ ]);
    
    enqueue_block_style('my-plugin/info-card', 
        plugin_dir_url(__FILE__) . 'blocks/info-card/style.css'
    );
});

WordPress автоматически определяет наличие блока в post_content и подключает CSS только для страниц, где блок используется. На страницах без блока CSS не загружается — это заметно ускоряет фронтенд при большом количестве кастомных блоков.

Серверные блоки — не универсальное решение. Для сложных интерактивных блоков (слайдеры, формы, интерактивные графики) всё равно нужен React. Но для контентных блоков — карточки, списки, цитаты, баннеры — PHP-подход идеален. Быстро, чисто, без сборки.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

WordPress: REST API и кастомные эндпоинты без плагинов

Для интеграции WordPress с мобильным приложением понадобилось создать кастомные REST-эндпоинты. Плагины типа ACF to REST API не подходили — нужна была специфическая логика: фильтрация по пользовательским полям, пагинация, кэширование. Сделали всё через register_rest_route и шорткоды — без единого плагина.

26 мая 2026 8 мин

WordPress REST API: где пропадает кастомное поле в сообщении о заказе WooCommerce?

Добавили кастомное поле в заказ WooCommerce через REST API — поле в админке видно, а в письме покупателя его нет. Знакомая ситуация? Разбираю, почему WooCommerce email-шаблоны игнорируют произвольные поля заказа, и показываю три способа заставить их отображаться без копипаста шаблонов.

6 мая 2026 5 мин

Сложности с разделами каталога в Битрикс24 через REST API: игра с деревом

Обновляете товар через API, передаете SECTION_ID, но он не появляется в нужной папке. В этой статье я объясняю разницу между простым SECTION_ID и сложным деревом инфоблоков, как правильно выстроить иерархию и почему метод crm.product.update не умеет создавать папки.

5 июля 2026 1 мин