Как создать кастомный Gutenberg-блок
Полгода назад передо мной стояла задача — создать уникальный блок для WordPress-сайта клиента, который бы отображал карточки товаров с анимацией при наведении. Стандартные блоки Gutenberg не давали нужной гибкости, а плагины добавляли лишний вес. Так началось моё погружение в разработку кастомных блоков, о котором хочу рассказать подробно.
Почему я выбрал именно этот путь
Многие разработчики боятся Gutenberg из-за React и сложности сборки. Признаюсь, первые пару дней я тоже путался в терминологии — attributes, InspectorControls, RichText. Но когда разобрался в логике, процесс создания блоков стал приносить удовольствие.
Подготовка окружения
Первое, что нужно сделать — установить Node.js и инициализировать проект с помощью официального инструмента от WordPress:
npx @wordpress/create-block product-card-block
cd product-card-block
npm start
Эта команда создаёт полноценную структуру плагина с настроенным webpack, babel и всеми необходимыми зависимостями. Раньше я настраивал сборку вручную, но @wordpress/scripts экономит часы времени.
Структура моего блока
После генерации я получил такую структуру файлов:
product-card-block/
├── src/
│ ├── block.json
│ ├── edit.js
│ ├── save.js
│ ├── index.js
│ ├── style.scss
│ └── editor.scss
├── build/
└── product-card-block.php
Настройка block.json
Этот файл — метаданные блока. Именно здесь я определяю атрибуты, поддержку и категорию:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "myplugin/product-card",
"title": "Карточка товара",
"category": "widgets",
"icon": "cart",
"description": "Блок для отображения карточки товара с анимацией",
"keywords": ["товар", "карточка", "продукт"],
"version": "1.0.0",
"textdomain": "product-card-block",
"attributes": {
"title": {
"type": "string",
"source": "html",
"selector": "h3"
},
"price": {
"type": "string",
"default": ""
},
"imageUrl": {
"type": "string",
"default": ""
},
"imageId": {
"type": "number"
},
"buttonText": {
"type": "string",
"default": "Купить"
}
},
"supports": {
"html": false,
"align": ["wide", "full"]
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}
Логика редактирования
Это самый интересный файл, где я собрал интерфейс редактирования блока edit.js:
import { __ } from '@wordpress/i18n';
import {
useBlockProps,
RichText,
MediaUpload,
MediaUploadCheck,
InspectorControls,
} from '@wordpress/block-editor';
import { PanelBody, TextControl, Button } from '@wordpress/components';
export default function Edit({ attributes, setAttributes }) {
const { title, price, imageUrl, imageId, buttonText } = attributes;
const blockProps = useBlockProps({
className: 'product-card-block',
});
const onSelectImage = (media) => {
setAttributes({
imageUrl: media.url,
imageId: media.id,
});
};
return (
<>
<InspectorControls>
<PanelBody title={__('Настройки товара', 'product-card-block')}>
<TextControl
label={__('Цена', 'product-card-block')}
value={price}
onChange={(value) => setAttributes({ price: value })}
/>
<TextControl
label={__('Текст кнопки', 'product-card-block')}
value={buttonText}
onChange={(value) => setAttributes({ buttonText: value })}
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
<MediaUploadCheck>
<MediaUpload
onSelect={onSelectImage}
allowedTypes={['image']}
value={imageId}
render={({ open }) => (
<div className="product-card-image" onClick={open}>
{imageUrl ? (
<img src={imageUrl} alt={title} />
) : (
<Button variant="secondary">
{__('Загрузить изображение', 'product-card-block')}
</Button>
)}
</div>
)}
/>
</MediaUploadCheck>
<RichText
tagName="h3"
value={title}
onChange={(value) => setAttributes({ title: value })}
placeholder={__('Название товара', 'product-card-block')}
/>
<div className="product-price">{price} ₽</div>
<button className="product-buy-btn">{buttonText}</button>
</div>
</>
);
}
Сохранение — save.js
Здесь важно точно соответствовать разметке из block.json, иначе получите ошибку валидации:
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function save({ attributes }) {
const { title, price, imageUrl, buttonText } = attributes;
const blockProps = useBlockProps.save({
className: 'product-card-block',
});
return (
<div {...blockProps}>
{imageUrl && (
<div className="product-card-image">
<img src={imageUrl} alt={title} />
</div>
)}
<RichText.Content tagName="h3" value={title} />
<div className="product-price">{price} ₽</div>
<button className="product-buy-btn">{buttonText}</button>
</div>
);
}
Стилизация с анимацией
В style.scss я добавил ту самую анимацию при наведении, ради которой всё затевалось:
.product-card-block {
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
cursor: pointer;
&:hover {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}
.product-card-image img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 8px;
}
.product-price {
font-size: 24px;
font-weight: bold;
color: #2c3e50;
margin: 12px 0;
}
.product-buy-btn {
background: #3498db;
color: white;
padding: 10px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: #2980b9;
}
}
}
Регистрация плагина в PHP
Основной файл плагина остаётся простым благодаря автоматизации:
<?php
/**
* Plugin Name: Product Card Block
* Description: Кастомный блок карточки товара
* Version: 1.0.0
*/
function register_product_card_block() {
register_block_type(__DIR__ . '/build');
}
add_action('init', 'register_product_card_block');
Сборка и тестирование
Для финальной сборки я использую:
npm run build
Это создаёт минифицированные файлы в папке build, оптимизированные для продакшена. После активации плагина блок появляется в инсертере Gutenberg под категорией «Виджеты».
Подводные камни, с которыми я столкнулся
Валидация блока — главная головная боль новичков. Если разметка в save.js не совпадает с тем, что реально сохранено в базе данных, WordPress выдаёт ошибку и предлагает восстановить блок. Я решил эту проблему, добавив deprecated версии при изменении структуры атрибутов.
Второй момент — локализация. Функция __() работает только если правильно подключен textdomain через wp_set_script_translations().
Итоговые мысли
Создание кастомных блоков — это инвестиция в долгосрочную поддерживаемость проекта. Вместо десятка плагинов для разных элементов интерфейса, я получаю чистый, контролируемый код, который полностью соответствует дизайну проекта. Освоив этот процесс однажды, вы будете использовать его снова и снова — благо, шаблон легко адаптируется под любые задачи, от простых текстовых блоков до сложных интерактивных виджетов.