Skip to content

Extraer el Contenido del Body de Documentos HTML Completos en PHP con DOMDocument

Introducción

Editores de contenido, page builders e importadores suelen recibir dos formas distintas de HTML:

  • un fragmento, como <p>Contáctanos</p>
  • un documento completo, como <!doctype html><html><head>...</head><body>...</body></html>

Si tu componente espera un fragmento, renderizar un documento completo dentro de él puede crear markup inválido, metadata duplicada o bugs de layout. La solución no es un sanitizador completo. Es un paso pequeño de normalización: cuando el input es un documento completo, extraer solo el contenido interno de <body>. Cuando el input ya es un fragmento, dejarlo igual.

Este artículo muestra un helper práctico en PHP usando DOMDocument.

El Objetivo

El helper debe seguir tres reglas:

  • input vacío devuelve una cadena vacía
  • fragmentos HTML normales se devuelven sin cambios
  • documentos HTML completos devuelven solo los hijos del body

Por ejemplo:

php
$html = '<!doctype html>
<html>
    <head><title>Ignorado</title></head>
    <body>
        <section>
            <h2>Formulario de reserva</h2>
            <p>Elige una hora.</p>
        </section>
    </body>
</html>';

echo bodyContents($html);

Salida:

html
<section>
    <h2>Formulario de reserva</h2>
    <p>Elige una hora.</p>
</section>

Pero este fragmento queda exactamente igual:

php
echo bodyContents('<p>Ya es un fragmento</p>');
// <p>Ya es un fragmento</p>

El Helper

Esta es la implementación completa:

php
function bodyContents(string $html): string
{
    $trimmed = trim($html);

    if ($trimmed === '') {
        return '';
    }

    if (! preg_match('/<html\b|<body\b|<!doctype/i', $trimmed)) {
        return $html;
    }

    $dom = new DOMDocument('1.0', 'UTF-8');

    libxml_use_internal_errors(true);
    $loaded = $dom->loadHTML(
        '<?xml encoding="UTF-8">' . $trimmed,
        LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED
    );
    libxml_clear_errors();

    if (! $loaded) {
        return $html;
    }

    $body = $dom->getElementsByTagName('body')->item(0);

    if (! $body) {
        return $html;
    }

    $inner = '';

    foreach ($body->childNodes as $child) {
        $inner .= $dom->saveHTML($child);
    }

    return $inner;
}

La función es conservadora a propósito. Solo parsea el input que parece un documento completo. Así evita pasar cada fragmento pequeño por DOMDocument, que puede normalizar espacios, reparar tags o cambiar levemente el formato de salida.

Por Qué Detectar Antes de Parsear

DOMDocument::loadHTML() es útil, pero no es un formateador neutral. Parsea y repara markup. Para HTML con forma de documento, eso es justo lo que queremos. Para fragmentos simples, puede ser innecesario y sorpresivo.

Este guard mantiene simple el camino común:

php
if (! preg_match('/<html\b|<body\b|<!doctype/i', $trimmed)) {
    return $html;
}

El patrón detecta las señales habituales de un documento completo:

  • un tag <html>
  • un tag <body>
  • una declaración <!doctype>

Si no existe ninguna de esas señales, el helper asume que el input ya sirve para renderizarse inline.

Parsear Sin Warnings

El HTML real de editores puede incluir tags HTML5, documentos incompletos o markup que genera quejas en libxml. El manual de PHP para DOMDocument::loadHTML indica que el comportamiento del parser depende de libxml y que el HTML moderno puede producir warnings.

Para un helper de normalización, esos warnings no deberían aparecer en logs o respuestas, así que el parser usa manejo interno de errores:

php
libxml_use_internal_errors(true);
$loaded = $dom->loadHTML($source, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED);
libxml_clear_errors();

Las flags reducen wrappers extra de documento en la salida serializada. Aun así, la función trata un fallo de parseo como algo no fatal:

php
if (! $loaded) {
    return $html;
}

Ese fallback importa. Si el helper no puede extraer un body con confianza, debe devolver el input original en vez de descartar contenido silenciosamente.

Preservar UTF-8

La implementación antepone una pista de encoding XML antes de parsear:

php
'<?xml encoding="UTF-8">' . $trimmed

Este es un workaround común de DOMDocument para contenido UTF-8. Sin una señal explícita de encoding, los caracteres no ASCII pueden interpretarse mal dependiendo del input y del comportamiento de libxml.

Si tus documentos fuente ya tienen metadata de charset confiable, puede que no necesites este enfoque exacto. Para snippets pegados y contenido de CMS, la pista explícita es un buen default defensivo.

Extraer Solo los Hijos del Body

Cuando el documento ya cargó, el paso importante es serializar cada hijo de <body>:

php
$body = $dom->getElementsByTagName('body')->item(0);

$inner = '';

foreach ($body->childNodes as $child) {
    $inner .= $dom->saveHTML($child);
}

DOMDocument::saveHTML() puede serializar el documento completo o un nodo específico. El manual de PHP documenta ese parámetro opcional en DOMDocument::saveHTML. Pasar cada nodo hijo nos da el equivalente a innerHTML en el navegador.

Así evitamos devolver el wrapper <body>.

Usarlo en un Pipeline de Views

Un caso común es normalizar la forma del HTML guardado antes de entregarlo a un componente:

php
$template = bodyContents((string) $storedTemplate);

return view('components.dynamic-form', [
    'template' => $template,
]);

En Laravel Blade, podría verse así:

blade
@php
    $template = \App\Support\Html::bodyContents((string) $template);
@endphp

<livewire:dynamic-form :template="$template" />

Ahora los editores pueden pegar un fragmento o un documento HTML completo, y el componente igual recibe la forma que espera.

Límite de Seguridad Importante

Importante

Este helper no sanitiza HTML. Solo extrae el contenido del body cuando el input tiene forma de documento.

Si los usuarios pueden enviar HTML no confiable, ejecuta un sanitizador real después de la extracción. Por ejemplo, usa un sanitizador basado en allowlist como HTML Purifier o el sanitizador ya aprobado en tu stack.

El pipeline seguro es:

  1. normalizar la forma del documento con bodyContents()
  2. sanitizar tags y atributos no confiables
  3. renderizar con las reglas de escaping correctas para tu framework

No trates el parseo DOM como un filtro de seguridad.

Nota Sobre PHP 8.4

Para código nuevo en PHP 8.4+, revisa también Dom\HTMLDocument, que PHP recomienda para parseo compatible con HTML5. DOMDocument::loadHTML() sigue estando ampliamente disponible y funciona bien para esta tarea estrecha de extracción, pero el parseo de HTML moderno sigue evolucionando.

Si necesitas construcción precisa de árboles HTML5, prefiere el parser nuevo. Si solo necesitas un helper pequeño de compatibilidad en una app PHP 8.1/8.2/8.3, DOMDocument sigue siendo una opción práctica.

Conclusión

bodyContents() es pequeño, pero evita un problema común de render: documentos HTML completos filtrándose dentro de componentes que esperan fragmentos.

Los patrones útiles son:

  • detectar input con forma de documento antes de parsear
  • dejar los fragmentos sin cambios
  • parsear con manejo silencioso de errores de libxml
  • extraer los hijos del body con saveHTML($child)
  • volver al input original cuando el parseo falle

Así tu pipeline de contenido recibe una forma HTML estable sin fingir que resuelve sanitización, validación o limpieza completa de HTML.