Коротко: собственный блок 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 собирается из трёх частей:
- JavaScript-регистрация. Функция
wp.blocks.registerBlockType()сообщает редактору: «есть блок с таким-то именем, вот его иконка, вот как его редактировать и как сохранять». Внутри неё — два метода:edit()возвращает интерфейс для редактора,save()— HTML, который сохранится в контент страницы. - Атрибуты (attributes). Это «память» блока — заголовок, текст, цвет, URL картинки. Редактор хранит атрибуты в объекте
props.attributes, а изменяет их черезprops.setAttributes(). Именно атрибуты отличают один блок от другого. - 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">.
Если блок не появился в списке — проверьте три вещи:
- Кэш. Плагины кэширования и CDN агрессивно кэшируют скрипты редактора. После добавления блока сбросьте кэш — как правильно устроено кэширование в WordPress, мы разбирали отдельно.
- Консоль браузера (F12). Ошибки JS видны только там. «wp.blocks is undefined» — забыты зависимости в
wp_enqueue_script. - Путь к файлу. Откройте в браузере 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>
Три важных момента:
get_block_wrapper_attributes()возвращает строку атрибутов обёртки — в редакторе и на фронте блок получит одинаковые классы (включаяwp-block-ds-notice).- Экранирование обязательно.
esc_html()для текстовых значений иwp_kses_post()для HTML — базовая безопасность WordPress: незакэскейпленный вывод атрибутов — прямой путь к XSS через редактор. - Атрибуты приходят в
$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-блоки: там интерфейс полей собирается кликами, а кода почти нет.
Типичные ошибки новичков при создании блоков
- Правка
save()на живом сайте без перепубликации. Старые страницы продолжают хранить прежнюю разметку, и редактор помечает их блоки недействительными. После измененияsave()перепубликуйте затронутые записи. - Забытые зависимости в
wp_enqueue_script. Безwp-blocks,wp-element,wp-block-editorскрипт падает молча. Всегда проверяйте консоль браузера (F12). - Двойная регистрация блока. Один и тот же
registerBlockType('ds/notice', …)в двух файлах (например, остался и ES5-файл, и собранный build) — редактор выдаст предупреждение о повторной регистрации. Подключайте ровно один источник. - Незакэскейпленный вывод в
render.php.echo $attributes['text']безwp_kses_post()— уязвимость XSS. Экранируйте всё, что выводите. - Регистрация не на том хуке.
register_block_type()должен вызываться наinit(или позже). Вызов наplugins_loadedприводит к ошибке,enqueue-хуки — к игнорированию блока. - Разные классы обёртки в
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 строкой, если нужен фирменный знак.

Вывод: ваш первый блок Gutenberg — за один вечер
Собственный блок — не эзотерика для фронтендеров, а навык, доступный новичку. Минимальный план на первый вечер:
- Создайте плагин-каркас с шапкой и активируйте его;
- Скопируйте ES5-блок из раздела 3, подключите его через
enqueue_block_editor_assetsи проверьте в редакторе; - Добавьте
block.jsonиregister_block_type()— почувствуйте разницу между «подключил файл» и «зарегистрировал блок»; - Установите Node.js, запустите
npm startи добавьте панель настроек с выбором цвета; - Переведите блок на
render.phpи отработайте динамический рендеринг с экранированием вывода.
После этого шага вы поймёте Gutenberg изнутри: станет ясно, почему некоторые чужие блоки «ломаются» при обновлениях, как читать документацию разработчика и какие блоки стоит написать под свой сайт. Дальше — InnerBlocks для вложенных блоков, свои категории и коллекции, вариации блоков и BlockContext.
Больше практических материалов о разработке — в блоге delai-sait.ru. Если нужен сайт под ключ с продуманной архитектурой блоков, которую удобно поддерживать, — посмотрите услуги разработки сайтов, а примеры готовых работ — в портфолио. Вопросы по разработке блоков под ваш проект можно задать через форму контактов.



