title: 'Arquitectura de Edit y Save, Serialización de Gramática y Migraciones deprecated' description: >- Análisis de ingeniería sobre el ciclo de vida de los bloques estáticos en Gutenberg (WordPress 7.0+, TypeScript 5.8): funcionamiento de useBlockProps en lienzos iframe aislados, algoritmo de verificación AST al abrir entradas y migración fluida del marcado mediante matrices deprecated sin errores de validación. module: gutenberg-blocks order: 2 difficulty: intermediate estimatedTime: 38 min topics:
La principal distinción entre la arquitectura de Gutenberg y los frameworks tradicionales de React radica en que un bloque estático existe en dos dimensiones temporales radicalmente distintas.
La primera dimensión es el Interactive Authoring Runtime (Edit): una aplicación React completa ejecutada dentro del editor de WordPress, conectada a los almacenes reactivos de @wordpress/data (core/block-editor, core/data), donde el autor manipula opciones en la barra lateral (InspectorControls), edita texto en tiempo real (RichText) y arrastra elementos dentro de un lienzo <iframe> aislado.
La segunda dimensión es el Deterministic AST Serializer (save): una función sincrónica y pura sin estado ni hooks del ciclo de vida de React (useState, useEffect, useSelect están estrictamente prohibidos en ella). En el momento en que se presiona el botón «Guardar», save({ attributes }) convierte el objeto JSON de atributos en una cadena de HTML puro, delimitada por los comentarios de la gramática de WordPress (<!-- wp:agentic-lab/callout {...} -->...<!-- /wp:agentic-lab/callout -->), y la persiste directamente en la columna wp_posts.post_content. En el frontend público, este HTML se entrega al navegador de forma instantánea sin ejecutar React y sin instanciar un Virtual DOM.
El precio de esta velocidad en el frontend es el riguroso Contrato de Validación Determinista (Block Validation Contract). Cada vez que se reabre una entrada en Gutenberg, el núcleo recupera el HTML persistido en la base de datos, ejecuta en paralelo la función save() actual contra los atributos guardados y compara ambos árboles DOM carácter por carácter. Si un desarrollador actualiza el plugin, cambia un <h2> por un <h3> o añade una sola clase CSS en save() sin declarar una regla de compatibilidad hacia atrás en la matriz deprecated, todos los bloques creados previamente en cientos de páginas del sitio fallarán de inmediato con el error crítico: «This block contains unexpected or invalid content».
Imagina el componente Edit como un software de modelado arquitectónico 3D (CAD), donde el ingeniero rota planos, ajusta medidas y selecciona materiales. Imagina la función save() como una impresora 3D industrial, que funde una pieza de hormigón terminada y la envía directamente al almacén (wp_posts.post_content).
Cuando seis meses después traes esa pieza de hormigón de vuelta al taller para modificarla (abrir la entrada en el editor), la máquina de inspección compara la geometría de la pieza antigua con el molde actual de save(). Si alteraste la forma del molde en fábrica, la máquina rechazará la pieza antigua (Block Validation Error) hasta que le suministres los planos de revisiones previas (deprecated) con las instrucciones precisas para transformar automáticamente la pieza v1 al formato v2 (migrate()).
┌─────────────────────────────────────────────────────────────────────────┐
│ APERTURA DE ENTRADA EN EL EDITOR GUTENBERG (VALIDACIÓN) │
└────────────────────────────────────┬────────────────────────────────────┘
│
┌───────────────────────────┴───────────────────────────┐
▼ ▼
[HTML de la Base de Datos] [Atributos del Comentario]
<!-- wp:agentic/cta {"v":1} --> Paso por save(attrs) actual
<div class="cta-v1">...</div> │
│ ▼
│ <section class="cta-v2">...
└───────────────────────────┬───────────────────────────┘
▼
¿Coinciden ambos árboles DOM?
├── SÍ ──► El bloque se monta en Edit()
└── NO ──► Recorrido de la matriz deprecated[0..N]:
¿Se halló coincidencia con save() antiguo?
├── SÍ ──► Llamada a migrate(oldAttrs) ──► ¡Auto-upgrade a v2!
└── NO ──► ERROR CRÍTICO: ¡Block Validation Error!El analizador @wordpress/block-serialization-default-parser descompone post_content en un AST, extrayendo atributos JSON e innerHTML.
El núcleo ejecuta la función save({ attributes }) actual en memoria y serializa el elemento React resultante en una cadena HTML canónica.
El algoritmo isEquivalentHTML normaliza espacios y orden de atributos, comparando la cadena canónica con el innerHTML guardado.
Ante una discrepancia, el núcleo evalúa ordenadamente las entradas de deprecated[]. La primera coincidencia transforma atributos vía migrate().
El bloque validado pasa al componente Edit(), que enlaza controladores de foco y selección al nodo raíz mediante useBlockProps().
En los despliegues empresariales de Fullstack Agentic Lab, la arquitectura Edit / Save con matrices deprecated se utiliza para:
<div class="box">) a v2 (<aside class="agentic-callout">), la matriz deprecated migra automáticamente miles de artículos antiguos al abrirlos en el editor sin intervención manual de los redactores.@wordpress/data): dentro de Edit(), el bloque consulta autores, taxonomías o ajustes globales mediante useSelect(), persistiendo en el save() estático únicamente una instantánea limpia o delegando la dinámica a render.php.Comparación de reglas y restricciones entre el entorno interactivo Edit() y el serializador determinista save():
A continuación se muestra el artefacto de producción en TypeScript 5.8 para un bloque de notificación de arquitectura (agentic-lab/architecture-alert). Demuestra el manejo riguroso de useBlockProps en apiVersion: 3, la separación entre BlockControls e InspectorControls, y una tubería de migración deprecated completa ante modificaciones de marcado (div → section) y evolución de atributos (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. Contrato actual de atributos de la versión v2 (2026)
export interface AlertBlockAttributesV2 {
heading: string;
bodyContent: string;
severityLevel: 'info' | 'warning' | 'critical';
isDismissible: boolean;
}
// Contrato de atributos heredado v1 (para tipado estricto del migrador)
interface AlertBlockAttributesV1 {
alertText: string;
isDanger: boolean;
}
/**
* 2. Matriz de compatibilidad hacia atrás (deprecated).
* Ordenada desde la versión obsoleta más reciente hacia la más antigua.
*/
const deprecatedMigrations: BlockDeprecation<AlertBlockAttributesV2>[] = [
{
// Esquema de atributos de la versión v1
attributes: {
alertText: {
type: 'string',
default: '',
},
isDanger: {
type: 'boolean',
default: false,
},
},
// Comprobación de si este migrador corresponde al bloque heredado
isEligible(attributes: Record<string, unknown>): boolean {
return typeof attributes.alertText === 'string' || typeof attributes.isDanger === 'boolean';
},
// Función pura de transformación de atributos v1 -> v2
migrate(oldAttributes: Record<string, unknown>): AlertBlockAttributesV2 {
const v1 = oldAttributes as unknown as AlertBlockAttributesV1;
return {
heading: v1.isDanger ? 'Restricción Arquitectónica Crítica' : 'Nota de Ingeniería',
bodyContent: v1.alertText || '',
severityLevel: v1.isDanger ? 'critical' : 'info',
isDismissible: false,
};
},
// ¡Copia exacta de la función save() antigua de la versión v1!
// Solo requerida para asegurar al analizador que el HTML en la BD fue legítimo.
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. Registro del bloque versión v2 con soporte para 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 inyecta automáticamente ref y clases dentro del iframe aislado
const blockProps = useBlockProps({
className: `agentic-alert-v2 is-severity-${severityLevel}`,
'data-dismissible': isDismissible ? 'true' : 'false',
});
return (
<>
{/* Selector rápido de severidad en la barra flotante */}
<BlockControls>
<ToolbarGroup>
{(['info', 'warning', 'critical'] as const).map((level) => (
<ToolbarButton
key={level}
label={`Nivel: ${level.toUpperCase()}`}
isPressed={severityLevel === level}
onClick={() => setAttributes({ severityLevel: level })}
>
{level.toUpperCase()}
</ToolbarButton>
))}
</ToolbarGroup>
</BlockControls>
{/* Paneles de configuración detallada en la barra lateral */}
<InspectorControls>
<PanelBody title={__('Parámetros del Aviso', 'agentic-lab')} initialOpen={true}>
<SelectControl
label={__('Nivel de Severidad', 'agentic-lab')}
value={severityLevel}
options={[
{ label: 'Info (Nota Técnica)', value: 'info' },
{ label: 'Warning (Advertencia)', value: 'warning' },
{ label: 'Critical (Bloqueador de Producción)', value: 'critical' },
]}
onChange={(value) =>
setAttributes({ severityLevel: value as AlertBlockAttributesV2['severityLevel'] })
}
/>
<ToggleControl
label={__('Permitir Ocultar por Usuario', 'agentic-lab')}
checked={isDismissible}
onChange={(val) => setAttributes({ isDismissible: Boolean(val) })}
/>
</PanelBody>
</InspectorControls>
{/* Marcado semántico v2 renderizado en el lienzo 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={__('Encabezado de la regla arquitectónica...', 'agentic-lab')}
allowedFormats={['core/code']}
/>
</header>
<RichText
tagName="div"
className="agentic-alert-v2__body"
value={bodyContent}
onChange={(val) => setAttributes({ bodyContent: val })}
placeholder={__('Describe el contexto técnico y las consecuencias...', 'agentic-lab')}
allowedFormats={['core/bold', 'core/italic', 'core/code', 'core/link']}
/>
</section>
</>
);
},
// 4. Serializador determinista v2 (¡función 100% pura sin efectos secundarios!)
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>
);
},
// Conectar la tubería de migración desde el marcado heredado v1
deprecated: deprecatedMigrations,
});
Antes de modificar el HTML, copia la definición de attributes y la función save() actual en un nuevo objeto dentro de deprecated[].
Implementa el método migrate() para que reciba los atributos antiguos y devuelva el objeto de atributos nuevo con todos los campos por defecto.
Introduce las nuevas etiquetas semánticas, clases y controles en el componente principal, manteniendo intacta la instantánea deprecated.
Pega el comentario HTML de la versión v1 en la vista de código de Gutenberg y verifica que al cambiar a la vista visual el bloque migre a v2 sin emitir errores en la consola.