Шорткоды в WordPress — это как костыли. Работают, но выглядят криво. Вставляешь [cta_button color="red"] в редактор, а в превью видишь… текст. Потому что шорткоды не рендерятся в визуальном редакторе. Приходится либо переключаться в режим кода, либо гадать, как это будет выглядеть на фронте.
Gutenberg изменил правила игры. Теперь вместо шорткодов мы используем блоки — визуальные компоненты, которые выглядят в редакторе так же, как на сайте. И самое крутое: написать свой блок не сложнее, чем создать шорткод. Нужен React? Минимально. Нужен Webpack? Опционально. Нужен ACF Pro за $49 в год? Ни в коем случае.
В этом руководстве создадим кастомный блок «Call to Action» с нуля. Разберём структуру, атрибуты, серверный рендеринг и стили. К концу статьи у вас будет рабочий блок, который можно вставить в любую тему.
Почему свои блоки лучше плагинов
Плагины вроде ACF Blocks или GenerateBlocks — отличные инструменты. Но у них есть три проблемы:
- Вес: ACF Blocks добавляет 2 МБ кода, который грузится на каждой странице
- Зависимости: если плагин перестанет обновляться, ваши блоки сломаются
- Ограничения: вы ограничены тем, что предлагает плагин. Хотите кастомную логику — дописывайте код
Свой блок весит 5-10 КБ. Он не зависит от сторонних обновлений. И вы контролируете каждую строчку кода. Для технического блога или магазина это критично.
Когда свои блоки — правильный выбор:
- Нужен уникальный блок, которого нет в плагинах (калькулятор, таймер, кастомная карточка)
- Хотите избежать зависимости от платных плагинов
- Нужна специфическая логика (интеграция с API, сложная валидация)
- Важна производительность (каждый килобайт на счету)
Структура Gutenberg блока
Блок в Gutenberg — это набор файлов, которые описывают три вещи: как блок выглядит в редакторе, как он сохраняется в базу и как рендерится на фронте.
Минимальная структура:
wp-content/themes/your-theme/
└── blocks/
└── call-to-action/
├── block.json # Метаданные блока
├── editor.js # Код для редактора (React)
├── render.php # Серверный рендеринг
├── style.css # Стили для фронта и редактора
└── editor.css # Стили только для редактораРазберём каждый файл. Но сначала — регистрация блока.
Шаг 1: Регистрация блока через block.json
Начиная с WordPress 5.8, блоки регистрируются через файл block.json. Это декларативный способ, который проще и надёжнее старой функции register_block_type().
Создаём файл blocks/call-to-action/block.json:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "customcode/call-to-action",
"version": "1.0.0",
"title": "Call to Action",
"category": "design",
"icon": "megaphone",
"description": "Блок с заголовком, текстом и кнопкой",
"keywords": ["cta", "кнопка", "призыв"],
"supports": {
"html": false,
"align": ["wide", "full"]
},
"attributes": {
"title": {
"type": "string",
"default": "Заголовок CTA"
},
"description": {
"type": "string",
"default": "Описание призыва к действию"
},
"buttonText": {
"type": "string",
"default": "Узнать больше"
},
"buttonUrl": {
"type": "string",
"default": "#"
},
"backgroundColor": {
"type": "string",
"default": "#2d2d30"
}
},
"editorScript": "file:./editor.js",
"editorStyle": "file:./editor.css",
"style": "file:./style.css",
"render": "file:./render.php"
}Что здесь важно:
- name: уникальное имя блока в формате
namespace/block-name - attributes: данные, которые блок сохраняет (заголовок, текст, цвет)
- editorScript: JavaScript для редактора (React-компонент)
- render: PHP-файл для серверного рендеринга на фронте
Регистрация в functions.php:
Теперь нужно сказать WordPress, что у нас есть блок. Добавляем в functions.php:
function customcode_register_blocks() {
register_block_type( __DIR__ . '/blocks/call-to-action' );
}
add_action( 'init', 'customcode_register_blocks' );Всё. WordPress сам прочитает block.json и зарегистрирует блок. Никакой магии.
Шаг 2: React-компонент для редактора
Теперь самое интересное — интерфейс блока в редакторе. Здесь используется React, но пугаться не стоит. Код простой, а если вы писали на JavaScript, разберётесь за 10 минут.
Создаём файл blocks/call-to-action/editor.js:
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, ColorPicker } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import metadata from './block.json';
import './style.css';
import './editor.css';
registerBlockType( metadata.name, {
edit: ( { attributes, setAttributes } ) => {
const { title, description, buttonText, buttonUrl, backgroundColor } = attributes;
const blockProps = useBlockProps( {
className: 'cta-block',
style: { backgroundColor }
} );
return (
<>
setAttributes( { buttonText: value } ) }
/>
setAttributes( { buttonUrl: value } ) }
/>
setAttributes( { backgroundColor: value.hex } ) }
/>
setAttributes( { title: value } ) } placeholder={ __( ‘Заголовок CTA’ ) } />
setAttributes( { description: value } ) } placeholder={ __( ‘Описание призыва к действию’ ) } />
</> ); }, save: () => null // Серверный рендеринг, поэтому save пустой } );
Разберём ключевые моменты:
- useBlockProps: хук, который добавляет нужные классы и атрибуты к корневому элементу блока
- InspectorControls: боковая панель настроек (справа в редакторе)
- RichText: компонент для редактируемого текста (заголовок, описание)
- setAttributes: функция для обновления атрибутов блока
- save: () => null: потому что мы используем серверный рендеринг (об этом ниже)
Компиляция JavaScript
WordPress не понимает JSX (синтаксис React) напрямую. Нужно скомпилировать editor.js в обычный JavaScript. Два варианта:
Вариант А: @wordpress/scripts (рекомендуется)
Официальный инструмент от WordPress. Устанавливается через npm:
npm install @wordpress/scripts --save-devДобавляем в package.json:
{
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
}
}Теперь команда npm run build скомпилирует editor.js в build/editor.js. А npm run start запустит watcher для разработки.
Вариант Б: Без компиляции (для простых блоков)
Если не хотите возиться с npm, можно писать на чистом JavaScript без JSX. Но это дольше и менее удобно. Для серьёзной разработки лучше настроить сборку.
Шаг 3: Серверный рендеринг через render.php
Серверный рендеринг — это когда блок генерирует HTML на сервере (через PHP), а не сохраняется в базу. Это решает три проблемы:
- Актуальность: если вы измените разметку в
render.php, все существующие блоки обновятся автоматически - Производительность: в базе хранятся только атрибуты (текст, ссылки), а не весь HTML
- Динамика: можно подтягивать данные из базы, API, опций в момент рендеринга
Создаём файл blocks/call-to-action/render.php:
<?php
/**
* Серверный рендеринг блока Call to Action
*
* @var array $attributes Атрибуты блока
* @var string $content Контент блока (не используется при серверном рендеринге)
* @var WP_Block $block Объект блока
*/
$title = esc_html( $attributes['title'] ?? 'Заголовок CTA' );
$description = esc_html( $attributes['description'] ?? 'Описание призыва к действию' );
$button_text = esc_html( $attributes['buttonText'] ?? 'Узнать больше' );
$button_url = esc_url( $attributes['buttonUrl'] ?? '#' );
$bg_color = esc_attr( $attributes['backgroundColor'] ?? '#2d2d30' );
$wrapper_attributes = get_block_wrapper_attributes( array(
'class' => 'cta-block',
'style' => 'background-color: ' . $bg_color,
) );
?>
<div <?php echo $wrapper_attributes; ?>>
<h2 class="cta-title"><?php echo $title; ?></h2>
<p class="cta-description"><?php echo $description; ?></p>
<a href="<?php echo $button_url; ?>" class="cta-button">
<?php echo $button_text; ?>
</a>
</div>Что здесь происходит:
- $attributes: WordPress автоматически передаёт атрибуты из
block.json - esc_html(), esc_url(), esc_attr(): обязательная санитизация данных (никогда не выводите сырые атрибуты!)
- get_block_wrapper_attributes(): функция, которая добавляет стандартные атрибуты блока (классы выравнивания, якоря и т.д.)
Шаг 4: Стили для блока
Стили делятся на две категории: общие (для редактора и фронта) и специфичные для редактора.
Общие стили (style.css):
Эти стили загружаются и в редакторе, и на фронте. Создаём blocks/call-to-action/style.css:
.cta-block {
padding: 32px;
border-radius: 8px;
text-align: center;
margin: 24px 0;
}
.cta-title {
font-size: 28px;
font-weight: 700;
color: #ffffff;
margin-bottom: 12px;
}
.cta-description {
font-size: 16px;
color: #d4d4d4;
margin-bottom: 20px;
line-height: 1.6;
}
.cta-button {
display: inline-block;
padding: 12px 24px;
background: #569cd6;
color: #ffffff;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
transition: background 0.2s;
}
.cta-button:hover {
background: #4a8bc2;
}Стили только для редактора (editor.css):
Эти стили загружаются только в редакторе. Полезно, если нужно выделить блок или добавить подсказки. Создаём blocks/call-to-action/editor.css:
.cta-block {
border: 2px dashed #3e3e42;
}
.cta-block:hover {
border-color: #569cd6;
}
.cta-title[contenteditable="true"]:empty::before,
.cta-description[contenteditable="true"]:empty::before {
content: attr(placeholder);
color: #6e6e6e;
}Шаг 5: Подключение стилей и скриптов
WordPress автоматически подключит стили и скрипты, указанные в block.json. Но нужно убедиться, что пути правильные.
Если вы использовали @wordpress/scripts и скомпилировали editor.js в папку build/, обновите block.json:
{
"editorScript": "file:./build/editor.js",
"editorStyle": "file:./build/editor.css",
"style": "file:./build/style.css"
}Практический пример: блок с динамическими данными
Давайте усложним задачу. Создадим блок «Последние статьи», который подтягивает данные из базы в момент рендеринга.
block.json:
{
"name": "customcode/latest-posts",
"title": "Последние статьи",
"category": "widgets",
"icon": "admin-post",
"attributes": {
"postsCount": {
"type": "number",
"default": 3
},
"categoryId": {
"type": "number",
"default": 0
}
},
"render": "file:./render.php"
}render.php:
<?php
$posts_count = absint( $attributes['postsCount'] ?? 3 );
$category_id = absint( $attributes['categoryId'] ?? 0 );
$args = array(
'posts_per_page' => $posts_count,
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'DESC',
);
if ( $category_id > 0 ) {
$args['cat'] = $category_id;
}
$posts = get_posts( $args );
if ( empty( $posts ) ) {
echo '<p class="no-posts">Статей не найдено</p>';
return;
}
$wrapper_attributes = get_block_wrapper_attributes( array(
'class' => 'latest-posts-block',
) );
?>
<div <?php echo $wrapper_attributes; ?>>
<?php foreach ( $posts as $post ) : ?>
<article class="latest-post">
<h3 class="latest-post-title">
<a href="<?php echo esc_url( get_permalink( $post->ID ) ); ?>">
<?php echo esc_html( $post->post_title ); ?>
</a>
</h3>
<time class="latest-post-date" datetime="<?php echo esc_attr( get_the_date( 'c', $post->ID ) ); ?>">
<?php echo esc_html( get_the_date( '', $post->ID ) ); ?>
</time>
</article>
<?php endforeach; ?>
</div>Теперь блок подтягивает свежие данные при каждом рендеринге. Если вы опубликуете новую статью, блок автоматически её покажет. Не нужно редактировать пост и пересохранять.
Шаг 6: Продвинутые атрибуты
Атрибуты могут быть не только строками и числами. Gutenberg поддерживает массивы, объекты и даже медиафайлы.
Атрибут для изображения:
{
"attributes": {
"imageId": {
"type": "number"
},
"imageUrl": {
"type": "string"
},
"imageAlt": {
"type": "string"
}
}
}Компонент MediaUpload в editor.js:
import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
// Внутри компонента edit:
<MediaUploadCheck>
<MediaUpload
onSelect={ ( media ) => setAttributes( {
imageId: media.id,
imageUrl: media.url,
imageAlt: media.alt
} ) }
allowedTypes={ [ 'image' ] }
value={ imageId }
render={ ( { open } ) => (
<Button onClick={ open } variant="secondary">
{ imageId ? 'Заменить изображение' : 'Выбрать изображение' }
</Button>
) }
/>
</MediaUploadCheck>Атрибут для массива (список ссылок):
{
"attributes": {
"links": {
"type": "array",
"default": [],
"items": {
"type": "object",
"properties": {
"label": { "type": "string" },
"url": { "type": "string" }
}
}
}
}
}Чек-лист: создание кастомного блока
- ✅ Создана папка
blocks/block-name/в теме или плагине - ✅ Написан
block.jsonс метаданными и атрибутами - ✅ Зарегистрирован блок в
functions.phpчерезregister_block_type() - ✅ Написан React-компонент в
editor.js - ✅ Настроен серверный рендеринг в
render.php - ✅ Добавлены стили в
style.cssиeditor.css - ✅ Скомпилирован JavaScript через
@wordpress/scripts - ✅ Протестирован блок в редакторе и на фронте
- ✅ Проверена санитизация данных (esc_html, esc_url, esc_attr)
Решение частых проблем
Блок не появляется в редакторе
Проверьте три вещи:
- Правильно ли указан путь в
register_block_type()(должен вести к папке сblock.json) - Нет ли ошибок в консоли браузера (F12 → Console)
- Скомпилирован ли
editor.js(если используете@wordpress/scripts)
Блок работает в редакторе, но не на фронте
Скорее всего, проблема в render.php. Проверьте:
- Нет ли ошибок PHP (включите
WP_DEBUG) - Правильно ли указаны пути к файлам в
block.json - Не забыли ли вывести HTML через
echoилиinclude
Стили не применяются в редакторе
Убедитесь, что style.css указан в block.json как "style": "file:./style.css". Если стили специфичны для редактора, добавьте "editorStyle": "file:./editor.css".
Атрибуты не сохраняются
Проверьте, что в editor.js вы вызываете setAttributes() с правильными ключами. Ключи должны совпадать с теми, что указаны в block.json.
Как отладить React-компонент?
Используйте console.log() в editor.js. Откройте консоль браузера (F12) и увидите вывод. Также полезен React DevTools — расширение для Chrome, которое показывает дерево компонентов.
Итог
Кастомные Gutenberg блоки — это не страшно. Да, нужно разобраться с React и структурой файлов. Но один раз настроив сборку, вы сможете создавать блоки за час. Они быстрее плагинов, легче обслуживаются и дают полный контроль над кодом.
Начните с простого блока (Call to Action или Latest Posts). Разберитесь в структуре. Потом усложняйте: добавляйте медиафайлы, массивы, интеграции с API. Через месяц вы удивитесь, как раньше жили без своих блоков.
А главное — откройте терминал прямо сейчас. Создайте папку blocks/ в теме. Напишите первый block.json. И посмотрите, как ваш блок появится в редакторе. Это чувство стоит всех усилий.