title: 'Архітектура Edit та Save, серіалізація граматики блоків і міграції deprecated' description: >- Інженерний розбір життєвого циклу статичних блоків у Gutenberg (WordPress 7.0+, TypeScript 5.8): робота useBlockProps всередині ізольованого iframe-полотна, алгоритм верифікації AST під час відкриття публікації та безшовна міграція верстки через масив deprecated без помилок валідації. module: gutenberg-blocks order: 2 difficulty: intermediate estimatedTime: 38 хв topics:
Головна відмінність архітектури Gutenberg від традиційних React-фреймворків полягає в тому, що статичний блок живе у двох абсолютно різних часових вимірах.
Перший вимір — Interactive Authoring Runtime (Edit): повноцінний React-застосунок усередині редактора WordPress, підключений до реактивного сховища @wordpress/data (core/block-editor, core/data), де редактор змінює налаштування в сайдбарі (InspectorControls), редагує текст на льоту (RichText) та перетягує елементи всередині ізольованого <iframe>-полотна.
Другий вимір — Deterministic AST Serializer (save): чиста синхронна функція без внутрішнього стану та без хуків життєвого циклу React (useState, useEffect, useSelect у ній категорично заборонені). У момент натискання кнопки «Зберегти» в адмінці функція save({ attributes }) перетворює JSON-об'єкт атрибутів на рядок чистого HTML, обрамлений коментарями граматики WordPress (<!-- wp:agentic-lab/callout {...} -->...<!-- /wp:agentic-lab/callout -->), і записує його безпосередньо в колонку wp_posts.post_content. На публічному фронтенді цей HTML віддається браузеру миттєво без запуску React і без створення Virtual DOM.
Ціна такої швидкості на фронтенді — суворий Контракт Детермінованої Валідації (Block Validation Contract). Під час кожного повторного відкриття статті в редакторі Gutenberg ядро бере збережений у БД HTML, паралельно пропускає збережені атрибути через поточну функцію save() і посимвольно порівнює обидва DOM-дерева. Якщо розробник оновив плагін і змінив <h2> на <h3> або додав один CSS-клас у save(), не описавши правило зворотної сумісності в масиві deprecated, усі раніше створені блоки на сотнях сторінок сайту миттєво випадуть у критичну помилку «This block contains unexpected or invalid content».
Уявіть компонент Edit як 3D-програму архітектурного моделювання (CAD), де інженер обертає креслення, змінює розміри та матеріали, а функцію save() — як промисловий 3D-принтер, який відливає готову бетонну деталь і відправляє її на склад (wp_posts.post_content).
Коли через пів року ви повертаєте деталь зі складу назад у цех для доопрацювання (відкриваєте публікацію в редакторі), верстат перевіряє форму старої бетонної деталі з поточною матрицею save(). Якщо ви змінили форму матриці на заводі, верстат забракує стару деталь (Block Validation Error), доки ви не завантажите в нього креслення попередніх ревізій (deprecated) з інструкцією, як автоматично переплавити деталь v1 у формат v2 (migrate()).
┌─────────────────────────────────────────────────────────────────────────┐
│ ВІДКРИТТЯ ПУБЛІКАЦІЇ В РЕДАКТОРІ GUTENBERG (ВАЛІДАЦІЯ) │
└────────────────────────────────────┬────────────────────────────────────┘
│
┌───────────────────────────┴───────────────────────────┐
▼ ▼
[HTML із бази даних] [Атрибути з коментаря]
<!-- wp:agentic/cta {"v":1} --> Прогін через поточний save(attrs)
<div class="cta-v1">...</div> │
│ ▼
│ <section class="cta-v2">...
└───────────────────────────┬───────────────────────────┘
▼
Чи збігаються обидва DOM-дерева?
├── ТАК ──► Блок монтується в Edit()
└── НІ ───► Обхід масиву deprecated[0..N]:
Знайдено збіг зі старою save()?
├── ТАК ──► Виклик migrate(oldAttrs) ──► Авто-апгрейд у v2!
└── НІ ───► КРИТИЧНА ПОМИЛКА: Block Validation Error!Парсер @wordpress/block-serialization-default-parser розбиває post_content на дерево блоків, витягуючи JSON-атрибути та innerHTML.
Ядро викликає актуальну функцію save({ attributes }) у пам'яті браузера та серіалізує отриманий React-елемент в еталонний HTML-рядок.
Алгоритм isEquivalentHTML нормалізує пробіли й порядок атрибутів і порівнює еталонний рядок зі збереженим у базі innerHTML.
При розбіжності ядро по черзі тестує записи з масиву deprecated[]. Перший відповідний запис трансформує атрибути через migrate().
Валідний блок передається в компонент Edit(), який прив'язує обробники фокусу та виділення до кореневого вузла через useBlockProps().
У проєктах Fullstack Agentic Lab архітектура Edit / Save із масивом deprecated застосовується для:
<div class="box">) на v2 (<aside class="agentic-callout">), масив deprecated автоматично мігрує тисячі старих статей під час їх відкриття в редакторі без жодного ручного втручання.@wordpress/data): усередині Edit() блок може динамічно підтягувати список авторів, таксономій або глобальних налаштувань сайту через useSelect(), зберігаючи в підсумковий статичний save() лише вивірений зліпок або делегуючи динаміку в render.php.Порівняємо архітектурні правила та обмеження між середовищем редагування Edit() і серіалізатором save():
Нижче наведено виробничий артефакт на TypeScript 5.8 для блока інженерного сповіщення (agentic-lab/architecture-alert). Він демонструє правильну роботу з useBlockProps в apiVersion: 3, розділення панелей BlockControls та InspectorControls, а також повноцінний конвеєр міграції deprecated під час зміни як HTML-структури (div → section), так і схеми атрибутів (alertText → heading + severityLevel).
/**
* Fullstack Agentic Lab — Architecture Alert Block (Block API v3)
* Стек: WordPress 7.0+, TypeScript 5.8, @wordpress/block-editor
*/
import { registerBlockType, type BlockDeprecation } from '@wordpress/blocks';
import {
useBlockProps,
RichText,
InspectorControls,
BlockControls,
} from '@wordpress/block-editor';
import {
PanelBody,
SelectControl,
ToggleControl,
ToolbarGroup,
ToolbarButton,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
// 1. Актуальний контракт атрибутів версії v2 (2026)
export interface AlertBlockAttributesV2 {
heading: string;
bodyContent: string;
severityLevel: 'info' | 'warning' | 'critical';
isDismissible: boolean;
}
// Застарілий контракт атрибутів версії v1 (для строгої типізації мігратора)
interface AlertBlockAttributesV1 {
alertText: string;
isDanger: boolean;
}
/**
* 2. Масив зворотної сумісності (deprecated).
* Розташовується в порядку від найсвіжішої застарілої версії до найстарішої.
*/
const deprecatedMigrations: BlockDeprecation<AlertBlockAttributesV2>[] = [
{
// Схема атрибутів старої версії v1
attributes: {
alertText: {
type: 'string',
default: '',
},
isDanger: {
type: 'boolean',
default: false,
},
},
// Перевірка, чи підходить цей мігратор для конкретного старого блока
isEligible(attributes: Record<string, unknown>): boolean {
return typeof attributes.alertText === 'string' || typeof attributes.isDanger === 'boolean';
},
// Чиста функція трансформації атрибутів v1 -> v2
migrate(oldAttributes: Record<string, unknown>): AlertBlockAttributesV2 {
const v1 = oldAttributes as unknown as AlertBlockAttributesV1;
return {
heading: v1.isDanger ? 'Критичне архітектурне обмеження' : 'Інженерна примітка',
bodyContent: v1.alertText || '',
severityLevel: v1.isDanger ? 'critical' : 'info',
isDismissible: false,
};
},
// Точна копія старої функції save() версії v1!
// Потрібна лише для того, щоб парсер переконався, що HTML у БД був створений легально.
save({ attributes }: { attributes: Record<string, unknown> }) {
const v1 = attributes as unknown as AlertBlockAttributesV1;
const legacyProps = useBlockProps.save({
className: `agentic-alert-v1 ${v1.isDanger ? 'is-danger' : 'is-info'}`,
});
return (
<div {...legacyProps}>
<RichText.Content tagName="p" value={v1.alertText} />
</div>
);
},
},
];
/**
* 3. Реєстрація блока версії v2 з підтримкою apiVersion: 3.
*/
registerBlockType<AlertBlockAttributesV2>('agentic-lab/architecture-alert', {
apiVersion: 3,
title: __('Architecture Decision Alert', 'agentic-lab'),
category: 'agentic-lab-architecture',
icon: 'shield-alt',
edit({ attributes, setAttributes }) {
const { heading, bodyContent, severityLevel, isDismissible } = attributes;
// useBlockProps автоматично інжектує ref і класи всередині ізольованого iframe
const blockProps = useBlockProps({
className: `agentic-alert-v2 is-severity-${severityLevel}`,
'data-dismissible': isDismissible ? 'true' : 'false',
});
return (
<>
{/* Швидке перемикання критичності прямо у спливаючому тулбарі над блоком */}
<BlockControls>
<ToolbarGroup>
{(['info', 'warning', 'critical'] as const).map((level) => (
<ToolbarButton
key={level}
label={`Рівень: ${level.toUpperCase()}`}
isPressed={severityLevel === level}
onClick={() => setAttributes({ severityLevel: level })}
>
{level.toUpperCase()}
</ToolbarButton>
))}
</ToolbarGroup>
</BlockControls>
{/* Детальні налаштування в правому сайдбарі (Inspector) */}
<InspectorControls>
<PanelBody title={__('Параметри алерта', 'agentic-lab')} initialOpen={true}>
<SelectControl
label={__('Рівень критичності (Severity)', 'agentic-lab')}
value={severityLevel}
options={[
{ label: 'Info (Інженерна замітка)', value: 'info' },
{ label: 'Warning (Попередження)', value: 'warning' },
{ label: 'Critical (Блокер продакшену)', value: 'critical' },
]}
onChange={(value) =>
setAttributes({ severityLevel: value as AlertBlockAttributesV2['severityLevel'] })
}
/>
<ToggleControl
label={__('Дозволити приховування користувачем', 'agentic-lab')}
checked={isDismissible}
onChange={(val) => setAttributes({ isDismissible: Boolean(val) })}
/>
</PanelBody>
</InspectorControls>
{/* Семантична розмітка v2 всередині iframe-полотна */}
<section {...blockProps} role="note">
<header className="agentic-alert-v2__header">
<span className="agentic-alert-v2__badge">{severityLevel.toUpperCase()}</span>
<RichText
tagName="h4"
className="agentic-alert-v2__title"
value={heading}
onChange={(val) => setAttributes({ heading: val })}
placeholder={__('Заголовок архітектурного правила...', 'agentic-lab')}
allowedFormats={['core/code']}
/>
</header>
<RichText
tagName="div"
className="agentic-alert-v2__body"
value={bodyContent}
onChange={(val) => setAttributes({ bodyContent: val })}
placeholder={__('Опишіть технічний контекст і наслідки...', 'agentic-lab')}
allowedFormats={['core/bold', 'core/italic', 'core/code', 'core/link']}
/>
</section>
</>
);
},
// 4. Детермінований серіалізатор v2 (100% чиста функція без сайд-ефектів!)
save({ attributes }) {
const { heading, bodyContent, severityLevel, isDismissible } = attributes;
const blockProps = useBlockProps.save({
className: `agentic-alert-v2 is-severity-${severityLevel}`,
'data-dismissible': isDismissible ? 'true' : 'false',
});
return (
<section {...blockProps} role="note">
<header className="agentic-alert-v2__header">
<span className="agentic-alert-v2__badge">{severityLevel.toUpperCase()}</span>
<RichText.Content tagName="h4" className="agentic-alert-v2__title" value={heading} />
</header>
<RichText.Content tagName="div" className="agentic-alert-v2__body" value={bodyContent} />
</section>
);
},
// Підключаємо конвеєр міграції зі старої розмітки v1
deprecated: deprecatedMigrations,
});
Перед внесенням будь-яких правок до HTML скопіюйте поточні об'єкти attributes та функцію save() у новий елемент масиву deprecated[].
Реалізуйте метод migrate(), який приймає старий словник атрибутів і повертає повний об'єкт атрибутів нової версії з усіма дефолтними полями.
Внесіть нові семантичні теги, класи та контроли в основний компонент блока, зберігаючи стару версію в deprecated недоторканною.
Вставте старий HTML-коментар v1 у редактор коду Gutenberg і переконайтеся, що під час перемикання у візуальний режим блок мігрує у v2 тихо й без помилок у консолі.