До появления блочного редактора Gutenberg экосистема WordPress страдала от двух архитектурных крайностей:
div.elementor-inner-section > div.elementor-column > div.elementor-widget-wrap), загружают 2–4 МБ неиспользуемого JavaScript и CSS на каждый просмотр страницы. Результат — проваленный аудит Core Web Vitals (метрики LCP, CLS, INP в красной зоне) и падение органического поискового трафика.[hero_card title="..." image="..."]):
Слепой ввод контента для контент-менеджеров. Отсутствие визуальной обратной связи (WYSIWYG), частые синтаксические опечатки в закрывающих тегах шорткодов, приводящие к поломке разметки всей страницы, и жесткая привязка логики к регулярным выражениям PHP.Блочный редактор Gutenberg (кодовое название ядра — Block Editor) переводит WordPress на компонентно-ориентированную frontend-архитектуру на базе React. Контент хранится в стандартном поле базы данных wp_posts.post_content в виде валидной HTML-разметки, обернутой в декларативные HTML-комментарии:
<!-- wp:agenticlab/hero-card {"title":"Масштабируемый WordPress","rating":5} -->
<div class="wp-block-agenticlab-hero-card">
<h2>Масштабируемый WordPress</h2>
<span class="rating">Оценка: 5</span>
</div>
<!-- /wp:agenticlab/hero-card -->Статические блоки Gutenberg выполняют React-код исключительно внутри панели администрирования при создании и редактировании контента. На публичной части сайта (фронтенде) WordPress отдает чистый скомпилированный HTML без единой строчки React-рантайма. Это гарантирует максимальную производительность (TTFB менее 100 мс) и 100 баллов в Google PageSpeed.
Архитектура кастомного блока опирается на строгую изоляцию трех сред выполнения:
┌────────────────────────────────────────────────────────────────────────┐
│ 1. ДЕКЛАРАТИВНЫЙ ПАСПОРТ │
│ block.json v3 │
│ Схема, метаданные, типизация атрибутов, поддержка стилей │
└───────────────────────────────────┬────────────────────────────────────┘
│
┌──────────────────────────┴──────────────────────────┐
▼ ▼
┌─────────────────────────────────┐ ┌──────────────────────────────────┐
│ 2. РЕАКТИВНЫЙ РЕДАКТОР │ │ 3. СЕРИАЛИЗАЦИЯ / ВЫВОД │
│ edit.tsx │ │ save.tsx │
│ (Выполняется в WP-Admin) │ │ (Статический HTML в БД) │
│ - useBlockProps │ │ - useBlockProps.save │
│ - RichText (inline редактир.) │ │ - RichText.Content │
│ - InspectorControls (боковая) │ │ - Чистая детерминированная ф-ция│
│ - BlockControls (тулбар) │ │ ИЛИ render.php (Dynamic SSR) │
└─────────────────────────────────┘ └──────────────────────────────────┘block.json#Файл block.json (спецификация Block API v3) — единый источник истины (Single Source of Truth) для блока. Он регистрирует блок одновременно в PHP-ядре и в JavaScript-реестре редактора, объявляет зависимости скриптов, стили и схему атрибутов.
edit.tsx#Реактивный React-компонент, работающий в контексте админ-панели. Он подписывается на состояние блока, получает объект attributes и мутирующую функцию setAttributes. В edit подключаются элементы взаимодействия:
RichText — инлайн-редактирование форматированного текста (заголовки, абзацы).InspectorControls — вывод настроек блока в правую боковую панель админки.BlockControls — плавающий тулбар форматирования над выделенным блоком.save.tsx#Строго чистая детерминированная функция! Она преобразует сохраненные attributes в итоговый HTML-код, который записывается в базу данных.
В save категорически запрещено:
useState, useEffect);Math.random() или временные метки через Date.now();fetch().Если разметка, которую возвращает save(), хоть на один символ или атрибут отличается от сохраненного в базе данных HTML-фрагмента, редактор WordPress выбросит фатальную ошибку «Block Validation Failed» (Этот блок содержит непредвиденный или недействительный контент). Для обновления структуры разметки старых блоков обязательно используется механизм версионирования deprecated.
Развертывание модульного плагина блока со сконфигурированным сборщиком @wordpress/scripts, TypeScript и линтерами WordPress Coding Standards.
Команда: npx @wordpress/create-block agenticlab-hero-card --template @wordpress/create-block-tutorial-template
Декларативное описание названия, иконки, категории, схемы атрибутов (строки, числа, булевы флаги) и директивы supports (цвета, отступы, типографика).
Объявление версии API 3 обеспечивает изолированный iframe для изоляции стилей редактора от темы.
Подключение useBlockProps, боковой панели InspectorControls с компонентами PanelBody, ToggleControl, SelectControl и инлайн-редактора RichText.
Управление состоянием блока осуществляется через атомарные мутации setAttributes({ title: val }).
Рендеринг статической разметки через useBlockProps.save() и RichText.Content. Гарантия отсутствия недетерминированных значений для предотвращения ошибок валидации.
Разметка сериализуется ядром и сохраняется в wp_posts.post_content внутри HTML-комментариев.
Создание резервных схем для сохранения обратной совместимости при изменении HTML-структуры или классов блока без поломки старых страниц сайта.
WordPress автоматически мигрирует устаревшую разметку в актуальный формат при открытии страницы в редакторе.
Регистрация каталога сборки /build через функцию register_block_type() на хуке ядра init с автозагрузкой сгенерированного файла зависимостей block.json.
Гарантирует автоматическое подключение стилей editor.css и style.css только на тех страницах, где блок реально используется.
Ниже представлена пошаговая архитектурная схема прохождения блока от момента инициализации в админке до рендеринга на клиенте:
На хуке PHP init вызывается register_block_type(__DIR__ . '/build'). Ядро считывает block.json, генерирует мета-схему и регистрирует дескрипторы JS/CSS ассетов.
React-компонент edit.tsx монтируется в дереве редактора блоков. Хук useBlockProps навешивает служебные data-атрибуты, классы выравнивания и обработчики выделения.
Пользователь редактирует текст через RichText или меняет тумблер в InspectorControls. Вызывается setAttributes(), обновляя дерево редактора в реальном времени.
При нажатии «Обновить страницу» ядро вызывает save(attributes). Сгенерированный HTML вместе с JSON-метаданными записывается в строку post_content таблицы wp_posts.
Посетитель открывает страницу. WordPress парсит HTML-комментарии блока, подключает style.css и отдает чистую разметку без участия React-движка.
Junior-разработчик решил сгенерировать уникальный идентификатор для якорной ссылки внутри функции save.tsx, написав: id={'card-' + Math.random().toString(36).substr(2, 9)}. Что произойдет при повторном открытии статьи в редакторе блоков?
Ниже представлен полноценный продакшн-код промышленного блока agenticlab/hero-card: с типизацией TypeScript, безопасными атрибутами, боковыми контролами и поддержкой темной темы.
block.json#{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "agenticlab/hero-card",
"version": "1.0.0",
"title": "Инженерная промо-карточка",
"category": "design",
"icon": "shield-alt",
"description": "Промо-карточка с заголовком, описанием, рейтингом и CTA-кнопкой для Fullstack Agentic Lab.",
"keywords": ["banner", "promo", "agenticlab", "hero"],
"textdomain": "agenticlab",
"attributes": {
"title": {
"type": "string",
"source": "html",
"selector": ".hero-card__title",
"default": ""
},
"description": {
"type": "string",
"source": "html",
"selector": ".hero-card__description",
"default": ""
},
"buttonText": {
"type": "string",
"default": "Подробнее"
},
"buttonUrl": {
"type": "string",
"default": ""
},
"badgeText": {
"type": "string",
"default": "NEW"
},
"isFeatured": {
"type": "boolean",
"default": false
},
"rating": {
"type": "number",
"default": 5
}
},
"supports": {
"html": false,
"align": ["wide", "full"],
"color": {
"background": true,
"text": true,
"gradients": true
},
"spacing": {
"margin": true,
"padding": true
},
"typography": {
"fontSize": true,
"lineHeight": true
}
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}src/edit.tsx#import React from 'react';
import {
useBlockProps,
RichText,
InspectorControls
} from '@wordpress/block-editor';
import {
PanelBody,
TextControl,
ToggleControl,
RangeControl
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
interface BlockAttributes {
title: string;
description: string;
buttonText: string;
buttonUrl: string;
badgeText: string;
isFeatured: boolean;
rating: number;
}
interface EditProps {
attributes: BlockAttributes;
setAttributes: (attrs: Partial<BlockAttributes>) => void;
isSelected: boolean;
}
export default function Edit({ attributes, setAttributes }: EditProps) {
const {
title,
description,
buttonText,
buttonUrl,
badgeText,
isFeatured,
rating
} = attributes;
// useBlockProps автоматически внедряет сгенерированные стили supports (цвета, паддинги)
const blockProps = useBlockProps({
className: `agenticlab-hero-card ${isFeatured ? 'is-featured' : ''}`,
});
return (
<>
{/* Боковая панель настроек инспектора */}
<InspectorControls>
<PanelBody title={__('Параметры баннера', 'agenticlab')} initialOpen={true}>
<TextControl
label={__('Текст бейджа', 'agenticlab')}
value={badgeText}
onChange={(val: string) => setAttributes({ badgeText: val })}
/>
<ToggleControl
label={__('Выделенный блок (Featured)', 'agenticlab')}
help={isFeatured ? __('Включена акцентная рамка', 'agenticlab') : __('Стандартный вид', 'agenticlab')}
checked={isFeatured}
onChange={(val: boolean) => setAttributes({ isFeatured: val })}
/>
<RangeControl
label={__('Рейтинг (звезды)', 'agenticlab')}
value={rating}
onChange={(val: number | undefined) => setAttributes({ rating: val ?? 5 })}
min={1}
max={5}
/>
</PanelBody>
<PanelBody title={__('Настройка целевой кнопки', 'agenticlab')} initialOpen={false}>
<TextControl
label={__('Текст кнопки', 'agenticlab')}
value={buttonText}
onChange={(val: string) => setAttributes({ buttonText: val })}
/>
<TextControl
label={__('Ссылка кнопки (URL)', 'agenticlab')}
value={buttonUrl}
onChange={(val: string) => setAttributes({ buttonUrl: val })}
placeholder="https://example.com"
/>
</PanelBody>
</InspectorControls>
{/* Визуальная рабочая область редактора */}
<div {...blockProps}>
{badgeText && (
<span className="hero-card__badge">
{badgeText}
</span>
)}
<RichText
tagName="h2"
className="hero-card__title"
value={title}
onChange={(val: string) => setAttributes({ title: val })}
placeholder={__('Заголовок промо-карточки...', 'agenticlab')}
allowedFormats={['core/bold', 'core/italic']}
/>
<RichText
tagName="p"
className="hero-card__description"
value={description}
onChange={(val: string) => setAttributes({ description: val })}
placeholder={__('Краткое описание преимуществ...', 'agenticlab')}
/>
<div className="hero-card__footer">
<div className="hero-card__rating">
{'★'.repeat(rating)}{'☆'.repeat(5 - rating)}
</div>
<div className="hero-card__btn-preview">
<span>{buttonText || __('Подробнее', 'agenticlab')}</span>
</div>
</div>
</div>
</>
);
}src/save.tsx#import React from 'react';
import { useBlockProps, RichText } from '@wordpress/block-editor';
interface BlockAttributes {
title: string;
description: string;
buttonText: string;
buttonUrl: string;
badgeText: string;
isFeatured: boolean;
rating: number;
}
export default function Save({ attributes }: { attributes: BlockAttributes }) {
const {
title,
description,
buttonText,
buttonUrl,
badgeText,
isFeatured,
rating
} = attributes;
const blockProps = useBlockProps.save({
className: `agenticlab-hero-card ${isFeatured ? 'is-featured' : ''}`,
});
return (
<div {...blockProps}>
{badgeText && (
<span className="hero-card__badge">{badgeText}</span>
)}
<RichText.Content
tagName="h2"
className="hero-card__title"
value={title}
/>
<RichText.Content
tagName="p"
className="hero-card__description"
value={description}
/>
<div className="hero-card__footer">
<div className="hero-card__rating" aria-label={`Рейтинг ${rating} из 5`}>
{'★'.repeat(rating)}{'☆'.repeat(5 - rating)}
</div>
{buttonUrl && (
<a
href={buttonUrl}
className="hero-card__btn"
target="_blank"
rel="noopener noreferrer nofollow"
>
{buttonText}
</a>
)}
</div>
</div>
);
}src/deprecated.ts#Если в новой версии плагина изменился класс обертки или HTML-структура, старая версия функции save помещается в массив deprecated. WordPress проверит старые сохраненные посты по этой резервной схеме и предотвратит ошибку валидации:
import { useBlockProps, RichText } from '@wordpress/block-editor';
export const deprecatedVersions = [
{
// Версия 0.9.0: использовался старый тег <div> вместо семантичного <article>
attributes: {
title: { type: 'string', source: 'html', selector: 'h2' },
description: { type: 'string', source: 'html', selector: 'p' },
buttonText: { type: 'string', default: 'Подробнее' },
buttonUrl: { type: 'string', default: '' },
rating: { type: 'number', default: 5 }
},
save({ attributes }: any) {
const blockProps = useBlockProps.save({ className: 'old-hero-banner' });
return (
<div {...blockProps}>
<RichText.Content tagName="h2" value={attributes.title} />
<RichText.Content tagName="p" value={attributes.description} />
<a href={attributes.buttonUrl}>{attributes.buttonText}</a>
</div>
);
},
migrate(attributes: any) {
return {
...attributes,
badgeText: 'LEGACY',
isFeatured: false
};
}
}
];agenticlab-hero-block.php#<?php
/**
* Plugin Name: Fullstack Agentic Lab Hero Card Gutenberg Block
* Description: Высокопроизводительный блочный компонент на React 19 и Block API v3.
* Version: 1.0.0
* Author: Artur Yusupov (Fullstack Agentic Lab)
* Requires at least: 6.5
* Requires PHP: 8.2
*/
declare(strict_types=1);
namespace AgenticLab\Blocks\HeroCard;
defined('ABSPATH') || exit;
add_action('init', function (): void {
$build_dir = __DIR__ . '/build';
// Регистрация блока на основе скомпилированного паспорта block.json
if (file_exists($build_dir . '/block.json')) {
register_block_type($build_dir);
}
});Команда приняла решение обновить дизайн: заменить CSS-класс '.hero-card__badge' на '.agenticlab-pill-badge' в файле save.tsx. Блок уже используется на 500 страницах рабочего сайта. Как правильно выкатить это изменение в продакшн?
useBlockProps)#Если забыть применить {...blockProps} к корневому DOM-элементу в edit.tsx или useBlockProps.save() в save.tsx, блок перестанет поддерживать:
align: "wide" и align: "full";supports;save.tsx#Попытка вызова fetch('/wp-json/...') внутри save.tsx приводит к тому, что в базу данных запишется [object Promise] или пустой тег. Если вам необходимы живые данные (например, список последних постов, актуальный курс валют или остатки товара на складе), блок обязан быть динамическим: функция save возвращает null, а вывод генерируется через файл render.php.
index.asset.php#Пакет @wordpress/scripts при сборке генерирует служебный PHP-файл index.asset.php, содержащий список скриптов-зависимостей WordPress (wp-blocks, wp-element, wp-i18n) и хэш версии для сброса кэша браузера. Регистрация через register_block_type(__DIR__ . '/build') считывает этот файл автоматически. Ручное подключение скриптов через wp_enqueue_script без файла ассетов приводит к конфликтам версий React.