Contents
Introducción
Este artículo explica con todo lujo de detalles cómo integrar un mini-builder propio en WordPress usando una metabox (meta box) en el editor de entradas/páginas y una interfaz JavaScript para crear, reordenar y editar bloques ligeros. La solución que se muestra es autónoma y no depende de editores complejos: se basa en una metabox personalizada que guarda un JSON con la estructura del layout y un front-end que lo interpreta. Incluye ejemplos de código listos para copiar en un plugin o en el functions.php de un tema.
Resumen de la arquitectura
- Back-end (PHP): añade la metabox, guarda/recupera el post_meta, valida nonce y permisos, y proporciona la función de render para el front-end (o un shortcode).
- Front-end del editor (JavaScript CSS): interfaz interactiva para añadir, editar, mover y eliminar bloques. Serializa el estado en JSON en un campo oculto.
- Seguridad y sanitización: nonce, capability checks, saneamiento de los datos al guardarlos y escape cuando se renderiza.
Requisitos
- WordPress 5.x o superior.
- Permisos para editar entradas/páginas (capability edit_post).
- Conocimientos básicos de PHP, JS y cómo añadir scripts/styles a WordPress.
Estructura de archivos sugerida
- plugin-mini-builder/
- plugin-mini-builder.php (o añadir al functions.php)
- js/mini-builder.js
- css/mini-builder.css
1) Registrar la metabox (PHP)
En este ejemplo se crea una metabox llamada Mini Builder que contiene la interfaz del builder y un campo oculto que guarda JSON.
lt?php // Añadir las acciones en plugin o functions.php add_action(add_meta_boxes, mb_mini_builder_add_meta_box) add_action(save_post, mb_mini_builder_save_post) function mb_mini_builder_add_meta_box() { add_meta_box( mb-mini-builder, Mini Builder, mb_mini_builder_render_meta_box, array(post, page), // tipos de post donde aparece normal, high ) } function mb_mini_builder_render_meta_box(post) { // Campo nonce para seguridad wp_nonce_field(mb_mini_builder_nonce_action, mb_mini_builder_nonce) // Recuperar datos guardados data = get_post_meta(post->ID, _mb_mini_builder_data, true) if (empty(data)) { data = [] // JSON vacío por defecto } // Contenedor de la UI: la estructura visible la construye JS. echo ltdiv id=mb-mini-builder-appgtlt/divgt // Campo oculto donde guardamos el JSON antes de submit echo ltinput type=hidden id=mb_mini_builder_data name=mb_mini_builder_data value= . esc_attr(data) . /gt } ?gt
2) Encolar scripts y estilos (PHP)
Registrar y encolar el JavaScript y CSS del builder. También se pasa información útil al script (nonce, post ID).
lt?php add_action(admin_enqueue_scripts, mb_mini_builder_enqueue_assets) function mb_mini_builder_enqueue_assets(hook) { // Solo en los edit screens para post / page if (hook !== post.php hook !== post-new.php) { return } // Ruta al plugin o tema - ajustar según ubicación real dir = plugin_dir_url(__FILE__) wp_enqueue_style(mb-mini-builder-css, dir . css/mini-builder.css, array(), 1.0) wp_enqueue_script(mb-mini-builder-js, dir . js/mini-builder.js, array(jquery), 1.0, true) // Pasamos datos de configuración al script wp_localize_script(mb-mini-builder-js, MBMiniBuilderConfig, array( nonce => wp_create_nonce(mb_mini_builder_nonce_action), post_id => get_the_ID(), ajax_url => admin_url(admin-ajax.php) // opcional )) } ?gt
3) Guardar los datos al guardar el post (PHP)
Validamos nonce, permisos y saneamos antes de guardar el JSON directamente en post_meta.
lt?php function mb_mini_builder_save_post(post_id) { // Revisar autosave if (defined(DOING_AUTOSAVE) DOING_AUTOSAVE) { return } // Verificar nonce if (!isset(_POST[mb_mini_builder_nonce]) !wp_verify_nonce(_POST[mb_mini_builder_nonce], mb_mini_builder_nonce_action)) { return } // Permisos if (!current_user_can(edit_post, post_id)) { return } // Guardar JSON (campo oculto) if (isset(_POST[mb_mini_builder_data])) { raw = wp_unslash(_POST[mb_mini_builder_data]) // quitar slashes añadidos por WP // Opcional: validar que sea JSON válido decoded = json_decode(raw, true) if (json_last_error() === JSON_ERROR_NONE is_array(decoded)) { // Aquí podríamos sanitizar cada bloque según su tipo. Para simplicidad guardamos el JSON. update_post_meta(post_id, _mb_mini_builder_data, wp_json_encode(decoded)) } else { // Si no es JSON válido, eliminar meta o ignorar delete_post_meta(post_id, _mb_mini_builder_data) } } } ?gt
4) Código JavaScript (mini-builder.js)
El JS crea la UI dentro de #mb-mini-builder-app: paleta de bloques, área de diseño y controles. Usa drag drop nativo para reordenar y guarda siempre la representación en el campo oculto.
// mini-builder.js (function(){ const postInput = document.getElementById(mb_mini_builder_data) const container = document.getElementById(mb-mini-builder-app) if (!container) return // Definición de bloques disponibles const BLOCK_TYPES = [ { type: text, label: Texto }, { type: image, label: Imagen }, { type: cta, label: Llamada a acción } ] // Estado: lista de bloques let blocks = [] try { blocks = JSON.parse(postInput.value []) } catch (e) { blocks = [] } // Construir UI function buildUI(){ container.innerHTML = // limpio // Paleta const palette = document.createElement(div) palette.className = mb-palette BLOCK_TYPES.forEach(b => { const btn = document.createElement(button) btn.type = button btn.className = mb-add-block btn.textContent = b.label btn.dataset.type = b.type btn.addEventListener(click, () => addBlock(b.type)) palette.appendChild(btn) }) container.appendChild(palette) // Área de montaje const stage = document.createElement(div) stage.className = mb-stage stage.id = mb-stage container.appendChild(stage) renderBlocks() } // Renderizar bloques en el stage function renderBlocks() { const stage = document.getElementById(mb-stage) stage.innerHTML = blocks.forEach((block, index) => { const item = document.createElement(div) item.className = mb-block item.draggable = true item.dataset.index = index const header = document.createElement(div) header.className = mb-block-header header.textContent = block.type.toUpperCase() item.appendChild(header) const content = document.createElement(div) content.className = mb-block-content // Vista previa simple según tipo if (block.type === text) { content.innerHTML =(block.text Texto vacío)
} else if (block.type === image) { content.innerHTML = block.url ? (ltimg src= block.url alt= style=max-width:100%/gt) : ltdiv class=mb-placeholdergtSin imagenlt/divgt } else if (block.type === cta) { content.innerHTML = ltbutton class=mb-ctagt (block.label Botón) lt/buttongt } item.appendChild(content) const controls = document.createElement(div) controls.className = mb-block-controls const editBtn = document.createElement(button) editBtn.type = button editBtn.textContent = Editar editBtn.addEventListener(click, () => editBlock(index)) const delBtn = document.createElement(button) delBtn.type = button delBtn.textContent = Eliminar delBtn.addEventListener(click, () => removeBlock(index)) controls.appendChild(editBtn) controls.appendChild(delBtn) item.appendChild(controls) // Drag events item.addEventListener(dragstart, dragStart) item.addEventListener(dragover, dragOver) item.addEventListener(drop, drop) stage.appendChild(item) }) // Actualizar campo oculto postInput.value = JSON.stringify(blocks) } // Acciones function addBlock(type){ let newBlock = { type: type } // Valores por defecto según tipo if (type === text) newBlock.text = Texto de ejemplo if (type === image) newBlock.url = if (type === cta) newBlock.label = Clica aquí blocks.push(newBlock) renderBlocks() } function editBlock(index){ const block = blocks[index] if (!block) return // Para ejemplo rápido, usamos prompt en producción usar modal propio if (block.type === text) { const v = prompt(Editar texto:, block.text ) if (v !== null) { block.text = v } } else if (block.type === image) { const v = prompt(URL de la imagen:, block.url ) if (v !== null) { block.url = v } } else if (block.type === cta) { const v = prompt(Etiqueta del botón:, block.label ) if (v !== null) { block.label = v } } renderBlocks() } function removeBlock(index) { if (!confirm(Eliminar este bloque?)) return blocks.splice(index, 1) renderBlocks() } // Drag drop básico let dragSrcIndex = null function dragStart(e) { dragSrcIndex = Number(this.dataset.index) e.dataTransfer.effectAllowed = move } function dragOver(e) { e.preventDefault() e.dataTransfer.dropEffect = move } function drop(e) { e.stopPropagation() const destIndex = Number(this.dataset.index) if (dragSrcIndex !== null destIndex !== dragSrcIndex) { const [moved] = blocks.splice(dragSrcIndex, 1) blocks.splice(destIndex, 0, moved) renderBlocks() } dragSrcIndex = null } // Inicializar UI buildUI() // Asegurar que antes de submit se actualice el campo (por si hay cambios no guardados) const form = document.getElementById(post) if (form) { form.addEventListener(submit, () => { postInput.value = JSON.stringify(blocks) }) } })()
5) Ejemplo de CSS básico (mini-builder.css)
/ mini-builder.css / #mb-mini-builder-app { border:1px solid #ddd padding:10px background:#fff } .mb-palette { margin-bottom:10px } .mb-palette .mb-add-block { margin-right:6px } .mb-stage { min-height:120px border:1px dashed #ccc padding:10px background:#fafafa } .mb-block { border:1px solid #e1e1e1 padding:8px margin-bottom:8px background:#fff cursor:grab } .mb-block-header { font-weight:bold margin-bottom:6px } .mb-block-controls button { margin-right:6px } .mb-cta { padding:8px 12px background:#0073aa color:#fff border: none cursor: pointer }
6) Renderizar el contenido en el front-end (PHP)
Para mostrar el resultado en la plantilla del tema, creamos una función que recupere el meta, decodifique el JSON y genere HTML seguro. Aquí un ejemplo sencillo que convierte cada bloque en HTML.
lt?php function mb_mini_builder_render_post(post_id = null) { if (!post_id) { post_id = get_the_ID() } raw = get_post_meta(post_id, _mb_mini_builder_data, true) if (empty(raw)) return blocks = json_decode(raw, true) if (!is_array(blocks)) return output = foreach (blocks as block) { switch (block[type]) { case text: // Escapar HTML básico permitir algunas etiquetas si es necesario text = isset(block[text]) ? wp_kses_post(block[text]) : output .= ltdiv class=mb-block-front mb-block-textgt . text . lt/divgt break case image: url = isset(block[url]) ? esc_url(block[url]) : if (url) { output .= ltdiv class=mb-block-front mb-block-imagegtltimg src= . url . alt= /gtlt/divgt } break case cta: label = isset(block[label]) ? esc_html(block[label]) : Botón output .= ltdiv class=mb-block-front mb-block-ctagtltbutton class=mb-cta-frontgt . label . lt/buttongtlt/divgt break default: // Bloque desconocido: ignorar o loggear break } } echo output } // Ejemplo: usar en single.php o page template // lt?php mb_mini_builder_render_post() ?gt // También se puede registrar un shortcode: add_shortcode(mini_builder, function(atts){ ob_start() mb_mini_builder_render_post(isset(atts[id]) ? intval(atts[id]) : null) return ob_get_clean() }) ?gt
7) Buenas prácticas y ampliaciones
- Saneamiento por tipo de bloque: en el ejemplo se guardó el JSON tal cual, pero lo ideal es validar y sanear cada propiedad antes de guardar (por ejemplo, esc_url para URLs, sanitize_text_field para textos breves, wp_kses_post para contenido HTML permitido).
- Seguridad: revisar capabilities (edit_post), verificar nonce en save_post, escapar la salida en front-end (esc_html, esc_attr, esc_url o wp_kses_post según convenga).
- UX mejorada: sustituir los prompt por un modal con formulario (usando ThickBox, custom modal o librerías como tingle.js).
- Orden y rendimiento: si el JSON puede crecer, piensa en límites o en almacenar bloques en postmeta individuales si necesitas consultas complejas.
- Drag drop avanzado: usar librerías como SortableJS para funcionalidades robustas de reordenado y mejor soporte táctil.
- Integración con REST API: para editores externos o sincronización en tiempo real, exponer endpoints seguros que validen nonce y capabilities.
- Versionado/rollback: se puede guardar un historial de versiones del JSON para deshacer cambios complejos.
8) Consideraciones finales y consejos prácticos
Este mini-builder es una base que permite añadir bloques simples, reordenarlos y guardarlos como JSON. La ventaja es su simplicidad y control total: puedes diseñar bloques con cualquier estructura, añadir controles avanzados para estilos, añadir condicionales (mostrar/ocultar en cierta resolución), o incluso exportar/importar layouts.
Al escalar, conviene diseñar una API interna de registro de bloques (registry) donde cada bloque declara:
- Tipo y label
- Campos esperados y validadores
- Render en front-end
De ese modo se separa la lógica de la UI del editor de la lógica de renderizado y validación, facilitando mantenimiento y extensibilidad.
Ejemplo rápido de flujo de trabajo recomendado
- Crear el esquema del bloque y su renderer PHP (sanitización y escape).
- Implementar la interfaz JS que maneje la edición de ese esquema.
- Guardar JSON validado en post_meta.
- Renderizar con un renderer PHP que utilice las funciones de escape adecuadas.
Notas finales
El código de este artículo está pensado para ser claro y educativo. En producción, adaptar la UI (modal, validaciones en el cliente), usar librerías maduras para drag drop si es necesario y reforzar la sanitización y pruebas con distintos inputs. Con estos bloques puedes construir un mini-builder ligero, eficiente y 100% personalizado para las necesidades de tu sitio WordPress.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |