title: 'Edit and Save Architecture, Block Grammar Serialization, and Deprecated Migrations' description: >- Engineering breakdown of static block lifecycles in Gutenberg (WordPress 7.0+, TypeScript 5.8): useBlockProps operation inside isolated iframe canvases, AST verification upon post opening, and seamless markup migration via deprecated arrays without validation errors. module: gutenberg-blocks order: 2 difficulty: intermediate estimatedTime: 38 min topics:
The fundamental distinction between Gutenberg architecture and traditional React single-page frameworks is that a static block exists across two completely different temporal dimensions.
The first dimension is the Interactive Authoring Runtime (Edit): a full-fledged React application running inside the WordPress editor, connected to the reactive @wordpress/data store (core/block-editor, core/data). Here, the author modifies sidebar settings (InspectorControls), edits text in place (RichText), and drags blocks inside an isolated <iframe> canvas.
The second dimension is the Deterministic AST Serializer (save): a pure, synchronous function completely stripped of state and React lifecycle hooks (useState, useEffect, useSelect are strictly forbidden here). When the author clicks "Save" in the admin interface, save({ attributes }) converts the JSON attribute payload into a pure HTML string wrapped in WordPress grammar comment delimiters (<!-- wp:agentic-lab/callout {...} -->...<!-- /wp:agentic-lab/callout -->), persisting it directly into the wp_posts.post_content column. On the public frontend, this HTML is served to the client browser instantly with zero React runtime overhead and zero Virtual DOM creation.
The cost of this frontend velocity is the strict Block Validation Contract. Every time a post is reopened in the Gutenberg editor, the core retrieves the persisted HTML from the database, executes the current save() function against the saved attributes in memory, and compares both DOM trees character by character. If an engineer updates the plugin, changes an <h2> to <h3>, or injects a single CSS class in save() without declaring a backward-compatibility rule in the deprecated array, every previously published block across hundreds of site pages instantly fails with a critical error: "This block contains unexpected or invalid content".
Think of the Edit component as Computer-Aided Design (CAD) 3D modeling software, where an engineer rotates schematics, tweaks dimensions, and selects materials. Think of the save() function as an industrial 3D printer casting a final concrete block and shipping it directly to the warehouse (wp_posts.post_content).
When you retrieve that concrete part from the warehouse six months later for refurbishment (reopening the post in the editor), the inspection rig checks the geometry of the old concrete piece against the current save() mold. If you modified the mold shape at the factory without maintaining blueprints, the inspection machine rejects the piece (Block Validation Error) until you load blueprints of previous revisions (deprecated) equipped with clear instructions on how to recool and transform the v1 part into the v2 specification (migrate()).
┌─────────────────────────────────────────────────────────────────────────┐
│ OPENING A POST IN GUTENBERG EDITOR (VALIDATION) │
└────────────────────────────────────┬────────────────────────────────────┘
│
┌───────────────────────────┴───────────────────────────┐
▼ ▼
[HTML from Database] [Attributes from Comment]
<!-- wp:agentic/cta {"v":1} --> Run through current save(attrs)
<div class="cta-v1">...</div> │
│ ▼
│ <section class="cta-v2">...
└───────────────────────────┬───────────────────────────┘
▼
Do both DOM trees match?
├── YES ──► Block mounts cleanly in Edit()
└── NO ───► Traverse deprecated[0..N] array:
Matching legacy save() found?
├── YES ──► Call migrate(oldAttrs) ──► Auto-upgraded to v2!
└── NO ───► CRITICAL ERROR: Block Validation Error!The @wordpress/block-serialization-default-parser splits post_content into an AST, extracting JSON attribute payloads and innerHTML.
The core calls the current save({ attributes }) function in browser memory and serializes the resulting React element into canonical HTML.
The isEquivalentHTML algorithm normalizes whitespace and attribute ordering, comparing the canonical string against persisted innerHTML.
Upon a mismatch, the core tests deprecated[] entries sequentially. The first matching entry transforms attributes via migrate().
The validated block passes into Edit(), which binds focus handlers and selection boundaries to the root DOM node via useBlockProps().
Across Fullstack Agentic Lab production deployments, Edit / Save architectures with deprecated pipelines are utilized for:
<div class="box">) to v2 (<aside class="agentic-callout">), the deprecated array automatically migrates thousands of legacy articles upon editor opening without manual editorial intervention.@wordpress/data): inside Edit(), blocks dynamically query authors, taxonomies, or global settings via useSelect(), writing a curated snapshot into the final static save() output or deferring live queries to render.php.Comparing architectural constraints between the interactive Edit() environment and the deterministic save() serializer:
Below is a production-grade TypeScript 5.8 artifact for an architecture decision alert block (agentic-lab/architecture-alert). It illustrates proper useBlockProps usage in apiVersion: 3, clean separation between BlockControls and InspectorControls, and an enterprise-ready deprecated migration pipeline handling both markup changes (div → section) and schema shifts (alertText → heading + severityLevel).
/**
* Fullstack Agentic Lab — Architecture Alert Block (Block API v3)
* Stack: 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. Current attribute contract for version v2 (2026)
export interface AlertBlockAttributesV2 {
heading: string;
bodyContent: string;
severityLevel: 'info' | 'warning' | 'critical';
isDismissible: boolean;
}
// Deprecated attribute contract for legacy version v1 (for strict migration typing)
interface AlertBlockAttributesV1 {
alertText: string;
isDanger: boolean;
}
/**
* 2. Backward compatibility migration pipeline (deprecated).
* Ordered from newest deprecated version to oldest.
*/
const deprecatedMigrations: BlockDeprecation<AlertBlockAttributesV2>[] = [
{
// Schema of legacy v1 attributes
attributes: {
alertText: {
type: 'string',
default: '',
},
isDanger: {
type: 'boolean',
default: false,
},
},
// Validates whether this deprecation handler applies to the given legacy block
isEligible(attributes: Record<string, unknown>): boolean {
return typeof attributes.alertText === 'string' || typeof attributes.isDanger === 'boolean';
},
// Pure attribute transformation function: v1 -> v2
migrate(oldAttributes: Record<string, unknown>): AlertBlockAttributesV2 {
const v1 = oldAttributes as unknown as AlertBlockAttributesV1;
return {
heading: v1.isDanger ? 'Critical Architectural Constraint' : 'Engineering Note',
bodyContent: v1.alertText || '',
severityLevel: v1.isDanger ? 'critical' : 'info',
isDismissible: false,
};
},
// Exact replica of the legacy v1 save() function!
// Solely required to verify that database HTML was legitimately created under v1.
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. Modern v2 block registration supporting 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 automatically injects ref and classes inside the isolated iframe
const blockProps = useBlockProps({
className: `agentic-alert-v2 is-severity-${severityLevel}`,
'data-dismissible': isDismissible ? 'true' : 'false',
});
return (
<>
{/* Rapid severity switcher in floating toolbar */}
<BlockControls>
<ToolbarGroup>
{(['info', 'warning', 'critical'] as const).map((level) => (
<ToolbarButton
key={level}
label={`Severity: ${level.toUpperCase()}`}
isPressed={severityLevel === level}
onClick={() => setAttributes({ severityLevel: level })}
>
{level.toUpperCase()}
</ToolbarButton>
))}
</ToolbarGroup>
</BlockControls>
{/* Detailed sidebar configuration panels */}
<InspectorControls>
<PanelBody title={__('Alert Parameters', 'agentic-lab')} initialOpen={true}>
<SelectControl
label={__('Severity Level', 'agentic-lab')}
value={severityLevel}
options={[
{ label: 'Info (Engineering Note)', value: 'info' },
{ label: 'Warning (System Warning)', value: 'warning' },
{ label: 'Critical (Production Blocker)', value: 'critical' },
]}
onChange={(value) =>
setAttributes({ severityLevel: value as AlertBlockAttributesV2['severityLevel'] })
}
/>
<ToggleControl
label={__('Allow User Dismissal', 'agentic-lab')}
checked={isDismissible}
onChange={(val) => setAttributes({ isDismissible: Boolean(val) })}
/>
</PanelBody>
</InspectorControls>
{/* Semantic v2 markup rendered inside the iframe canvas */}
<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={__('Architectural rule heading...', 'agentic-lab')}
allowedFormats={['core/code']}
/>
</header>
<RichText
tagName="div"
className="agentic-alert-v2__body"
value={bodyContent}
onChange={(val) => setAttributes({ bodyContent: val })}
placeholder={__('Describe technical context and trade-offs...', 'agentic-lab')}
allowedFormats={['core/bold', 'core/italic', 'core/code', 'core/link']}
/>
</section>
</>
);
},
// 4. Deterministic v2 serializer (100% pure function with zero side-effects!)
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>
);
},
// Link backward-compatibility pipeline for v1 markup
deprecated: deprecatedMigrations,
});
Before editing any HTML, copy existing attributes definitions and the current save() implementation into a new entry in deprecated[].
Implement migrate() to receive the legacy attribute dictionary and return a complete modern attribute object with all default fields populated.
Introduce new semantic tags, class names, and UI controls into the main block component, keeping deprecated snapshots strictly intact.
Paste raw v1 HTML comments into Gutenberg's Code Editor view and verify that switching to Visual Editor upgrades the block to v2 silently without console errors.