Como integrar un mini-builder propio con metabox JavaScript en WordPress

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

  1. plugin-mini-builder/
    1. plugin-mini-builder.php (o añadir al functions.php)
    2. js/mini-builder.js
    3. 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

  1. Crear el esquema del bloque y su renderer PHP (sanitización y escape).
  2. Implementar la interfaz JS que maneje la edición de ese esquema.
  3. Guardar JSON validado en post_meta.
  4. 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 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *