Todos los artículos

Desarrollo WordPress

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": 3 declara que el bloque funciona dentro del lienzo del editor en iframe. La versión 3 llegó con WordPress 6.3.
  • supports agrega controles de color y espaciado a la barra lateral sin código propio. Las opciones salen de theme.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": false quita «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.
  • false levanta el bloqueo del padre en un área anidada. Úsalo en una fila de tarjetas donde el equipo deba agregar elementos, junto con allowedBlocks para 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, en editorStyle.
  • Haz que el JSX de Edit genere los mismos elementos y clases que render.php. En el ejemplo, ambos lados producen un section con las clases rw-hero y un contenedor interno rw-hero__content.
  • Limita las opciones en theme.json: pon color.custom y typography.customFontSize en false y 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 ServerSideRender en 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 en deprecated, 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 h1 por 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