Как создать кастомный Gutenberg-блок

Как создать кастомный 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