ROSVIT
Proyecto · Magento 2

CMS Content Sync: módulo gratuito para exportar e importar páginas y bloques CMS en Magento 2

Módulo gratuito de Magento 2 para exportar páginas y bloques CMS a JSON e importarlos en otro entorno, con vista previa y sin copiar la base de datos.

Versión
1.0.0
Licencia
MIT
Compatibilidad
Magento 2.4.x / Adobe Commerce
Publicado
5 oct 2026
Stack
Magento 2PHP 8ComposerPage Builder
CMS Content Sync: módulo gratuito para exportar e importar páginas y bloques CMS en Magento 2

Rosvit_CmsContentSync es un módulo gratuito para Magento 2 que exporta páginas y bloques CMS a un archivo JSON y los importa en otro entorno. Antes de escribir nada muestra una vista previa con lo que va a crear o actualizar. Lo construí porque en casi todos los proyectos Magento el contenido se arma en staging y después hay que llevarlo a producción, y Magento no trae una forma de hacerlo.

¿Magento 2 permite exportar páginas y bloques CMS?

No de forma nativa. System → Data Transfer exporta productos, precios y clientes, pero no contenido CMS. Para mover páginas y bloques entre entornos suelen quedar estas opciones:

OpciónQué resuelveDónde falla
Copiar y pegar desde el adminNo requiere desarrolloEs lento y es fácil equivocarse con un campo o un store view
Copiar la base de datos o tablas sueltasMueve todo de una vezPisa contenido que producción ya tiene y arrastra IDs que no coinciden
Data patches con el contenido en códigoEs repetible y queda versionadoCada cambio de texto exige un desarrollador y un despliegue
Extensiones de import/export CMSResuelven el caso desde el adminLa mayoría son de pago y trabajan con CSV o XML

Lo que yo quería era más simple: seleccionar contenido en un entorno, descargar un archivo, subirlo en el otro y ver qué va a pasar antes de confirmar.

¿Qué hace Rosvit_CmsContentSync?

Agrega una acción Export to JSON a los grids de páginas y bloques CMS, y una pantalla CMS Content Sync para importar. Todo el intercambio pasa por un archivo JSON legible, que se puede revisar, comparar con un diff o guardar en git.

  • Exportación masiva de páginas y bloques CMS a un solo archivo.
  • Vista previa antes de importar: cada fila dice si se va a crear, actualizar, si no tiene cambios o si tiene un error.
  • Store views por código: el archivo guarda default o en, no el ID del store, porque los IDs cambian entre entornos.
  • Bloques dentro de páginas: las referencias a bloques CMS, incluidas las de Page Builder, viajan por identifier y se traducen al ID del destino.
  • Permisos separados para exportar e importar, configurables por rol.

Funciona con Magento Open Source y Adobe Commerce 2.4.x sobre PHP 8.2 a 8.5. Es gratuito y de código abierto, con licencia MIT.

¿Cómo instalar el módulo?

Se instala con Composer desde Packagist, en los dos entornos: el que exporta y el que importa.

composer require rosvit/module-cms-content-sync
bin/magento module:enable Rosvit_CmsContentSync
bin/magento setup:upgrade
bin/magento cache:flush

El acceso se controla con dos recursos ACL, Export CMS Content e Import CMS Content, en System → Permissions → User Roles.

¿Cómo exportar páginas y bloques CMS a JSON?

  1. En el entorno de origen ve a Content → Pages o Content → Blocks.
  2. Selecciona las filas que quieres llevar.
  3. Elige Actions → Export to JSON. Se descarga un archivo como cms-content-export-20261005-153000.json.
Exportar páginas CMS en Magento 2 con la acción Export to JSON del grid de Content → Pages
Exportar páginas CMS: Export to JSON aparece junto a las acciones nativas del grid.
Exportar bloques CMS en Magento 2 con la acción Export to JSON del grid de Content → Blocks
Exportar bloques CMS: la misma acción en Content → Blocks.

¿Cómo importar páginas y bloques CMS en otro entorno?

  1. En el entorno destino ve a Content → CMS Content Sync.
  2. Elige el archivo .json (hasta 8 MB) y haz clic en Upload and Preview.
  3. Revisa la vista previa y deja marcadas las filas que quieres importar.
  4. Haz clic en Import Selected. Al terminar ves cuántas filas se crearon, actualizaron, omitieron o fallaron.
Menú Content de Magento 2 con la opción CMS Content Sync para importar contenido CMS
La importación vive en Content → CMS Content Sync.

La columna Status de la vista previa dice qué va a pasar con cada fila:

EstadoQué significaMarcada por defecto
CreateNo existe en el destino y se va a crearSí
UpdateExiste y el archivo trae cambiosSí
No changesExiste y es idéntica a lo que trae el archivoNo
ErrorNo se puede importar, por ejemplo por un store view que no existeNo se puede marcar
Vista previa de importación de bloques CMS en Magento 2 con estados Create y Update y aviso de dependencia entre bloques
Vista previa con un bloque nuevo y otro a actualizar. Cartagena usa el bloque block_call, que viene en el mismo archivo, y la vista previa pide importar las dos filas.

Si una fila falla durante la importación, las demás se importan igual. Importar 40 páginas no debería fallar entero porque una use un store view que no existe en el destino.

¿Qué contiene el archivo JSON?

Un sobre con datos de origen y la lista de entidades. Este es un ejemplo simplificado con un bloque:

{
    "format_version": 1,
    "exported_at": "2026-10-07T04:47:03+00:00",
    "exported_from": "magento-pruebas.ddev.site",
    "exported_by": "admin",
    "entities": [
        {
            "entity_type": "cms_block",
            "identifier": "Cartagena",
            "title": "Cartagena",
            "content": "{{widget type=\"Magento\\Cms\\Block\\Widget\\Block\" template=\"widget/static_block/default.phtml\" block_id=\"block_call\"}}",
            "is_active": true,
            "store_codes": ["*"]
        }
    ]
}

No hay ningún ID. block_id apunta al identifier del bloque, store_codes usa códigos de store y * significa All Store Views. Las páginas llevan además page_layout, los campos meta, content_heading, sort_order y los campos de diseño.

Decisiones técnicas

¿Por qué un archivo JSON?

Porque quería que el archivo se pudiera leer. Sale con formato legible a propósito, para abrirlo, compararlo con un diff y guardarlo en el repositorio junto al resto del proyecto.

Solo format_version tiene efecto en la importación: si un día cambia el formato, el módulo rechaza el archivo de entrada en vez de fallar a mitad de camino. exported_at, exported_from y exported_by se muestran en la vista previa para que quien importa sepa de dónde viene el archivo.

¿Por qué los IDs no viajan entre entornos?

Porque no coinciden. La parte difícil no fue exportar, sino que el contenido funcionara en un entorno donde los IDs son distintos. El archivo guarda códigos e identifiers, y el destino los traduce a sus propios IDs.

El store view con ID 1 en staging puede ser el 3 en producción, así que el archivo guarda el código del store. El alcance All Store Views viaja como *, porque su código real es admin, y en un archivo de contenido eso parece un error.

¿Cómo se exporta un bloque CMS usado dentro de una página?

Reescribiendo la referencia: de ID a identifier al exportar, y de identifier al ID local al importar. Este fue el caso que más me costó. Una página que usa un bloque guarda en su contenido algo así:

{{widget type="Magento\Cms\Block\Widget\Block"
         template="widget/static_block/default.phtml"
         block_id="1"}}

Si la página se exporta tal cual, en el destino sigue apuntando al bloque 1. Pero ese bloque puede quedar con el ID 3 al importarlo, y la página termina mostrando otro bloque o nada. Por eso:

  • Al exportar, cada block_id="1" se cambia por el identifier del bloque, por ejemplo block_id="footer_links".
  • Al importar, el módulo busca footer_links en el destino, dentro de los store views de la página, y lo cambia por su ID local.
  • Si el bloque no existe, deja el identifier y avisa, en la vista previa y al terminar la importación.

El último punto tiene un detalle útil: Magento también carga un bloque por su identifier. Una página importada antes que su bloque empieza a funcionar sola en cuanto ese bloque se crea, sin volver a importarla.

El módulo traduce las tres formas de referenciar un bloque CMS: el widget Magento\Cms\Block\Widget\Block (el que usa Page Builder), {{block class="Magento\Cms\Block\Block" block_id="..."}} y {{block id="..."}}. También aplica a bloques que usan otros bloques.

¿En qué orden se importan páginas y bloques?

Primero los bloques, después las páginas. Si un mismo archivo trae una página y el bloque que usa, así la página encuentra el bloque recién creado. La vista previa además avisa cuando un contenido depende de un bloque del mismo archivo, para no olvidar marcarlo.

¿Cómo decide si crear o actualizar?

Busca una entidad con el mismo identifier cuyo alcance de store views se cruce con el del archivo. El identifier solo no basta, porque puede repetirse en distintos store views.

La vista previa y la importación usan exactamente la misma búsqueda. Si no fuera así, la vista previa podría prometer Create mientras la importación sobrescribe otra cosa.

¿Cuándo se escribe en la base de datos?

Solo en Import Selected. Al subir el archivo, el módulo lo valida (extensión, tamaño, format_version, tipo e identifier de cada entidad) y lo guarda en var/. En la sesión del admin viaja solo un token que apunta a ese archivo, para que una exportación pesada no infle la sesión.

Validación

Lo probé en un entorno local de Magento 2.4.9 con DDEV, con un caso que tiene dependencias entre contenidos: una página blog que usa el bloque Cartagena, que a su vez usa el bloque block_call.

Importar una página CMS en Magento 2 cuando el bloque que usa no existe en el entorno destino
La página blog llega a un entorno donde Cartagena todavía no existe. La vista previa lo avisa y la referencia se conserva por identifier.
Resultado de importar bloques CMS en Magento 2: 1 creado, 1 actualizado, 0 omitidos, 0 con errores
Resumen de la importación. Los dos avisos amarillos de arriba vienen del validador de HTML de Magento, no del módulo.

En esa última captura también aparecen avisos del propio Magento: valida el HTML del contenido al guardarlo y muestra "Temporarily allowed to save HTML value that contains restricted elements". No bloquean la importación, pero no dicen a qué página o bloque se refieren.

Limitaciones conocidas

El módulo resuelve el contenido CMS, no todo lo que ese contenido toca:

  • Las imágenes no viajan. El contenido guarda las rutas de pub/media, pero los archivos hay que copiarlos aparte.
  • Solo se traducen los IDs de bloques. Un widget de productos o categorías sigue apuntando a los IDs del origen.
  • El layout XML no se reescribe. layout_update_xml y custom_layout_update_xml viajan tal como están en el origen.
  • Los store views deben existir con el mismo código en el destino. Si falta uno, esa fila sale con error en la vista previa.
  • La importación reemplaza todos los campos exportados, incluidos los vacíos. El archivo es la fuente de verdad: si en el origen se borró una meta description, en el destino también se borra.
  • Un identifier formado solo por números (por ejemplo 123) se exporta como ID, porque al importar no se podría distinguir de uno.

Lecciones aprendidas

  • El formato de intercambio es la decisión de arquitectura. Exportar es un json_encode. Lo que hace que el archivo sirva en otro entorno es decidir desde el principio que ningún ID entra en él.
  • La vista previa solo sirve si coincide con la importación. Por eso las dos comparten la misma búsqueda de entidades, y la comparación pasa el contenido actual por el mismo exportador que generó el archivo.
  • null y vacío no son lo mismo. Si el archivo es la fuente de verdad, tiene que poder expresar "borra esta meta description". Por eso los null se conservan de punta a punta en lugar de convertirse en cadenas vacías.
  • Un lote no debe fallar por una fila. Cada fila se importa por separado y el resumen final dice cuántas se crearon, actualizaron, omitieron o fallaron.

Código, licencia y feedback

Rosvit_CmsContentSync es gratuito y de código abierto, con licencia MIT. El código está en GitHub y el paquete en Packagist como rosvit/module-cms-content-sync.

Si lo usas y encuentras un caso que no cubre, abre un issue. Me interesa sobre todo saber qué otros tipos de referencias te gustaría que viajaran entre entornos.

Preguntas frecuentes

¿Magento 2 permite exportar páginas y bloques CMS de forma nativa?

No. System → Data Transfer de Magento 2 exporta productos, precios y clientes, pero no páginas ni bloques CMS. Para moverlos entre entornos hace falta un módulo como Rosvit_CmsContentSync, data patches o copiar la base de datos.

¿Funciona con Page Builder?

Sí. Page Builder inserta los bloques CMS con el widget Magento\Cms\Block\Widget\Block, que es una de las tres formas de referencia que el módulo traduce de ID a identifier al exportar y de vuelta a ID al importar.

¿Copia las imágenes de pub/media?

No. El contenido conserva las rutas de las imágenes, pero los archivos de pub/media hay que copiarlos aparte al entorno destino.

¿Qué pasa si una página usa un bloque que no existe en el destino?

La página se importa igual y la referencia queda con el identifier del bloque. La vista previa y el resumen final avisan qué bloques faltan. Como Magento también carga bloques por identifier, la página empieza a mostrar el bloque en cuanto se crea.

¿Sirve para Adobe Commerce?

Sí. Funciona con Magento Open Source y Adobe Commerce 2.4.x sobre PHP 8.2 a 8.5.

¿Es gratuito?

Sí. Es de código abierto con licencia MIT y se instala desde Packagist con composer require rosvit/module-cms-content-sync.