Bloques de Gutenberg a medida que el cliente edita sin romper
Por Rolando Escobar, desarrollador WordPress para agencias · · 6 min de lectura
El rediseño de una portada puede desarmarse en pocas semanas. Alguien del equipo edita un titular, arrastra un bloque fuera de su grupo, elige un color que no es de la marca, y una sección construida con cuidado deja de parecerse al diseño. El error contrario, fijar la portada en una plantilla PHP, convierte cada cambio de texto en un ticket para el desarrollador.
Los bloques personalizados quedan en medio. Rediseñé las portadas del Louis Armstrong House Museum y de Counterintuity con bloques de Gutenberg a medida. Lo que sigue es el enfoque en formato de tutorial: bloques que permiten cambiar textos, imágenes y enlaces mientras la estructura se queda en su lugar.
Un bloque por sección, dentro de un plugin
Piensa la portada como una pila de secciones: un hero, contenido destacado, próximos eventos, una llamada a la acción. Cada sección es un bloque. Evita un único bloque «portada» con cuarenta controles en la barra lateral: es difícil de editar y todavía más difícil de cambiar después.
Pon los bloques en un plugin y no en el tema. El marcado de los bloques vive en el contenido de la entrada, así que si un tema futuro deja fuera tus bloques, el equipo verá un aviso de que el sitio ya no los admite justo donde antes estaba la portada. Un plugin pequeño mantiene el contenido funcionando en el próximo rediseño.
Describe el bloque en block.json
block.json es la fuente única de verdad para el nombre, los atributos, los supports y los assets de un bloque. WordPress lo lee en el servidor y en el editor, así que ambos lados coinciden.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "rw/home-hero",
"title": "Home hero",
"category": "design",
"description": "Homepage hero with a fixed structure and editable content.",
"textdomain": "rw-blocks",
"attributes": {
"layout": {
"type": "string",
"enum": [ "text-left", "text-center" ],
"default": "text-left"
}
},
"supports": {
"html": false,
"align": [ "wide", "full" ],
"color": { "background": true, "text": true, "link": false },
"spacing": { "padding": [ "vertical" ], "margin": [ "top", "bottom" ] }
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:./render.php"
}
Unas pocas líneas de ese archivo hacen la mayor parte del trabajo:
"apiVersion": 3declara que el bloque funciona dentro del lienzo del editor en iframe. La versión 3 llegó con WordPress 6.3.supportsagrega controles de color y espaciado a la barra lateral sin código propio. Las opciones salen detheme.json, así que el equipo elige de la paleta y la escala de espaciado de la marca en lugar de escribir códigos hexadecimales y píxeles."html": falsequita «Editar como HTML» del menú del bloque, una fuente habitual de marcado roto."style"carga la misma hoja de estilos en el editor y en el front end."render"apunta a una plantilla PHP, lo que convierte al bloque en dinámico.
La referencia de metadatos de bloques lista todos los campos, y la referencia de block supports lista todos los controles que obtienes sin programarlos.
Registra todos los bloques desde el plugin
<?php
/**
* Plugin Name: RW Homepage Blocks
* Description: Custom blocks for the homepage sections.
* Requires at least: 6.5
* Requires PHP: 8.1
* Text Domain: rw-blocks
*/
defined( 'ABSPATH' ) || exit;
add_action( 'init', 'rw_blocks_register' );
function rw_blocks_register(): void {
$build = __DIR__ . '/build';
// WordPress 6.8+: una sola llamada registra todos los bloques del manifiesto generado.
if ( function_exists( 'wp_register_block_types_from_metadata_collection' ) ) {
wp_register_block_types_from_metadata_collection( $build, $build . '/blocks-manifest.php' );
return;
}
// Versiones anteriores: registra cada carpeta que contenga un archivo block.json.
foreach ( glob( $build . '/*/block.json' ) ?: [] as $metadata ) {
register_block_type( dirname( $metadata ) );
}
}
WordPress 6.8 agregó wp_register_block_types_from_metadata_collection(), que registra todos los bloques listados en un manifiesto PHP generado por wp-scripts build --blocks-manifest. Así WordPress no tiene que leer y analizar cada archivo block.json en cada petición. El bucle mantiene funcionando los sitios con las versiones 6.5 a 6.7.
Renderizado estático o dinámico
Un bloque estático guarda su HTML final en el contenido de la entrada desde una función save() de JavaScript. Un bloque dinámico guarda solo sus atributos y su contenido interno, y PHP arma el HTML en cada petición.
| Estático (función save) | Dinámico (render.php) | |
|---|---|---|
| Dónde vive el marcado | En cada entrada que usa el bloque | En tu plugin |
| Cambiar el marcado después | Los bloques existentes muestran un error de validación hasta que agregas una deprecación | Todas las instancias se actualizan en la siguiente carga |
| Datos en vivo (últimos eventos, una consulta) | Congelados al guardar | Se leen en cada render |
| Vista previa en el editor | El mismo JavaScript en ambos lados | Replicas el marcado en JSX |
Para las secciones de una portada funciona bien un híbrido: el contenedor es dinámico y el texto de adentro está hecho con bloques del núcleo guardados como contenido interno. El equipo usa los bloques de párrafo, encabezado y botón que ya conoce, y tú puedes cambiar el marcado y las clases del contenedor más adelante sin invalidar ninguna entrada guardada.
Bloquea la estructura, libera el contenido
InnerBlocks recibe un template, los bloques con los que empieza, y un templateLock, que controla lo que el equipo puede hacer con ellos.
// edit.js
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
import { PanelBody, SelectControl } from '@wordpress/components';
const TEMPLATE = [
[ 'core/heading', { level: 1, placeholder: __( 'Main headline', 'rw-blocks' ) } ],
[ 'core/paragraph', { placeholder: __( 'One or two sentences that support the headline.', 'rw-blocks' ) } ],
[ 'core/buttons', {}, [ [ 'core/button', { placeholder: __( 'Button text', 'rw-blocks' ) } ] ] ],
];
export default function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps( { className: `rw-hero rw-hero--${ attributes.layout }` } );
const innerBlocksProps = useInnerBlocksProps(
{ className: 'rw-hero__content' },
{ template: TEMPLATE, templateLock: 'all' } // Contenido editable, estructura fija.
);
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Layout', 'rw-blocks' ) }>
<SelectControl
label={ __( 'Text position', 'rw-blocks' ) }
value={ attributes.layout }
options={ [
{ label: __( 'Left', 'rw-blocks' ), value: 'text-left' },
{ label: __( 'Centered', 'rw-blocks' ), value: 'text-center' },
] }
onChange={ ( layout ) => setAttributes( { layout } ) }
__next40pxDefaultSize
__nextHasNoMarginBottom
/>
</PanelBody>
</InspectorControls>
<section { ...blockProps }>
<div { ...innerBlocksProps } />
</section>
</>
);
}
// index.js
import { registerBlockType } from '@wordpress/blocks';
import { InnerBlocks } from '@wordpress/block-editor';
import metadata from './block.json';
import Edit from './edit';
import './style.scss';
import './editor.scss';
registerBlockType( metadata.name, {
edit: Edit,
save: () => <InnerBlocks.Content />, // Guarda los bloques internos; render.php arma el contenedor.
} );
Según la documentación de InnerBlocks, los valores de bloqueo se comportan así:
'all'impide insertar, mover y eliminar bloques. Textos, enlaces e imágenes siguen siendo editables, así que es el valor adecuado por defecto para un hero.'insert'impide insertar y eliminar, pero permite reordenar.'contentOnly'es el más estricto. Solo se puede cambiar el contenido, los bloques sin contenido desaparecen de la vista de lista y las áreas anidadas no pueden anularlo. La experiencia de edición en este modo ha cambiado entre versiones, así que pruébalo en tu versión de WordPress.falselevanta el bloqueo del padre en un área anidada. Úsalo en una fila de tarjetas donde el equipo deba agregar elementos, junto conallowedBlockspara limitar qué puede agregar.
El único control propio es la posición del texto, una opción que el diseño realmente necesita. Color, padding y margen vienen de supports, así que no hay controles propios que escribir ni mantener para ellos. La plantilla además empieza el hero con un h1, de modo que la portada tiene su encabezado principal en el lugar correcto desde la primera inserción.
Renderiza en el servidor
Como el texto vive en bloques internos, la función save devuelve solo <InnerBlocks.Content />, que guarda esos bloques en la entrada. La plantilla PHP los envuelve:
<?php
/**
* Marcado del front end para rw/home-hero.
*
* @var array $attributes Atributos del bloque.
* @var string $content Bloques internos, ya renderizados por el núcleo.
* @var WP_Block $block Instancia del bloque.
*/
defined( 'ABSPATH' ) || exit;
// Los atributos viven en el contenido de la entrada: valídalos antes de usarlos.
$layout = in_array( $attributes['layout'] ?? '', [ 'text-left', 'text-center' ], true )
? $attributes['layout']
: 'text-left';
$wrapper = get_block_wrapper_attributes( [ 'class' => 'rw-hero rw-hero--' . $layout ] );
?>
<section <?php echo $wrapper; // Escapado por el núcleo. ?>>
<div class="rw-hero__content">
<?php echo $content; // Bloques internos renderizados. ?>
</div>
</section>
get_block_wrapper_attributes() imprime las clases y los estilos en línea que generan los block supports y suma tus propias clases. Si lo omites, los controles de color y espaciado parecen no hacer nada en el front end. $content contiene los bloques internos ya renderizados por el núcleo. El valor de layout se compara con una lista de valores permitidos antes de convertirse en nombre de clase, porque los atributos se guardan en el contenido de la entrada y cualquiera que pueda editarla puede cambiarlos.
Da un punto de partida con patrones
Un patrón de bloques es una combinación guardada de bloques con contenido de ejemplo. Los temas registran patrones automáticamente desde archivos PHP en una carpeta /patterns, y los plugins usan register_block_pattern(). Un patrón por sección, o uno para toda la portada, permite rehacer una página o empezar una landing de campaña con un clic.
<?php
/**
* Title: Homepage hero
* Slug: rw-theme/home-hero
* Categories: featured
* Block Types: core/post-content
* Post Types: page
* Description: Hero with a headline, a short intro and one button.
*/
?>
<!-- wp:rw/home-hero {"layout":"text-center","align":"full"} -->
<!-- wp:heading {"level":1} -->
<h1 class="wp-block-heading"><?php esc_html_e( 'Your main headline', 'rw-theme' ); ?></h1>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p><?php esc_html_e( 'One or two sentences that support the headline.', 'rw-theme' ); ?></p>
<!-- /wp:paragraph -->
<!-- wp:buttons -->
<div class="wp-block-buttons"><!-- wp:button -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="#"><?php esc_html_e( 'Primary action', 'rw-theme' ); ?></a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons -->
<!-- /wp:rw/home-hero -->
El encabezado Block Types: core/post-content ofrece el patrón en el diálogo que aparece al crear una página nueva. Los patrones insertan una copia, así que los cambios posteriores en el archivo del patrón no alteran las páginas ya creadas con él. Usa patrones sincronizados solo para contenido que deba ser idéntico en todas partes, como una llamada a la acción compartida.
Mantén sincronizados el editor y el front end
- Carga una sola hoja de estilos en ambos lados con el campo
style. Deja las reglas exclusivas del editor, como las pistas de los placeholders, eneditorStyle. - Haz que el JSX de
Editgenere los mismos elementos y clases querender.php. En el ejemplo, ambos lados producen unsectioncon las clasesrw-heroy un contenedor internorw-hero__content. - Limita las opciones en
theme.json: poncolor.customytypography.customFontSizeenfalsey define presets de espaciado, para que cada opción de la barra lateral sea una que el diseño permite. - Prueba con contenido real: un titular que ocupa tres líneas, una imagen que falta, la etiqueta de un botón traducida que mide el doble que en inglés.
- Evita
ServerSideRenderen secciones con bloques internos. No puede mostrar bloques internos editables y hace una petición con cada cambio.
Qué vigilar
- Nunca cambies el nombre de un bloque después del lanzamiento. Ese nombre queda guardado en cada entrada que lo usa.
- Si cambias la salida de
save()de un bloque estático, agrega una entrada endeprecated, o el equipo verá errores de validación en el contenido existente. - No guardes HTML en atributos de texto. El texto con formato va en bloques internos o en texto enriquecido tomado del marcado guardado.
- Revisa la accesibilidad dentro de cada plantilla: un solo
h1por página, texto alternativo en las imágenes y etiquetas de botón que se entiendan fuera de contexto. - Entrega al equipo una guía de una página con una captura de cada bloque. El bloqueo evita accidentes; la guía evita confusiones.
Este es el tipo de trabajo que hago para clientes: De Figma a WordPress