Desarrollo CMS · HTML · TinyMCE
Por qué TinyMCE rompe los saltos de línea (y cómo evitarlo en tu CMS)
No es un bug del editor. Es el parser HTML del navegador cumpliendo la especificación. Si construyes un CMS con TinyMCE, CKEditor o cualquier editor WYSIWYG, vas a encontrarte con esto.
Si has construido un CMS con TinyMCE y el contenido “se rompe” en el frontend, este problema ya lo has vivido.
El problema: el usuario ve espacios que desaparecen
Un usuario edita una página en el panel de administración. Usa TinyMCE. Mete saltos de línea entre secciones para separar visualmente el contenido — algo que hace cualquier persona intuitivamente. En el editor se ve perfecto. Guarda. Va al frontend. Los saltos han desaparecido. Todo el contenido está pegado.
El usuario vuelve al editor. Los saltos siguen ahí. Guarda de nuevo. Frontend: siguen sin aparecer. Abre un ticket: “los espacios entre secciones no funcionan”.
Es un problema clásico que afecta a cualquier CMS que use un editor WYSIWYG y procese el contenido en el backend antes de servirlo.
Qué hace TinyMCE con los saltos de línea
Cuando el usuario pulsa Enter dos veces en TinyMCE para crear una línea en blanco, el editor genera esto:
<p> </p>
Un párrafo con un espacio no-rompible. Es la forma estándar que tienen todos los editores WYSIWYG de representar “una línea en blanco”. El es necesario porque un <p> completamente vacío colapsa a altura cero.
En WordPress esto funciona sin problemas. El HTML pasa por wpautop() y llega al frontend tal cual. En un CMS propio — especialmente uno que procesa shortcodes o componentes dinámicos — es donde se rompe.
Dónde se rompe todo: shortcodes + bloques
Imaginemos que el usuario escribe esto en el editor:
<p> </p>
<p>[slider id=408]</p>
<p> </p>
<p><strong>Feature Films</strong></p>
Un espaciador, un slider, otro espaciador, y un título. En el editor se ve limpio. Pero cuando el backend reemplaza [slider id=408] por el HTML real del slider, queda:
<p> </p>
<p><div class="swiper">...<script>...</script></div></p> ← HTML inválido
<p> </p>
<p><strong>Feature Films</strong></p>
Un <div> dentro de un <p> es HTML inválido. Y aquí es donde el navegador interviene.
El parser HTML hace su trabajo (y rompe el tuyo)
La especificación HTML es explícita: <p> solo puede contener phrasing content — texto, <span>, <strong>, <img>. Nunca bloques como <div>.
Antes de sumergirte en el código, visualiza lo que pasa:
Cuando el parser HTML encuentra un <div> dentro de un <p>, ejecuta el algoritmo de autocorrección: cierra el <p> antes del <div>, reinserta el bloque como hermano, y reconstruye la estructura. En ese reordenamiento, los <p> </p> que estaban cerca se pierden o colapsan.
No es un bug. Es el navegador cumpliendo la especificación. Tu HTML es inválido y el parser lo corrige de la única forma que puede.
Cómo lo resuelve WordPress
WordPress tiene wpautop(), una función que extrae los bloques del contenido para que nunca queden dentro de <p>, luego convierte dobles saltos en párrafos, y finalmente reinserta los bloques en su posición original. Elegante, pero específico de WordPress. Si tu CMS guarda el HTML tal cual lo genera TinyMCE, no tienes esa capa de protección.
La solución: convertir espaciadores antes de inyectar bloques
La idea es sencilla: antes de que el procesamiento de shortcodes genere HTML inválido, convertimos los espaciadores frágiles en elementos que sobrevivan al reordenamiento del DOM.
La implementación puede variar según tu stack, pero el concepto es el mismo: detectar los <p> </p> y reemplazarlos por <div> con altura explícita.
En PHP, antes de procesar shortcodes:
// Convertir espaciadores del editor a elementos robustos
// ANTES de procesar shortcodes
$spacer = '<div class="editor-spacing" style="height:24px;clear:both;"></div>';
$content = preg_replace(
'/<p(\s[^>]*)?\>\s*( |\xC2\xA0|\s)*\s*<\/p>/',
$spacer,
$content
);
// Ahora sí, procesar shortcodes
$content = process_shortcodes($content);
El <div> tiene tres ventajas sobre el <p> original:
- No tiene restricción de contenido. Un
<div>junto a otros<div>es HTML perfectamente válido. - Tiene altura explícita. El
height:24pxinline garantiza el espacio aunque el CSS externo intente colapsarlo. - Tiene
clear:both. Si hay elementos flotantes, el espaciador no se esconde detrás.
El CSS complementario:
.editor-spacing {
display: block !important;
height: 24px !important;
min-height: 24px !important;
margin: 0 !important;
padding: 0 !important;
}
Las variantes que TinyMCE genera
La regex básica cubre el caso estándar, pero TinyMCE genera variantes según cómo el usuario interactúe con el editor. No necesitas cubrir todas desde el primer día — estas son las más comunes:
<!-- Estándar -->
<p> </p>
<!-- Con estilo inline (el usuario centró el texto) -->
<p style="text-align: center;"> </p>
<!-- Con span decorativo (cambió color o fuente) -->
<p><span style="color: #5cc0ff;"> </span></p>
<!-- Con span + strong (cambió formato y luego borró) -->
<p><span><strong> </strong></span></p>
La implementación concreta dependerá de tu lenguaje y de cuántas variantes necesites cubrir. Lo importante es el patrón: cualquier <p> cuyo único contenido real sea (posiblemente envuelto en tags inline) es un espaciador del editor y debe convertirse a <div>.
La trampa: párrafos con imágenes que parecen vacíos
Si añades JavaScript para reforzar los espaciadores — por ejemplo, recorriendo los <p> para detectar cuáles están “vacíos” — ten cuidado con este falso positivo:
<p>
<a href="..."><img src="foto1.jpg" width="150"></a>
<a href="..."><img src="foto2.jpg" width="150"></a>
</p>
// textContent = " " (solo espacios entre los <a>)
// Las imágenes NO generan texto
// trimmed === "" → true → ¡FALSO POSITIVO!
Un script que evalúe trimmed === "" le pondrá height: 24px a un párrafo con imágenes de 150px dentro. Resultado: las imágenes se desbordan y el texto siguiente se superpone encima de ellas.
La solución es una línea al inicio del loop:
// Saltar párrafos que contienen media
if (p.querySelector('img, video, iframe, canvas, svg')) return;
Conclusión
El problema de los saltos de línea no es un bug de TinyMCE, ni de tu CMS, ni del CSS. Es una consecuencia natural de cómo funciona HTML: los <p> no pueden contener bloques, y cuando lo intentas, el parser autocorrige destruyendo la estructura que esperabas.
La solución es intervenir en el punto correcto del pipeline — antes de que la inyección de bloques genere HTML inválido — y convertir los espaciadores frágiles en elementos robustos que sobrevivan al reordenamiento del DOM.
Si estás construyendo un CMS y no controlas este punto del pipeline, no es cuestión de si fallará — es cuestión de cuándo.
Este comportamiento lo encontramos y resolvimos desarrollando el motor de renderizado de contenido de Laraflex, donde TinyMCE se combina con shortcodes de sliders, galerías y componentes dinámicos.