Свои блоки Gutenberg: как создать блок для редактора с нуля

Свои блоки Gutenberg: как создать блок для редактора с нуля

Коротко: собственный блок Gutenberg — это JavaScript-регистрация через wp.blocks.registerBlockType() с функциями edit (что видит редактор) и save (что видит посетитель) плюс PHP-сторона: подключение файла через enqueue_block_editor_assets или современный вариант — папка блока с block.json и register_block_type(). Простейший блок пишется на чистом ES5 без Node.js и запускается за 15–20 минут; для сложных блоков нужна сборка через @wordpress/scripts (npm start / npm run build), а если содержимое должно быть свежим при каждом показе — динамический рендеринг через render.php. Ниже — рабочий код всех трёх вариантов по шагам.

Готовых блоков в редакторе десятки: абзацы, изображения, колонки, кнопки. Но как только возникает задача «сделать заметку с фирменным оформлением», «карточку сотрудника с фото и должностью» или «аккордеон с собственными полями», начинается компромисс: то стили не подобрать, то разметка не та, то плагин тянет за собой полмегабайта лишнего кода. Собственный блок решает проблему радикально — вы контролируете и разметку, и стили, и поведение.

Хорошая новость для новичка: минимальный блок Gutenberg — это один JavaScript-файл на 40 строк и пара строк PHP. Не нужно знать React на продвинутом уровне, не обязателен Node.js. В этом гайде разберём путь от простого к сложному: сначала первый блок без сборки, затем современный каркас с block.json, панель настроек, динамический блок и типичные ошибки, на которых спотыкаются все начинающие.

Из чего состоит свой блок: три части, которые важно понять сразу

Любой блок Gutenberg собирается из трёх частей:

  1. JavaScript-регистрация. Функция wp.blocks.registerBlockType() сообщает редактору: «есть блок с таким-то именем, вот его иконка, вот как его редактировать и как сохранять». Внутри неё — два метода: edit() возвращает интерфейс для редактора, save() — HTML, который сохранится в контент страницы.
  2. Атрибуты (attributes). Это «память» блока — заголовок, текст, цвет, URL картинки. Редактор хранит атрибуты в объекте props.attributes, а изменяет их через props.setAttributes(). Именно атрибуты отличают один блок от другого.
  3. PHP-сторона. JavaScript нужно физически подключить в редактор (хук enqueue_block_editor_assets) либо зарегистрировать блок на сервере функцией register_block_type() с описанием в block.json. Серверная часть также отвечает за динамический рендеринг через render.php.

Ключевая мысль, которая сэкономит вам часы отладки: редактор и фронтенд — две разные картины мира. То, что вы вернули из edit(), живёт только в админке. То, что вернул save(), сохраняется в базу как HTML записи и показывается посетителям. Если эти две функции возвращает разную разметку — WordPress покажет ошибку «блок содержит недействительный контент».

Куда положить код блока: плагин или дочерняя тема

Технически код блока можно подключить и из темы, и из плагина. Практическое правило:

  • Плагин — если блок про контент и функциональность (заметки, карточки, кнопки, аккордеоны). Блок переживёт смену темы — как переживают её записи и страницы.
  • Тема — если блок жёстко связан с дизайном конкретной темы. В этом случае правильнее завести дочернюю тему WordPress, чтобы обновление родительской темы не стёрло ваш код.

Для обучения создадим плагин. В wp-content/plugins/ сделайте папку ds-custom-blocks, а в ней файл ds-custom-blocks.php с шапкой плагина:

<?php
/**
 * Plugin Name: DS Custom Blocks
 * Description: Собственные блоки Gutenberg: заметка с оформлением.
 * Version: 1.0.0
 * Text Domain: ds-custom-blocks
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

Пока это пустой, но уже рабочий плагин — он появится в «Плагины → Все плагины» и его можно активировать. Внутри плагина будет расти структура:

ds-custom-blocks/
├── ds-custom-blocks.php      ← шапка + PHP-регистрация блоков
└── blocks/
    └── notice/               ← наш блок «Заметка»
        ├── notice.js         ← ES5-версия (без сборки)
        ├── block.json        ← современное описание блока
        ├── render.php        ← серверный шаблон динамического блока
        ├── src/              ← исходники для сборки (ES6/JSX, CSS)
        └── build/            ← результат сборки (создаст npm)

Не пугайтесь количества файлов: для первого блока понадобятся только ds-custom-blocks.php и notice.js. Остальное появится по мере усложнения.

Первый блок без сборки: ES5 и wp.element.createElement

Классический «порог входа» в Gutenberg — сборка через npm. Обойдём её: WordPress сам предоставляет все библиотеки, а блок можно написать на чистом ES5, который поймёт любой браузер. Создайте файл blocks/notice/notice.js:

(function (wp) {
    var el = wp.element.createElement;
    var registerBlockType = wp.blocks.registerBlockType;
    var RichText = wp.blockEditor.RichText;

    registerBlockType('ds/notice', {
        title: 'Заметка (delai-sait)',
        description: 'Цветная заметка с заголовком и текстом',
        icon: 'marker',
        category: 'widgets',
        keywords: ['заметка', 'notice'],

        attributes: {
            heading: { type: 'string', source: 'html', selector: '.ds-notice__heading' },
            text:    { type: 'string', source: 'html', selector: '.ds-notice__text' }
        },

        edit: function (props) {
            return el('div', { className: props.className },
                el(RichText, {
                    tagName: 'p',
                    className: 'ds-notice__heading',
                    value: props.attributes.heading,
                    onChange: function (value) { props.setAttributes({ heading: value }); },
                    placeholder: 'Заголовок заметки…'
                }),
                el(RichText, {
                    tagName: 'p',
                    className: 'ds-notice__text',
                    value: props.attributes.text,
                    onChange: function (value) { props.setAttributes({ text: value }); },
                    placeholder: 'Текст заметки…'
                })
            );
        },

        save: function (props) {
            return el('div', { className: 'ds-notice' },
                el(RichText.Content, {
                    tagName: 'p',
                    className: 'ds-notice__heading',
                    value: props.attributes.heading
                }),
                el(RichText.Content, {
                    tagName: 'p',
                    className: 'ds-notice__text',
                    value: props.attributes.text
                })
            );
        }
    });
})(window.wp);

Что здесь происходит:

  • el() — это псевдоним wp.element.createElement, функция создания элементов. Первый аргумент — тег или компонент, второй — свойства, дальше — вложенные элементы.
  • attributes с параметрами source: 'html' и selector говорят редактору: «вытащи значение атрибута из этого HTML-элемента сохранённой разметки». Благодаря этому старый контент не ломается при изменении кода.
  • RichText — компонент редактируемого текста; в save() его парный метод RichText.Content выводит сохранённое содержимое.

Теперь подключите этот файл в редактор. Добавьте в ds-custom-blocks.php:

add_action( 'enqueue_block_editor_assets', function () {
    wp_enqueue_script(
        'ds-notice-block',
        plugin_dir_url( __FILE__ ) . 'blocks/notice/notice.js',
        array( 'wp-blocks', 'wp-element', 'wp-block-editor' ),
        '1.0.0',
        true
    );
} );

Третий аргумент — зависимости. WordPress подгрузит их раньше вашего файла; если забыть хотя бы одну (wp-block-editor — частый кандидат), скрипт молча не сработает. Точно так же подключаются скрипты при создании своих шорткодов WordPress — приёмы один в один.

Проверяем блок в редакторе — и что делать с «недействительным контентом»

Активируйте плагин, создайте тестовую страницу и нажмите «+» в редакторе. В поиске наберите «Заметка» — блок должен появиться в категории «Виджеты» (category widgets). Добавьте его, введите заголовок и текст, сохраните страницу и посмотрите на фронтенде: заголовок и текст обёрнуты в <div class="ds-notice">.

Если блок не появился в списке — проверьте три вещи:

  1. Кэш. Плагины кэширования и CDN агрессивно кэшируют скрипты редактора. После добавления блока сбросьте кэш — как правильно устроено кэширование в WordPress, мы разбирали отдельно.
  2. Консоль браузера (F12). Ошибки JS видны только там. «wp.blocks is undefined» — забыты зависимости в wp_enqueue_script.
  3. Путь к файлу. Откройте в браузере URL скрипта из панели Network — 404 означает опечатку в пути.

Вторая классическая ситуация — редактор показывает сообщение «Блок содержит неожиданный или недействительный контент». Это значит: HTML в базе не совпадает с тем, что сейчас вернул бы save(). Причины — изменили разметку в save(), поменяли selector у атрибутов или правили сохранённый HTML вручную. Нажмите «Попытаться восстановить блок» — редактор перезапишет разметку по актуальному save(). После правок save() пройдитесь по страницам со старым блоком и перепубликуйте их.

Современный каркас: block.json и register_block_type()

С WordPress 5.8 стандартом стало серверное описание блока через block.json. Это даёт три плюса: метаданные блока читаются из одного файла, скрипты и стили подключаются автоматически с правильными зависимостями, а PHP-часть становится одной строкой. Создайте blocks/notice/block.json:

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "ds/notice",
    "title": "Заметка (delai-sait)",
    "category": "widgets",
    "icon": "marker",
    "description": "Цветная заметка с заголовком и текстом",
    "keywords": [ "заметка", "notice" ],
    "textdomain": "ds-custom-blocks",
    "attributes": {
        "heading": { "type": "string", "default": "" },
        "text":    { "type": "string", "default": "" },
        "bgColor": { "type": "string", "default": "#fff8e1" }
    },
    "supports": {
        "html": false,
        "alignWide": true
    },
    "editorScript": "file:./build/index.js",
    "editorStyle":  "file:./build/index.css",
    "style":        "file:./build/style-index.css",
    "render":       "file:./render.php"
}

Здесь apiVersion: 3 — актуальная версия API блока, supports.html: false запрещает редактировать блок как HTML, а пути file:./build/… указывают на собранные файлы (их мы получим в разделе про npm). Регистрация на сервере — одна строка в ds-custom-blocks.php:

add_action( 'init', function () {
    register_block_type( __DIR__ . '/blocks/notice' );
} );

WordPress прочитает block.json, сам подключит скрипты и стили (зависимости возьмёт из генерируемого файла index.asset.php) и при наличии render.php сделает блок динамическим. Обратите внимание на хук init — регистрация должна происходить на инициализации, вместе со всеми другими хуками WordPress: слишком раннее подключение вызовет фатальную ошибку «Function not exist».

Атрибуты, RichText и панель настроек в инспекторе

Теперь перепишем блок на современном синтаксисе с импортами и добавим панель настроек: выбор цвета фона в сайдбаре инспектора. Код ниже использует ES6+ и JSX, поэтому требует сборки (следующий раздел) — файл blocks/notice/src/index.js:

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ColorPalette } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

const COLORS = [
    { name: 'Жёлтый',  color: '#fff8e1' },
    { name: 'Голубой', color: '#e3f2fd' },
    { name: 'Зелёный', color: '#e8f5e9' }
];

registerBlockType( 'ds/notice', {
    edit( { attributes, setAttributes } ) {
        const blockProps = useBlockProps( {
            style: { backgroundColor: attributes.bgColor }
        } );

        return (
            <>
                <InspectorControls>
                    <PanelBody title={ __( 'Оформление' ) }>
                        <ColorPalette
                            colors={ COLORS }
                            value={ attributes.bgColor }
                            onChange={ ( color ) => setAttributes( { bgColor: color || '#fff8e1' } ) }
                        />
                    </PanelBody>
                </InspectorControls>

                <div { ...blockProps }>
                    <RichText
                        tagName="p"
                        className="ds-notice__heading"
                        value={ attributes.heading }
                        onChange={ ( value ) => setAttributes( { heading: value } ) }
                        placeholder="Заголовок заметки…"
                    />
                    <RichText
                        tagName="p"
                        className="ds-notice__text"
                        value={ attributes.text }
                        onChange={ ( value ) => setAttributes( { text: value } ) }
                        placeholder="Текст заметки…"
                    />
                </div>
            </>
        );
    },

    save( { attributes } ) {
        const blockProps = useBlockProps.save( {
            style: { backgroundColor: attributes.bgColor }
        } );

        return (
            <div { ...blockProps }>
                <RichText.Content tagName="p" className="ds-notice__heading" value={ attributes.heading } />
                <RichText.Content tagName="p" className="ds-notice__text" value={ attributes.text } />
            </div>
        );
    }
} );

Разбор по компонентам:

  • useBlockProps() — обязательный помощник новых версий API: он возвращает атрибуты обёртки блока, включая служебный класс wp-block-ds-notice (имя блока превращается в CSS-класс автоматически). Без него стили и выравнивание будут вести себя непредсказуемо.
  • InspectorControls помещает содержимое в боковую панель настроек; PanelBody — сворачиваемая секция внутри неё.
  • ColorPalette — готовый выбор цвета; выбранное значение уходит в атрибут bgColor и применяется к обеим картинкам мира — и в edit, и в save.
  • Функция __() — перевод строк, полезная привычка с самого старта.

Сборка через @wordpress/scripts: npm без боли

Ручная настройка webpack — главный кошмар новичка. К счастью, команда WordPress поддерживает пакет @wordpress/scripts (wp-scripts), который прячет всю конфигурацию. Понадобится установленный Node.js (берите LTS-версию с официального сайта). Создайте в папке блока blocks/notice файл package.json:

{
    "name": "ds-notice-block",
    "version": "1.0.0",
    "scripts": {
        "start": "wp-scripts start",
        "build": "wp-scripts build"
    },
    "devDependencies": {
        "@wordpress/scripts": "^28.0.0"
    }
}

Затем в терминале (в папке блока):

npm install
npm start

Команда npm start запускает режим разработки: wp-scripts читает src/index.js, компилирует ES6+/JSX в понятный браузерам ES5 и после каждого сохранения файла пересобирает результат в build/. Для боевой версии выполните npm run build — на выходе будут минифицированные файлы плюс index.asset.php со списком зависимостей.

Структура исходников блока:

blocks/notice/
├── block.json          ← пути на ./build/…
├── render.php
├── package.json
├── src/
│   ├── index.js        ← код из раздела 6
│   ├── index.css       ← стили для редактора
│   └── style.scss      ← стили для фронта (можно писать обычный CSS)
└── build/              ← создаётся wp-scripts, не редактируется руками
    ├── index.js
    ├── index.asset.php
    ├── index.css
    └── style-index.css

Важно: папку build/ никогда не правят руками — только src/. А папку node_modules не загружают на хостинг: на сервере нужны только PHP-файлы, block.json и build/.

Динамический блок: render.php и серверный рендеринг

Статичный блок хранит HTML в контенте записи — он зафиксирован на момент публикации. Динамический блок работает иначе: save() возвращает null, а HTML генерирует PHP-файл render.php при каждом показе страницы. Это идеально для содержимого, которое должно быть всегда свежим: последние записи, текущая дата, данные из базы.

Добавьте в src/index.js вместо прежнего save:

save: () => null,

И создайте blocks/notice/render.php:

<?php
/**
 * Шаблон динамического рендеринга блока ds/notice.
 *
 * @var array $attributes Атрибуты блока.
 */

$wrapper = get_block_wrapper_attributes( array( 'class' => 'ds-notice' ) );
?>
<div <?php echo $wrapper; // phpcs:ignore WordPress.Security.EscapeOutput ?>>
    <p class="ds-notice__heading"><?php echo esc_html( $attributes['heading'] ); ?></p>
    <div class="ds-notice__text"><?php echo wp_kses_post( $attributes['text'] ); ?></div>
</div>

Три важных момента:

  1. get_block_wrapper_attributes() возвращает строку атрибутов обёртки — в редакторе и на фронте блок получит одинаковые классы (включая wp-block-ds-notice).
  2. Экранирование обязательно. esc_html() для текстовых значений и wp_kses_post() для HTML — базовая безопасность WordPress: незакэскейпленный вывод атрибутов — прямой путь к XSS через редактор.
  3. Атрибуты приходят в $attributes уже валидированными по типам из block.json — но доверяй, потом проверяй: любые сложные данные дополнительно проверяйте сами.

Динамический рендеринг — не единственный способ получать «живые» данные. Если блоку нужно тянуть контент по кнопке без перезагрузки страницы, пригодится AJAX в WordPress, а для обмена данными с внешними сервисами — REST API WordPress: из собственного блока они вызываются теми же функциями, что и из темы.

Стили блока: подключаем CSS и supports

Осталось оформить заметку. Файл src/style.scss (стили фронта) — обратите внимание на авто-класс обёртки:

.wp-block-ds-notice {
    border-left: 4px solid #ffb300;
    border-radius: 6px;
    padding: 16px 20px;
    margin: 24px 0;
}

.wp-block-ds-notice .ds-notice__heading {
    font-weight: 700;
    margin: 0 0 8px;
}

.wp-block-ds-notice .ds-notice__text {
    margin: 0;
}

src/index.css (стили редактора) может повторять те же правила или добавлять подсказки для редактора, например пунктирную подсветку пустых полей:

.wp-block-ds-notice .ds-notice__heading:empty::before {
    content: "Заголовок заметки…";
    opacity: 0.5;
}

Через supports в block.json блок получает встроенные возможности редактора без единой строки кода: "alignWide": true — широкое выравнивание, "color": { "background": true, "text": true } — встроенные контролы цвета, "spacing": { "margin": true } — настройка отступов. Значения, выбранные в инспекторе, применяются автоматически, если в коде вы используете useBlockProps().

Какой подход выбрать: сравнение четырёх способов

Собрать блок в WordPress можно разными путями, и «правильного» среди них нет — есть подходящий под задачу и ваш стек. Сводная таблица:

Подход Что нужно знать Сборка (npm) Когда выбирать
ES5 без сборки (createElement) Базовый JS, зависимости wp-* Не нужна Первый блок, быстрый тест идеи, хостинг без Node
block.json + @wordpress/scripts ES6+/JSX, npm Нужна (start/build) Регулярная разработка блоков, командная работа
Динамический блок (render.php) PHP, экранирование вывода Как у основного варианта Свежий контент при каждом показе, данные из БД
ACF-блоки (acf_register_block_type) Плагин ACF, поля Не обязательна Быстрые блоки под контент-менеджера без JS

Мой совет новичку: начните с ES5-варианта из раздела 3, чтобы почувствовать механику edit/save без установки инструментов. Как только захочется компонентов из инспектора и удобного синтаксиса — переходите на @wordpress/scripts. А если проект «сайт на заказ» и блоки нужны в большом количестве для клиента, который будет их наполнять, — посмотрите на ACF-блоки: там интерфейс полей собирается кликами, а кода почти нет.

Типичные ошибки новичков при создании блоков

  1. Правка save() на живом сайте без перепубликации. Старые страницы продолжают хранить прежнюю разметку, и редактор помечает их блоки недействительными. После изменения save() перепубликуйте затронутые записи.
  2. Забытые зависимости в wp_enqueue_script. Без wp-blocks, wp-element, wp-block-editor скрипт падает молча. Всегда проверяйте консоль браузера (F12).
  3. Двойная регистрация блока. Один и тот же registerBlockType('ds/notice', …) в двух файлах (например, остался и ES5-файл, и собранный build) — редактор выдаст предупреждение о повторной регистрации. Подключайте ровно один источник.
  4. Незакэскейпленный вывод в render.php. echo $attributes['text'] без wp_kses_post() — уязвимость XSS. Экранируйте всё, что выводите.
  5. Регистрация не на том хуке. register_block_type() должен вызываться на init (или позже). Вызов на plugins_loaded приводит к ошибке, enqueue-хуки — к игнорированию блока.
  6. Разные классы обёртки в edit и save. Если в edit забыть useBlockProps(), а в save — useBlockProps.save(), стили применятся только в одной «картине мира», и вы долго будете искать причину.

Часто задаваемые вопросы

Нужно ли знать React, чтобы создать свой блок Gutenberg?

Глубоких знаний React не требуется. Для простого блока достаточно понимать createElement и объект атрибутов — это чистый JavaScript. Для сложных блоков на JSX полезно знать основы React: компоненты, props, состояние. Разбираться в хуках React на продвинутом уровне для типовых задач не нужно — большинство UI-компонентов уже готово в пакетах @wordpress/components.

Можно ли создать блок без Node.js и npm?

Да. ES5-вариант из раздела 3 собирается без каких-либо инструментов: один JS-файл, подключённый через enqueue_block_editor_assets. Ограничения — нет JSX, нет импортов, нет минификации. Для одного-двух простых блоков этого достаточно; если блоков много или они сложные, сборка через @wordpress/scripts окупается уже на втором блоке.

Где хранить код блока — в плагине или в теме?

По умолчанию — в плагине: контентные блоки (заметки, карточки, аккордеоны) должны переживать смену темы, как переживают её записи. В тему (лучше в дочернюю) блок кладут, только когда он завязан на дизайн конкретной темы — например, уникальный hero-блок с разметкой под эту вёрстку.

Почему редактор пишет «Блок содержит неожиданный или недействительный контент»?

Разметка в базе данных не совпадает с тем, что сейчас возвращает save(). Типичные причины: изменили HTML-структуру в save(), поменяли selector или source у атрибутов, правили сохранённый контент вручную. Решение — кнопка «Попытаться восстановить блок» и перепубликация. Чтобы избежать проблемы, планируйте разметку save() заранее и не меняйте её мелочно.

Чем статичный блок отличается от динамического?

Статичный сохраняет готовый HTML в контент записи через save() — он быстрее при показе и работает даже после отключения плагина. Динамический возвращает null из save(), а HTML генерирует render.php при каждом показе — содержимое всегда актуальное, но при отключении плагина блок исчезает со страниц. Правило: дизайн-элементы — статичные, «живые» данные — динамические.

Как добавить свой блок в отдельную категорию и задать свою иконку?

Своя категория регистрируется фильтром blocks.registerBlockType или через registerBlockCollection (папка с несколькими блоками объединяется в коллекцию с общим названием). Иконка задаётся в icon — встроенной Dash-иконкой ('marker', 'admin-comments') или inline-SVG строкой, если нужен фирменный знак.

TimeWeb

Вывод: ваш первый блок Gutenberg — за один вечер

Собственный блок — не эзотерика для фронтендеров, а навык, доступный новичку. Минимальный план на первый вечер:

  1. Создайте плагин-каркас с шапкой и активируйте его;
  2. Скопируйте ES5-блок из раздела 3, подключите его через enqueue_block_editor_assets и проверьте в редакторе;
  3. Добавьте block.json и register_block_type() — почувствуйте разницу между «подключил файл» и «зарегистрировал блок»;
  4. Установите Node.js, запустите npm start и добавьте панель настроек с выбором цвета;
  5. Переведите блок на render.php и отработайте динамический рендеринг с экранированием вывода.

После этого шага вы поймёте Gutenberg изнутри: станет ясно, почему некоторые чужие блоки «ломаются» при обновлениях, как читать документацию разработчика и какие блоки стоит написать под свой сайт. Дальше — InnerBlocks для вложенных блоков, свои категории и коллекции, вариации блоков и BlockContext.

Больше практических материалов о разработке — в блоге delai-sait.ru. Если нужен сайт под ключ с продуманной архитектурой блоков, которую удобно поддерживать, — посмотрите услуги разработки сайтов, а примеры готовых работ — в портфолио. Вопросы по разработке блоков под ваш проект можно задать через форму контактов.

Clearfy

Поделиться:
Telegram ВКонтакте