Архитектура блочного редактора Gutenberg в WordPress строится на парадигме строгого декларативного контракта между серверной средой (PHP) и клиентским окружением редактора (React). Центральным элементом этой архитектуры является спецификация block.json версии 3 (API v3). В третьей ревизии спецификации редактор Gutenberg изолирует блоки внутри отдельных <iframe>, что исключает взаимное влияние глобальных стилей админки и фронтенда, а также форсирует оптимизированную загрузку ассетов (Script Module API и Deferred Loading).
Генерация блоков с помощью автономных AI-агентов требует глубокого понимания двухфазного жизненного цикла Gutenberg:
edit.tsx): Выполняется исключительно в контексте админки. Отвечает за реактивный UI управления состоянием, взаимодействие с инспектором (InspectorControls) и работу с контекстами через Redux-подобный @wordpress/data store.save.tsx или render.php): Статические блоки компилируют дерево атрибутов в детерминированную HTML-разметку с сохранением в wp_posts.post_content в виде сериализованных комментариев <!-- wp:namespace/block {...} -->...<!-- /wp:namespace/block -->. Динамические блоки сохраняют лишь атрибуты в комментарии, делегируя рендеринг серверному файлу render.php или микро-рантайму Interactivity API.AI-модель при генерации блока обязана строго соблюдать математическую детерминированность функции save(). Малейшее расхождение между сериализованным в базе HTML и результатом выполнения save() при повторном открытии редактора вызывает критическую ошибку — Block Validation Error, превращающую блок в нередактируемый сырой HTML.
Внутреннее устройство современного Gutenberg-блока представляет собой конвейер, где block.json является единым источником истины (Single Source of Truth) для PHP-регистрации, валидации типов в React и сборщика @wordpress/scripts.
[ block.json (v3 Schema & Types) ]
|
+---> [ PHP Registration: register_block_type() ]
| |
| +---> Server Script Modules & Style Enqueue (JIT)
|
+---> [ React Editor: edit.tsx ]
| |
| +---> useBlockProps() + InspectorControls + RichText
|
+---> [ Persistence Layer ]
|
+---> Static: save.tsx (useBlockProps.save() -> post_content)
+---> Dynamic: render.php (Server-Side Execution / Interactivity API)Декларирование атрибутов, стилей, категорий и флагов поддержек (supports: color, spacing, align).
Интеграция UI компонентов Gutenberg, привязка setAttributes и контекста InspectorControls.
Построение чистой семантической разметки с применением useBlockProps.save().
Вызов register_block_type_from_metadata() в хуке init с автоматическим обнаружением ассетов.
Автоматизированный headless-тест с парсингом parse_blocks() для контроля отсутствия Block Invalidation.
theme.json.postmeta заменяются одним компактным блоком с сериализованными JSON-атрибутами.render.php и директивы data-wp-interactive, исключая необходимость писать громоздкие REST API хэндлеры.При разработке блоков под стандарты 2026 года мы категорически отказываемся от монолитных legacy-подходов в пользу модульной TypeScript-архитектуры с декларативным описанием схемы.
Ниже представлен эталонный рабочий артефакт современного коммерческого блока карточки эксперта с поддержкой TypeScript, гибких настроек в инспекторе и серверной валидацией.
/**
* Author: Artur Yusupov (Fullstack Agentic Lab)
* Package: @agentic-lab/gutenberg-blocks
* Module: expert-card
* Standard: WordPress 6.8+ / 7.0 Alpha, React 19, block.json v3
*/
// ==========================================
// 1. block.json (API Version 3 Specification)
// ==========================================
/*
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "agentic-lab/expert-card",
"version": "1.0.0",
"title": "Agentic Expert Card",
"category": "widgets",
"icon": "businessperson",
"description": "Enterprise-grade expert profile card with structured Schema.org metadata.",
"supports": {
"html": false,
"align": ["wide", "full"],
"color": {
"background": true,
"text": true,
"gradients": true
},
"spacing": {
"margin": true,
"padding": true
}
},
"attributes": {
"expertName": {
"type": "string",
"source": "html",
"selector": ".agentic-expert-name",
"default": ""
},
"expertRole": {
"type": "string",
"default": "Principal Architect"
},
"expertBio": {
"type": "string",
"source": "html",
"selector": ".agentic-expert-bio",
"default": ""
},
"avatarId": {
"type": "number",
"default": 0
},
"avatarUrl": {
"type": "string",
"default": ""
},
"socialLinkedin": {
"type": "string",
"default": ""
}
},
"textdomain": "agentic-lab",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}
*/
// ==========================================
// 2. types.ts
// ==========================================
export interface ExpertCardAttributes {
expertName: string;
expertRole: string;
expertBio: string;
avatarId: number;
avatarUrl: string;
socialLinkedin: string;
}
export interface BlockEditProps {
attributes: ExpertCardAttributes;
setAttributes: (attrs: Partial<ExpertCardAttributes>) => void;
isSelected: boolean;
}
// ==========================================
// 3. edit.tsx (Editor Component)
// ==========================================
import React from 'react';
import { __ } from '@wordpress/i18n';
import {
useBlockProps,
RichText,
InspectorControls,
MediaUpload,
MediaUploadCheck
} from '@wordpress/block-editor';
import {
PanelBody,
TextControl,
Button,
ResponsiveWrapper
} from '@wordpress/components';
export function Edit({ attributes, setAttributes }: BlockEditProps): React.JSX.Element {
const { expertName, expertRole, expertBio, avatarId, avatarUrl, socialLinkedin } = attributes;
const blockProps = useBlockProps({
className: 'agentic-expert-card-container'
});
const onSelectImage = (media: { id: number; url: string }) => {
setAttributes({
avatarId: media.id,
avatarUrl: media.url
});
};
const onRemoveImage = () => {
setAttributes({ avatarId: 0, avatarUrl: '' });
};
return (
<>
<InspectorControls>
<PanelBody title={__('Expert Credentials', 'agentic-lab')} initialOpen={true}>
<TextControl
label={__('Professional Role / Title', 'agentic-lab')}
value={expertRole}
onChange={(val: string) => setAttributes({ expertRole: val })}
help={__('Displayed beneath the expert name.', 'agentic-lab')}
/>
<TextControl
label={__('LinkedIn Profile URL', 'agentic-lab')}
value={socialLinkedin}
onChange={(val: string) => setAttributes({ socialLinkedin: val })}
placeholder="https://linkedin.com/in/username"
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
<div className="agentic-expert-avatar-wrap">
<MediaUploadCheck>
<MediaUpload
onSelect={onSelectImage}
allowedTypes={['image']}
value={avatarId}
render={({ open }) => (
<div className="avatar-uploader">
{avatarUrl ? (
<div className="avatar-preview">
<img src={avatarUrl} alt={expertName || 'Expert'} />
<Button
onClick={onRemoveImage}
isDestructive
variant="link"
className="remove-avatar-btn"
>
{__('Remove Image', 'agentic-lab')}
</Button>
</div>
) : (
<Button onClick={open} variant="secondary" className="upload-avatar-btn">
{__('Upload Photo', 'agentic-lab')}
</Button>
)}
</div>
)}
/>
</MediaUploadCheck>
</div>
<div className="agentic-expert-details">
<RichText
tagName="h3"
className="agentic-expert-name"
value={expertName}
onChange={(val: string) => setAttributes({ expertName: val })}
placeholder={__('Enter Full Name...', 'agentic-lab')}
allowedFormats={['core/bold', 'core/italic']}
/>
<span className="agentic-expert-role-badge">{expertRole}</span>
<RichText
tagName="p"
className="agentic-expert-bio"
value={expertBio}
onChange={(val: string) => setAttributes({ expertBio: val })}
placeholder={__('Write expert biography and credentials...', 'agentic-lab')}
/>
{socialLinkedin && (
<a
href={socialLinkedin}
className="agentic-expert-social-link"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.preventDefault()}
>
{__('LinkedIn Profile →', 'agentic-lab')}
</a>
)}
</div>
</div>
</>
);
}
// ==========================================
// 4. save.tsx (Deterministic HTML Serialization)
// ==========================================
export function Save({ attributes }: { attributes: ExpertCardAttributes }): React.JSX.Element {
const { expertName, expertRole, expertBio, avatarUrl, socialLinkedin } = attributes;
const blockProps = useBlockProps.save({
className: 'agentic-expert-card-container'
});
return (
<div {...blockProps} itemScope itemType="https://schema.org/Person">
{avatarUrl && (
<div className="agentic-expert-avatar-wrap">
<img src={avatarUrl} alt={expertName || 'Expert Profile'} itemProp="image" />
</div>
)}
<div className="agentic-expert-details">
<RichText.Content tagName="h3" className="agentic-expert-name" value={expertName} itemProp="name" />
<span className="agentic-expert-role-badge" itemProp="jobTitle">
{expertRole}
</span>
<RichText.Content tagName="p" className="agentic-expert-bio" value={expertBio} itemProp="description" />
{socialLinkedin && (
<a
href={socialLinkedin}
className="agentic-expert-social-link"
target="_blank"
rel="noopener noreferrer"
itemProp="sameAs"
>
LinkedIn Profile →
</a>
)}
</div>
</div>
);
}
// ==========================================
// 5. Registration in PHP (plugin.php)
// ==========================================
/*
<?php
declare(strict_types=1);
namespace AgenticLab\GutenbergBlocks;
add_action('init', static function (): void {
register_block_type_from_metadata(__DIR__ . '/build');
});
*/Симптом: При открытии статьи в редакторе возникает предупреждение "This block contains unexpected or invalid content" (Block Validation Error).
Причина: Функция save() возвращает разметку, зависящую от динамических параметров: текущего времени (Date.now()), случайных ID (Math.random()), или не экранирует символы так же, как парсер WordPress.
Решение: Функция save() должна быть абсолютно чистой (pure function). Все уникальные идентификаторы должны генерироваться один раз в edit.tsx и сохраняться в статические атрибуты блока.
Симптом: Стили блока корректно отображаются на фронтенде сайта, но в редакторе Gutenberg верстка разваливается или шрифты не подгружаются.
Причина: В apiVersion: 3 редактор изолирует полотно в <iframe>. Если стили блока зарегистрированы только через глобальный хук wp_enqueue_scripts, они не попадают внутрь iframe.
Решение: Обязательно регистрировать стили блока через свойства editorStyle и style непосредственно в block.json, либо подключать через add_editor_style().
Симптом: Изменения в компонентах инспектора или текстовых полях не сохраняются при публикации страницы.
Причина: Прямая модификация объекта атрибутов: attributes.expertRole = 'Lead Architect'. Redux-хранилище Gutenberg не фиксирует мутацию и не ставит флаг isDirty.
Решение: Всегда использовать иммутабельный сеттер: setAttributes({ expertRole: 'Lead Architect' }).
Симптом: Блок на фронтенде теряет классы выравнивания (alignwide, alignfull), пользовательские CSS-классы и встроенные стили цветов темы.
Причина: В функции save() корневой HTML-тег отрендерен без spread-оператора атрибутов useBlockProps.save().
Решение: Всегда передавать <div {...useBlockProps.save({ className: 'custom-wrapper' })}>.
Генерация block.json v3 со строгим описанием атрибутов, сорсинга из HTML и поддержек theme.json.
Интеграция RichText, InspectorControls и безопасных сеттеров состояния.
Рендеринг семантического HTML с микроразметкой Schema.org и хуком useBlockProps.save().
Запуск Vitest с фикстурами сериализованного HTML для исключения регрессий валидации.