Como migrar del editor clásico a bloques con mapeo en PHP en WordPress

Contents

Introducción y objetivo

Este artículo explica con todo lujo de detalles cómo migrar contenido creado con el editor clásico de WordPress a bloques (Gutenberg) realizando el mapeo de etiquetas HTML, imágenes, listas, shortcodes y metadatos a bloques nativos y personalizados usando PHP. Incluye la filosofía del proceso, decisiones prácticas y ejemplos de scripts listos para ejecutar en un entorno WordPress (mu-plugin, plugin temporal o WP-CLI).

Por qué migrar y criterios a considerar

  • Beneficios: contenido más estructurado, mejor experiencia WYSIWYG, reutilización y compatibilidad con bloques dinámicos y patterns.
  • Riesgos: pérdida de formato si el mapeo no es correcto, imágenes externas no importadas, shortcodes con lógica compleja que requieren reescritura a bloques server-side o client-side.
  • Recomendaciones previas: copia de seguridad completa (DB archivos), probar en staging, usar control de versiones y ejecutar la migración en lotes pequeños.

Estrategia de mapeo

Antes de lanzar el script, haz un inventario del contenido que quieres convertir. Una tabla de ejemplo de mapeo podría ser:

Origen (HTML / Shortcode / Meta) Destino (Bloque) Comentarios
lth1gt-lth6gt core/heading mapear el nivel (level) y el contenido
ltpgt core/paragraph mantener HTML interno simple
ltulgt / ltolgt core/list convertir ítems li a lista interna
ltimg src=…gt core/image si es necesario, sideload/importar a librería y añadir ID
core/gallery parsear ids y crear atributos del bloque
[mi_cta texto=…] (shortcode personalizado) mi-plugin/cta (bloque personalizado) crear bloque server-side o estático con atributos del shortcode

Herramientas de PHP en WordPress que usaremos

  • parse_blocks() — para analizar contenido que ya tiene bloques.
  • serialize_block() — para serializar un array de bloque a su HTML con comentarios .
  • has_blocks() — para detectar si un post ya contiene bloques.
  • attachment_url_to_postid() y media_sideload_image() — para mapear/importar imágenes a la librería.
  • shortcode_parse_atts() — para extraer atributos de un shortcode.
  • wp_update_post() y WP-CLI o scripts en background para procesar lotes.

Flujo general recomendado

  1. Analizar el sitio y contar posts sin bloques: usar has_blocks().
  2. Definir mapeos exactos (h1 → core/heading(level:1), p → core/paragraph, etc.).
  3. Crear un script en PHP que recorra posts, parse el HTML (DOMDocument), genere un array de bloques y serialice con serialize_block().
  4. Actualizar los posts de forma controlada (prueba en 10 posts, verificar resultados, luego lanzar por lotes).
  5. Revisar shortcodes complejos y convertirlos a bloques personalizados (registro de bloque y mapeo de atributos).
  6. Test exhaustivo en staging controlar rollback si algo falla.

Ejemplo práctico: script PHP para mapear HTML básico a bloques

El siguiente ejemplo transforma h1-h6, párrafos, listas y ltimggt a bloques nativos. Está pensado para ejecutarse dentro del contexto de WordPress (por ejemplo como mu-plugin o comando WP-CLI). El script omite lógica avanzada (galerías complejas, embed, shortcodes complejos) pero sirve como plantilla base.

lt?php
// Ejemplo de migración simple: transforma HTML clásico a bloques básicos.
// Úsalo en un entorno controlado (staging) y con backups.
// Colócalo en un mu-plugin o ejecútalo vía WP-CLI.

add_action(init, function() {
    // Evita ejecución en frontend por accidente.
    if (!defined(WP_CLI)  !is_admin()) {
        return
    }

    // Función principal para migrar un solo post
    function mi_migracion_post_a_bloques(post_id) {
        post = get_post(post_id)
        if (!post) return false

        // Si ya contiene bloques, salimos
        if (has_blocks(post->post_content)) {
            return false
        }

        content = post->post_content
        if (trim(content) === ) {
            return false
        }

        // Parseamos HTML con DOMDocument
        libxml_use_internal_errors(true)
        dom = new DOMDocument()
        // Convertir a HTML-ENTITIES para preservar acentos
        dom->loadHTML(mb_convert_encoding(content, HTML-ENTITIES, UTF-8))
        libxml_clear_errors()

        body = dom->getElementsByTagName(body)->item(0)
        if (!body) return false

        blocks = array()

        foreach (body->childNodes as node) {
            // Saltar nodos vacíos de texto
            if (node->nodeType === XML_TEXT_NODE) {
                text = trim(node->textContent)
                if (text === ) continue
            }

            nodeName = strtolower(node->nodeName)

            switch (nodeName) {
                case h1:
                case h2:
                case h3:
                case h4:
                case h5:
                case h6:
                    level = intval(substr(nodeName, 1))
                    inner = 
                    // Guardar el HTML interno del heading
                    foreach (node->childNodes as c) {
                        inner .= dom->saveHTML(c)
                    }
                    blocks[] = array(
                        blockName => core/heading,
                        attrs => array(level => level),
                        innerHTML => inner,
                        innerBlocks => array(),
                    )
                    break

                case p:
                    inner = 
                    foreach (node->childNodes as c) {
                        inner .= dom->saveHTML(c)
                    }
                    blocks[] = array(
                        blockName => core/paragraph,
                        attrs => array(),
                        innerHTML => inner,
                        innerBlocks => array(),
                    )
                    break

                case ul:
                case ol:
                    // Guardar la lista tal cual en innerHTML core/list espera HTML interno de 
  • inner = dom->saveHTML(node) blocks[] = array( blockName => core/list, attrs => array(), innerHTML => inner, innerBlocks => array(), ) break case figure: case img: // Si es figura o img, intentamos obtener la url de la imagen img = null if (nodeName === figure) { imgs = node->getElementsByTagName(img) if (imgs->length) img = imgs->item(0) } else { img = node } if (img) { src = img->getAttribute(src) alt = img->getAttribute(alt) caption = // Si hay figcaption if (nodeName === figure) { caps = node->getElementsByTagName(figcaption) if (caps->length) caption = dom->saveHTML(caps->item(0)) } // Intentar obtener ID de adjunto si ya está en la librería attachment_id = 0 if (src) { attachment_id = attachment_url_to_postid(src) } // Si no está, podríamos sideloadear. Atención: side-loading puede tardar y necesita permisos. if (!attachment_id !empty(src) function_exists(media_sideload_image)) { // media_sideload_image devuelve HTML o WP_Error extraemos URL devuelta y buscamos ID res = media_sideload_image(src, post_id, null, src) if (!is_wp_error(res)) { attachment_id = attachment_url_to_postid(res) } } attrs = array(url => src, alt => alt) if (attachment_id) attrs[id] = intval(attachment_id) if (!empty(caption)) attrs = wp_strip_all_tags(caption) blocks[] = array( blockName => core/image, attrs => attrs, innerHTML => , innerBlocks => array(), ) } break default: // Para nodos que no mapeamos, creamos un bloque core/html con el HTML crudo. raw = dom->saveHTML(node) blocks[] = array( blockName => core/html, attrs => array(), innerHTML => raw, innerBlocks => array(), ) break } } // Serializar bloques new_content = foreach (blocks as b) { new_content .= serialize_block(b) . nn } // Actualizar post (si hay cambios) if (trim(new_content) !== ) { wp_update_post(array( ID => post_id, post_content => new_content, )) return true } return false } // Ejemplo: migrar los primeros 20 posts no bloqueados if (defined(WP_CLI) WP_CLI) { // Si se ejecuta por wp-cli, no hacer nada aquí (se podría registrar un comando). return } // Si estás lanzando desde admin, coméntalo o controlalo para evitar ejecuciones no deseadas. // Aquí solo como ejemplo: procesar 10 posts en estado publish sin bloques. posts = get_posts(array( post_type => post, posts_per_page => 10, post_status => publish, suppress_filters => false, )) foreach (posts as p) { mi_migracion_post_a_bloques(p->ID) } }) ?gt
  • Notas sobre el script anterior

    • DOMDocument puede introducir etiquetas lthtmlgt ltbodygt y cambiar entidades por eso se usa mb_convert_encoding y libxml_use_internal_errors.
    • media_sideload_image puede ser lento y consumir ancho de banda si hay muchas imágenes externas. Valora hacer esto en segundo plano o solo para imágenes internas.
    • serialize_block genera el HTML con los comentarios de bloque adecuados para Gutenberg.

    Ejemplo: convertir un shortcode personalizado a un bloque

    Imagina que tienes shortcodes tipo [cta texto=Compra ahora color=red] y quieres cambiarlos a bloques mi-plugin/cta. Este snippet localiza el shortcode y lo reemplaza por el bloque serializado.

    lt?php
    function reemplazar_cta_shortcode_por_bloque(content, post_id) {
        // Regex simple para capturar atributos dentro del shortcode [cta ...]
        return preg_replace_callback(/[cta([^]])]/i, function(m) use (post_id) {
            attr_string = isset(m[1]) ? trim(m[1]) : 
            attrs = shortcode_parse_atts(attr_string)
            if (!is_array(attrs)) attrs = array()
    
            // Normalizar atributos, por ejemplo texto => content
            bloque_attrs = array(
                texto => isset(attrs[texto]) ? attrs[texto] : ,
                color => isset(attrs[color]) ? attrs[color] : default,
            )
    
            block = array(
                blockName => mi-plugin/cta,
                attrs => bloque_attrs,
                innerHTML => ,
                innerBlocks => array(),
            )
    
            return serialize_block(block)
        }, content)
    }
    
    // Uso: al preparar contenido para migración
    post = get_post(123)
    nuevo = reemplazar_cta_shortcode_por_bloque(post->post_content, post->ID)
    if (nuevo !== post->post_content) {
        wp_update_post(array(ID => post->ID, post_content => nuevo))
    }
    ?gt
    

    Migrar metadatos o campos personalizados a bloques

    Si tienes datos en postmeta (por ejemplo, un repeatedly used call-to-action guardado en meta _cta_text), puedes crear un bloque con atributos que incluyan esos valores o insertar un bloque que lea esos metadatos en tiempo de renderizado (server-side render).

    Ejemplo de flujo:

    1. Crear un bloque server-side que use render_callback y durante render recupere get_post_meta(post_id, _cta_text, true).
    2. Si prefieres materializar el contenido en post_content, el script de migración lee get_post_meta y crea serialize_block con esos atributos concretos y lo inserta en content.

    Buenas prácticas y recomendaciones finales

    • Probar primero en staging: siempre verificar visualmente las migraciones y compararlas con el original.
    • Logs y dry-run: añade logs o un modo dry-run que muestre el nuevo HTML sin escribir en la base de datos.
    • Batch processing: no proceses miles de posts en una sola petición usa WP-CLI, WP Cron o scripts por lotes para evitar timeouts.
    • Versionado: registra cambios en control de versiones si cambias bloques o templates.
    • Backups: imprescindible antes de ejecutar cualquier script que modifique post_content.
    • Mapping iterativo: empieza migrando contenidos simples y añade reglas para casos especiales (embeds, tablas, shortcodes complejos).

    Conclusión

    La migración del editor clásico a bloques es un proceso que gana en confiabilidad cuando se diseña un mapeo claro y se implementa con scripts que usen las funciones nativas de WordPress (parse_blocks, serialize_block, attachment helpers). El ejemplo anterior proporciona una base sólida que puedes ampliar: añadir detección de embeds, soporte para galleries, conversión de shortcodes a bloques personalizados y manejo avanzado de imágenes. Ejecuta el proceso en entornos controlados, con backups y por lotes para minimizar riesgos.

    Enlaces útiles



    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 *