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

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ón | Qué resuelve | Dónde falla |
|---|---|---|
| Copiar y pegar desde el admin | No requiere desarrollo | Es lento y es fácil equivocarse con un campo o un store view |
| Copiar la base de datos o tablas sueltas | Mueve todo de una vez | Pisa contenido que producción ya tiene y arrastra IDs que no coinciden |
| Data patches con el contenido en código | Es repetible y queda versionado | Cada cambio de texto exige un desarrollador y un despliegue |
| Extensiones de import/export CMS | Resuelven el caso desde el admin | La 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
defaultoen, 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:flushEl 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?
- En el entorno de origen ve a Content → Pages o Content → Blocks.
- Selecciona las filas que quieres llevar.
- Elige Actions → Export to JSON. Se descarga un archivo como
cms-content-export-20261005-153000.json.


¿Cómo importar páginas y bloques CMS en otro entorno?
- En el entorno destino ve a Content → CMS Content Sync.
- Elige el archivo
.json(hasta 8 MB) y haz clic en Upload and Preview. - Revisa la vista previa y deja marcadas las filas que quieres importar.
- Haz clic en Import Selected. Al terminar ves cuántas filas se crearon, actualizaron, omitieron o fallaron.

La columna Status de la vista previa dice qué va a pasar con cada fila:
| Estado | Qué significa | Marcada por defecto |
|---|---|---|
| Create | No existe en el destino y se va a crear | Sí |
| Update | Existe y el archivo trae cambios | Sí |
| No changes | Existe y es idéntica a lo que trae el archivo | No |
| Error | No se puede importar, por ejemplo por un store view que no existe | No se puede marcar |

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 ejemploblock_id="footer_links". - Al importar, el módulo busca
footer_linksen 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.


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_xmlycustom_layout_update_xmlviajan 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.
nully vacío no son lo mismo. Si el archivo es la fuente de verdad, tiene que poder expresar "borra esta meta description". Por eso losnullse 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.