Cómo extraer el primer enlace del contenido de una entrada (fragmento PHP)

Cómo extraer el primer enlace del contenido de una entrada (fragmento PHP)

Última verificación: 14 de octubre de 2026
21 min de lectura
Tutorial
Desarrollador full-stack

A veces construimos temas tipo “agregador de noticias”, donde una entrada no tiene su propio contenido, sino que solo enlaza a un artículo externo. O queremos que la primera imagen del contenido se convierta automáticamente en la “imagen destacada” si el editor olvida establecerla.

Descubre más sobre desarrollo profesional WordPress en WPPoland. En ambos casos, necesitamos “escanear” el contenido de la entrada (the_content) y extraer de él la primera etiqueta <a> o <img>.

Esta guía cubre las opciones reales en PHP y WordPress: WP_HTML_Tag_Processor (en el núcleo desde WordPress 6.2), la función nativa get_url_in_content(), WP_HTML_Processor cuando importa el contexto del enlace, la clase DOMDocument de PHP y el nuevo Dom\HTMLDocument de PHP 8.4, además de la caché del resultado para no repetir el análisis en cada visita.

#Por qué regex falla al extraer enlaces de HTML

Muchos desarrolladores usan Expresiones Regulares (Regex) para esto, pero analizar HTML con Regex es una mala práctica. Aquí está el enfoque con Regex, solo para comparación:

function get_first_link_url_regex( $content ) {
    if ( empty( $content ) ) return false;

    // Patrón para encontrar la primera etiqueta <a>
    $pattern = '/<a\s[^>]*href=["\']([^"\']+)["\'][^>]*>/i';

    if ( preg_match( $pattern, $content, $matches ) ) {
        return $matches[1];
    }

    return false;
}

Funciona en una prueba con una frase corta y falla en silencio con entradas reales:

  1. Comentarios de bloque y borradores. Gutenberg delimita cada bloque con comentarios HTML como <!-- wp:paragraph -->. Si un editor comentó un enlace antiguo, el patrón lo captura antes que cualquier enlace visible.
  2. Contenido de <script> y <style>. Un bloque HTML personalizado cuyo JavaScript monta una cadena como "<a href=...>" contiene, para Regex, una etiqueta de enlace perfectamente válida. Un analizador HTML sabe que el interior de <script> es texto y no marcado.
  3. Saltos de línea y orden de atributos. Atributos repartidos en varias líneas, comillas simples o dobles y atributos data-* antes de href rompen patrones ingenuos.
  4. El límite de backtracking. El motor PCRE de PHP se detiene cuando un patrón supera pcre.backtrack_limit, cuyo valor por defecto es 1000000 según el manual de PHP. Entonces preg_match() devuelve false en lugar de 0 o 1, y solo preg_last_error() informa de PREG_BACKTRACK_LIMIT_ERROR. En entradas largas el enlace simplemente desaparece, sin nada en el log.

Por qué no usar Regex:

  • Falla con HTML malformado
  • No maneja correctamente atributos en diferente orden
  • Problemas con HTML anidado
  • Difícil de mantener y depurar
  • No entiende la estructura del DOM

El propio núcleo de WordPress aprendió esta lección: get_url_in_content() funcionó con una expresión regular durante más de una década antes de pasar a la HTML API, como se explica más abajo.

#Extraer el primer enlace con WP_HTML_Tag_Processor

WordPress 6.2 incorporó WP_HTML_Tag_Processor al núcleo. La documentación de la clase lo describe así: no analiza el HTML por completo ni recorre su estructura de forma recursiva; avanza por el documento de forma lineal y solo analiza las etiquetas de apertura. Es justo lo que hace falta para leer un href, sin el coste de memoria de construir un árbol DOM completo.

Probamos el comportamiento con el código de WordPress 7.1.3: next_tag( 'a' ) salta los comentarios HTML y el contenido de <script>, y get_attribute( 'href' ) devuelve el valor ya decodificado, de modo que &amp; en una query string llega a tu código como &. Si el atributo no existe devuelve null, y en un atributo booleano sin valor devuelve true, así que la comprobación is_string() no es opcional.

<?php
declare(strict_types=1);

/**
 * Devuelve la URL del primer enlace http(s) de un fragmento HTML.
 */
function wppoland_primer_enlace_tag_processor( string $content ): ?string {
    if ( '' === trim( $content ) ) {
        return null;
    }

    $processor = new WP_HTML_Tag_Processor( $content );

    // Recorre las etiquetas <a> en el orden del documento
    while ( $processor->next_tag( 'a' ) ) {
        $href = $processor->get_attribute( 'href' );

        if ( ! is_string( $href ) || '' === trim( $href ) ) {
            continue;
        }

        $href    = trim( $href );
        $esquema = strtolower( (string) wp_parse_url( $href, PHP_URL_SCHEME ) );

        // Ignora mailto:, tel:, anclas y rutas relativas
        if ( in_array( $esquema, array( 'http', 'https' ), true ) ) {
            return esc_url_raw( $href );
        }
    }

    return null;
}

Ventajas frente a las demás opciones en WordPress 6.2 o posterior:

  • Memoria: el procesador avanza por la cadena sin crear un nodo por cada etiqueta.
  • Resistencia: los comentarios, el contenido de <script> y <style> y los atributos sin comillas no lo confunden, porque sigue las reglas de tokenización de HTML en lugar de buscar patrones de texto.
  • Sin extensiones extra: viene con WordPress y no necesita ext-dom ni libxml.

#Usar get_url_in_content en WordPress

Antes de escribir tu propia función, conviene saber que el núcleo ya tiene una: get_url_in_content( $content ), documentada como “extrae y devuelve la primera URL del contenido recibido” (traducción propia de la referencia oficial). Existe desde WordPress 3.6.0 y se usa con el formato de entrada “link”: el tema Twenty Thirteen la llama en twentythirteen_get_link_url() y, si no hay enlace, vuelve al enlace permanente de la entrada.

La implementación cambió de forma importante. Comparamos el código de las ramas 6.8 y 6.9 del repositorio oficial:

  • Hasta WordPress 6.8 la función usaba preg_match( '/<a\s[^>]*?href=([\'"])(.+?)\1/is', ... ). Con el mismo HTML de prueba de este artículo, ese patrón devolvió el enlace que estaba dentro de un comentario HTML, un enlace que ningún lector vio.
  • Desde WordPress 6.9 la función crea un WP_HTML_Tag_Processor, recorre las etiquetas A y devuelve el primer href no vacío pasado por sanitize_url(). El enlace comentado ya no aparece.
$url = get_url_in_content( get_the_content() );

if ( $url ) {
    printf(
        '<a href="%s" rel="noopener">Leer en la fuente</a>',
        esc_url( $url )
    );
}

Devuelve false cuando no encuentra nada. Tiene dos límites. Acepta cualquier esquema, así que un mailto: o un #comentarios al principio del texto se devuelve antes que el enlace editorial. Y en un sitio que aún funciona con WordPress 6.8 o anterior obtienes el comportamiento de la expresión regular. En la 6.9 o posterior, si el primer href es lo que necesitas, usa la función del núcleo; si necesitas filtrar esquemas o soportar versiones antiguas, la función de la sección anterior encaja mejor.

#Encontrar el primer enlace dentro de un párrafo con WP_HTML_Processor

El Tag Processor no sabe dónde está. Para él, el enlace que envuelve una imagen en un bloque core/image y el enlace en mitad de un párrafo son solo dos etiquetas a. Cuando la regla es “el primer enlace escrito en el texto, no el de la imagen”, necesitas WP_HTML_Processor, disponible desde WordPress 6.4. Mantiene la pila de elementos abiertos y la expone mediante get_breadcrumbs().

<?php
function wppoland_primer_enlace_en_parrafo( string $content ): ?string {
    $processor = WP_HTML_Processor::create_fragment( $content );

    if ( null === $processor ) {
        return null;
    }

    while ( $processor->next_tag( 'A' ) ) {
        $ruta = $processor->get_breadcrumbs();

        // Solo enlaces dentro de <p> y fuera de <figure>
        if ( ! in_array( 'P', $ruta, true ) || in_array( 'FIGURE', $ruta, true ) ) {
            continue;
        }

        $href = $processor->get_attribute( 'href' );
        if ( is_string( $href ) && '' !== trim( $href ) ) {
            return esc_url_raw( trim( $href ) );
        }
    }

    return null;
}

Comprobamos la ruta nosotros mismos en lugar de usar la consulta next_tag( array( 'breadcrumbs' => array( 'P', 'A' ) ) ). Esa consulta exige que el A sea hijo directo del P; en nuestra prueba, un enlace dentro de <p><strong><a> quedó fuera con ella y se encontró con la comprobación in_array().

La documentación de la clase deja clara una regla: cuando el procesador encuentra marcado que aún no soporta, se detiene en lugar de arriesgar una interpretación errónea. En ese caso next_tag() devuelve false y get_last_error() deja de ser null. Si obtienes null en una entrada con enlaces visibles, revisa get_last_error() y recurre al Tag Processor.

#Obtener el primer enlace con DOMDocument en PHP

Fuera de WordPress, o en código heredado, la clase integrada DOMDocument de PHP (basada en libxml2) sigue siendo la opción clásica. Su analizador HTML es anterior a HTML5, así que hay que suprimir avisos sobre etiquetas como <section>, conservar UTF-8 y vaciar el búfer de errores de libxml.

Aquí tienes una función lista para pegar en functions.php:

function get_first_link_url( $content ) {
    // Si el contenido está vacío, devolver false
    if ( empty( $content ) ) return false;

    $doc = new DOMDocument();

    // Suprimir errores de HTML5 (DOMDocument es antiguo y a veces se queja de <section> etc.)
    $errores_previos = libxml_use_internal_errors( true );

    // Cargar HTML forzando UTF-8 con la cabecera XML
    $doc->loadHTML( '<?xml encoding="utf-8" ?>' . $content );

    libxml_clear_errors();
    libxml_use_internal_errors( $errores_previos );

    $links = $doc->getElementsByTagName('a');

    if ( $links->length > 0 ) {
        // Devolver href del primer enlace
        return $links->item(0)->getAttribute('href');
    }

    return false;
}

Muchos ejemplos usan mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ). PHP 8.2 declaró obsoleta la codificación HTML-ENTITIES en todas las funciones mbstring (lista de obsolescencias de PHP 8.2) y PHP 8.4 emite un aviso Deprecated en la llamada. El propio aviso propone mb_encode_numericentity() como sustituto; la cabecera XML de arriba resuelve lo mismo sin tocar mbstring.

#Mostrar el primer enlace en el loop de WordPress

$link = get_first_link_url( get_the_content() );

if ( $link ) {
   echo '<a href="' . esc_url($link) . '" class="read-more-external">Leer original</a>';
}

Esta solución es sólida, segura y maneja errores en la estructura HTML mejor que cualquier Regex. Ten en cuenta que get_the_content() no aplica los filtros de the_content: devuelve el contenido guardado, con los comentarios de bloque incluidos.

#Usar Dom\HTMLDocument en PHP 8.4

PHP 8.4 añadió una nueva API DOM en el espacio de nombres Dom, con “soporte conforme a los estándares para analizar documentos HTML5”, según el anuncio de la versión (traducción propia). Dom\HTMLDocument analiza HTML según el living standard, lo que elimina los avisos sobre etiquetas HTML5 y el truco de la cabecera XML. DOMDocument sigue disponible por compatibilidad.

<?php
function wppoland_primer_enlace_dom84( string $html ): ?string {
    if ( '' === trim( $html ) ) {
        return null;
    }

    $doc = Dom\HTMLDocument::createFromString( $html, LIBXML_NOERROR, 'UTF-8' );

    foreach ( $doc->querySelectorAll( 'a[href]' ) as $enlace ) {
        $href    = trim( $enlace->getAttribute( 'href' ) );
        $esquema = strtolower( (string) parse_url( $href, PHP_URL_SCHEME ) );

        if ( in_array( $esquema, array( 'http', 'https' ), true ) ) {
            return $href;
        }
    }

    return null;
}

Tres detalles de la página del manual de createFromString(): el segundo argumento acepta constantes LIBXML_* combinadas con OR; el tercero, overrideEncoding, fija la codificación en lugar de dejar que PHP la adivine; y una codificación desconocida lanza ValueError. Con PHP 8.4.24 esta función devolvió https://exemplo.pt/ação?x=1&y=2 intacta, con &amp; decodificado. Los selectores CSS permiten además consultas como p > a[href^="https://"], que con DOMDocument requieren XPath.

#Qué método elegir para extraer el primer enlace

No publicamos tiempos de ejecución: dependen del servidor, de la versión de PHP y del tamaño de la entrada, y solo valen si los mides en tu propio proyecto. Las diferencias de diseño sí son estables:

CriterioRegex (preg_match)DOMDocument (libxml2)Dom\HTMLDocumentWP_HTML_Tag_Processor
Modelo de análisisPatrón de textoÁrbol DOM, reglas previas a HTML5Árbol DOM, HTML5Lectura lineal de etiquetas de apertura
MemoriaBajaÁrbol completoÁrbol completoSin árbol
Comentarios y <script>Poco fiableCorrectosCorrectosCorrectos
Contexto (padre del enlace)NoSíSí, con selectores CSSNo (usa WP_HTML_Processor)
Fallo típicofalse por límite de backtrackingAvisos y UTF-8ValueError con codificación inválidaNo conoce el contexto del enlace
Requisito mínimoCualquier PHPExtensión DOMPHP 8.4WordPress 6.2+

En un sitio WordPress actual, el Tag Processor es la opción fiable más sencilla. Dom\HTMLDocument gana cuando el código se ejecuta fuera de WordPress o necesita selectores.

#Obtener la primera imagen de una entrada

El mismo enfoque funciona para imágenes. Esto es útil para temas de “lista de posts” donde necesitas una miniatura automática:

function get_first_image_url( $content ) {
    if ( empty( $content ) ) return false;

    $doc = new DOMDocument();
    libxml_use_internal_errors(true);
    $doc->loadHTML( '<?xml encoding="utf-8" ?>' . $content );
    libxml_clear_errors();

    $images = $doc->getElementsByTagName('img');

    if ( $images->length > 0 ) {
        return $images->item(0)->getAttribute('src');
    }

    return false;
}

#Usar la primera imagen como imagen destacada

/**
 * Establecer automáticamente la primera imagen del contenido como imagen destacada
 * si no se ha establecido una manualmente.
 */
add_action( 'save_post', function( $post_id ) {
    // Evitar autoguardados y revisiones
    if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
        return;
    }

    // Si ya tiene imagen destacada, no hacer nada
    if ( has_post_thumbnail( $post_id ) ) {
        return;
    }

    $post = get_post( $post_id );
    $image_url = get_first_image_url( $post->post_content );

    if ( $image_url ) {
        // Buscar el attachment por URL
        $attachment_id = attachment_url_to_postid( $image_url );
        if ( $attachment_id ) {
            set_post_thumbnail( $post_id, $attachment_id );
        }
    }
});

attachment_url_to_postid() solo encuentra archivos que ya están en la biblioteca de medios. Una imagen enlazada desde otro dominio no se convierte en miniatura sin descargarla antes.

#Obtener texto, título y rel del primer enlace

A veces necesitas más que solo la URL. Aquí hay una versión que extrae toda la información del enlace:

function get_first_link_data( $content ) {
    if ( empty( $content ) ) return false;

    $doc = new DOMDocument();
    libxml_use_internal_errors(true);
    $doc->loadHTML( '<?xml encoding="utf-8" ?>' . $content );
    libxml_clear_errors();

    $links = $doc->getElementsByTagName('a');

    if ( $links->length > 0 ) {
        $first_link = $links->item(0);

        return array(
            'url'    => $first_link->getAttribute('href'),
            'text'   => $first_link->textContent,
            'title'  => $first_link->getAttribute('title'),
            'target' => $first_link->getAttribute('target'),
            'rel'    => $first_link->getAttribute('rel'),
            'class'  => $first_link->getAttribute('class'),
        );
    }

    return false;
}
$link_data = get_first_link_data( get_the_content() );

if ( $link_data ) {
    printf(
        '<a href="%s" title="%s" target="%s" rel="noopener noreferrer">%s</a>',
        esc_url( $link_data['url'] ),
        esc_attr( $link_data['title'] ),
        esc_attr( $link_data['target'] ?: '_blank' ),
        esc_html( $link_data['text'] )
    );
}

#Obtener el primer enlace externo de una entrada

Para temas más complejos, puedes necesitar filtrar enlaces por tipo (internos vs externos):

function get_first_external_link( $content ) {
    if ( empty( $content ) ) return false;

    $doc = new DOMDocument();
    libxml_use_internal_errors(true);
    $doc->loadHTML( '<?xml encoding="utf-8" ?>' . $content );
    libxml_clear_errors();

    $links = $doc->getElementsByTagName('a');
    $site_host = wp_parse_url( home_url(), PHP_URL_HOST );

    foreach ( $links as $link ) {
        $href = trim( $link->getAttribute('href') );

        // Saltar enlaces vacíos y anclas
        if ( '' === $href || strpos( $href, '#' ) === 0 ) {
            continue;
        }

        // Comprobar si es externo comparando el host
        $host = wp_parse_url( $href, PHP_URL_HOST );
        if ( $host && $host !== $site_host ) {
            return $href;
        }
    }

    return false;
}

Comparar el host con wp_parse_url() es más seguro que buscar home_url() dentro de la cadena con strpos(): un enlace externo que incluya la URL del sitio en su query string se clasificaría como interno.

#Qué URLs rechazan wp_http_validate_url y FILTER_VALIDATE_URL

Los dos nombres sugieren “¿es una URL válida?”. Ninguno responde a esa pregunta para enlaces editoriales.

wp_http_validate_url() protege las peticiones HTTP que hace el propio servidor. Según su página de referencia y su código:

  • acepta solo http y https y rechaza URLs con usuario y contraseña;
  • rechaza URLs relativas, porque exige un host;
  • para cualquier host distinto del propio sitio llama a gethostbyname(), es decir, hace una consulta DNS, y si el nombre no se resuelve en ese momento la URL se rechaza;
  • rechaza direcciones de redes privadas salvo que el filtro http_request_host_is_external las permita;
  • por defecto solo admite los puertos 80, 443 y 8080 (filtro http_allowed_safe_ports).

En un hook save_post eso significa una consulta DNS por cada guardado y la pérdida silenciosa de enlaces válidos a dominios caídos un momento o a servidores de demostración en un puerto no estándar. Úsala solo cuando tu código vaya a pedir esa URL.

El filtro FILTER_VALIDATE_URL de PHP tiene otro problema, documentado en el manual: “este filtro solo funciona con URLs ASCII”, así que los nombres de dominio internacionalizados se rechazan siempre (traducción propia). En nuestra prueba con PHP 8.4, https://exemplo.pt/ação fue rechazada, y FILTER_SANITIZE_URL convirtió la misma dirección en https://exemplo.pt/ao, borrando las letras acentuadas sin avisar. Para un sitio en español, con URLs como /años/ o /diseño/, esto importa.

#Validar las URLs extraídas en PHP

Siempre valida las URLs extraídas antes de usarlas. Basta con comprobar el esquema; no hace falta FILTER_VALIDATE_URL:

function get_safe_first_link( $content ) {
    $url = get_first_link_url( $content );

    if ( ! $url ) return false;

    // Solo permitir protocolos seguros
    $allowed_protocols = array( 'http', 'https' );
    $esquema = strtolower( (string) wp_parse_url( trim( $url ), PHP_URL_SCHEME ) );

    if ( ! in_array( $esquema, $allowed_protocols, true ) ) {
        return false;
    }

    return esc_url_raw( trim( $url ) );
}

Usa esc_url_raw() antes de guardar la URL en la base de datos y esc_url() justo antes de imprimirla en el HTML.

#Cachear el primer enlace con transients

#Cachear el primer enlace con get_transient()

Si extraes enlaces frecuentemente, almacena en caché los resultados:

function get_cached_first_link( $post_id = null ) {
    $post_id = $post_id ?: get_the_ID();
    $cache_key = 'first_link_' . $post_id;

    $result = get_transient( $cache_key );
    if ( false === $result ) {
        $post = get_post( $post_id );
        $result = get_first_link_url( $post->post_content );
        $result = $result ?: 'none'; // Almacenar 'none' para evitar consultas repetidas

        set_transient( $cache_key, $result, HOUR_IN_SECONDS );
    }

    return $result === 'none' ? false : $result;
}

// Invalidar caché al actualizar la entrada
add_action( 'save_post', function( $post_id ) {
    delete_transient( 'first_link_' . $post_id );
});

#Cachear el primer enlace con Redis o Memcached

Para sitios con Redis o Memcached:

function get_cached_first_link_object_cache( $post_id = null ) {
    $post_id = $post_id ?: get_the_ID();
    $cache_key = 'first_link_' . $post_id;

    $result = wp_cache_get( $cache_key, 'post_links' );
    if ( false === $result ) {
        $post = get_post( $post_id );
        $result = get_first_link_url( $post->post_content ) ?: '';
        wp_cache_set( $cache_key, $result, 'post_links', 3600 );
    }

    return $result ?: false;
}

#Guardar el primer enlace en post meta con save_post

Los transients caducan y la caché de objetos se vacía. Si el enlace solo cambia cuando el redactor guarda la entrada, lo más estable es calcularlo en ese momento y guardarlo como metadato:

add_action( 'save_post', function ( int $post_id, WP_Post $post ): void {
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }
    if ( wp_is_post_revision( $post_id ) || 'post' !== $post->post_type ) {
        return;
    }

    $url = wppoland_primer_enlace_tag_processor( $post->post_content );

    if ( $url ) {
        update_post_meta( $post_id, '_wppoland_primer_enlace', $url );
    } else {
        delete_post_meta( $post_id, '_wppoland_primer_enlace' );
    }
}, 10, 2 );

WP_Query carga por defecto los metadatos de las entradas del loop en la caché de objetos, así que leer este metadato en un archivo no añade una consulta por tarjeta. Si añades una comprobación current_user_can() al hook, recuerda que los guardados sin usuario conectado, como wp post update en WP-CLI sin --user o una importación por cron, dejarán de actualizar el metadato.

#Rellenar el post meta de entradas antiguas con WP-CLI

El hook save_post solo cubre las entradas guardadas después de publicar el código. Para el archivo existente bastan dos comandos documentados de WP-CLI. wp post list --field=ID imprime un ID por línea y, según la documentación del comando, devuelve por defecto todas las entradas que coinciden (--posts_per_page vale -1). wp eval-file ejecuta un archivo PHP con WordPress cargado y coloca los argumentos posicionales en la variable $args (documentación).

<?php
// rellenar-primer-enlace.php
foreach ( $args as $post_id ) {
    $post = get_post( (int) $post_id );
    if ( ! $post ) {
        continue;
    }

    $url = wppoland_primer_enlace_tag_processor( $post->post_content );

    if ( $url ) {
        update_post_meta( $post->ID, '_wppoland_primer_enlace', $url );
    } else {
        delete_post_meta( $post->ID, '_wppoland_primer_enlace' );
    }
}
wp post list --post_type=post --post_status=publish --field=ID \
  | xargs -n 100 wp eval-file rellenar-primer-enlace.php

xargs -n 100 pasa los IDs en lotes de 100 y cada lote arranca un proceso PHP nuevo, así que la memoria no crece con el tamaño del archivo. Ejecútalo primero en staging y revisa a mano algunas entradas.

#Por qué no aparecen los enlaces de bloques dinámicos y shortcodes

El post_content guardado en la base de datos no es el HTML que recibe el visitante. Hay dos casos en los que el enlace existe en la página pero no en el contenido en bruto:

  • Bloques dinámicos. El bloque core/navigation-link guarda su url como atributo en el comentario del bloque, en JSON, y el HTML se genera en el servidor. En cambio, core/button guarda el enlace en su propio HTML: en block.json el atributo url se lee del href de la etiqueta a (fuente attribute, selector a). La referencia de bloques del núcleo lista los atributos de cada bloque.
  • Shortcodes y embeds. Solo se expanden cuando el contenido pasa por el filtro the_content.

Si la regla es “el primer enlace que escribió el redactor”, analiza $post->post_content. Si es “el primer enlace que ve el visitante”, pasa antes el contenido por do_blocks() y do_shortcode(), o por apply_filters( 'the_content', $content ). Cuidado: el filtro completo también ejecuta lo que añaden los plugins (botones para compartir, entradas relacionadas), que puede insertar enlaces antes que el tuyo.

WordPress tiene formatos de entrada como “Link” que se benefician de esta funcionalidad. En WordPress 6.9 o posterior puedes usar directamente get_url_in_content(); el ejemplo usa get_safe_first_link() para quedarse solo con enlaces http y https:

// En functions.php - Soporte para formatos de entrada
add_theme_support( 'post-formats', array( 'link', 'image', 'video', 'quote' ) );

// En el template - Obtener URL para formato 'link'
if ( has_post_format( 'link' ) ) {
    $link = get_safe_first_link( get_the_content() );
    if ( $link ) {
        echo '<a href="' . esc_url( $link ) . '" class="post-link-format" target="_blank" rel="noopener">';
        the_title();
        echo '</a>';
    }
} else {
    echo '<a href="' . esc_url( get_permalink() ) . '">';
    the_title();
    echo '</a>';
}

#Mostrar enlaces externos en un tema de agregador de noticias

Un caso de uso completo para un tema estilo “agregador de noticias”:

// En archive.php o index.php
while ( have_posts() ) : the_post();
    $external_link = get_first_link_url( get_the_content() );
    $target_url = $external_link ?: get_permalink();
    $is_external = (bool) $external_link;
    ?>

    <article class="news-item <?php echo $is_external ? 'external' : 'internal'; ?>">
        <?php if ( has_post_thumbnail() ) : ?>
            <a href="<?php echo esc_url( $target_url ); ?>"
               <?php echo $is_external ? 'target="_blank" rel="noopener noreferrer"' : ''; ?>>
                <?php the_post_thumbnail( 'medium' ); ?>
            </a>
        <?php endif; ?>

        <h2>
            <a href="<?php echo esc_url( $target_url ); ?>"
               <?php echo $is_external ? 'target="_blank" rel="noopener noreferrer"' : ''; ?>>
                <?php the_title(); ?>
                <?php if ( $is_external ) : ?>
                    <span class="external-icon" aria-label="Enlace externo">&#8599;</span>
                <?php endif; ?>
            </a>
        </h2>

        <div class="meta">
            <?php if ( $is_external ) : ?>
                <span class="source"><?php echo esc_html( wp_parse_url( $external_link, PHP_URL_HOST ) ); ?></span>
            <?php endif; ?>
            <time datetime="<?php echo esc_attr( get_the_date('c') ); ?>"><?php echo esc_html( get_the_date() ); ?></time>
        </div>
    </article>

<?php endwhile; ?>

En un archivo con muchas entradas, sustituye get_first_link_url( get_the_content() ) por la lectura del metadato _wppoland_primer_enlace para no analizar el HTML de cada tarjeta en cada visita.

#Tratar anclas, mailto y rutas relativas

No todos los atributos href contienen direcciones web remotas. En el contenido del editor es habitual encontrar:

  • Anclas locales: #comentarios, #introduccion.
  • Enlaces de contacto: mailto:[email protected], tel:+34910000000.
  • Rutas relativas: /sobre-nosotros/, ./descargas/guia.pdf.

Las funciones de este artículo basadas en WP_HTML_Tag_Processor y Dom\HTMLDocument, igual que get_safe_first_link(), ya descartan los esquemas que no son http o https. Si quieres aceptar rutas relativas, conviértelas antes en direcciones absolutas: el núcleo tiene WP_Http::make_absolute_url( $href, home_url( '/' ) ), disponible desde WordPress 3.4.0. No lo confundas con wp_make_link_relative(), que hace lo contrario.

#Resumen

La extracción de enlaces del contenido es una técnica fundamental para desarrolladores WordPress. Un analizador HTML es siempre la opción correcta frente a Regex. Recuerda siempre:

  • Usar WP_HTML_Tag_Processor en WordPress 6.2 o posterior, get_url_in_content() en la 6.9 o posterior si te vale el primer href, y DOMDocument o Dom\HTMLDocument fuera de WordPress
  • Manejar correctamente la codificación UTF-8 sin el truco obsoleto de HTML-ENTITIES
  • Validar el esquema con wp_parse_url() en lugar de FILTER_VALIDATE_URL o wp_http_validate_url()
  • Almacenar los resultados en post meta al guardar y rellenar las entradas antiguas con WP-CLI
  • Considerar la seguridad al mostrar enlaces externos
Siguiente paso

Transforma el artículo en una implementación real

Este bloque refuerza el enlazado interno y lleva al lector al siguiente paso más útil dentro de la arquitectura del sitio.

¿Quieres implementar esto en tu sitio?

Si quieres transformar el artículo en mejoras concretas, rediseño o un plan de implementación, puedo cerrar el alcance y ejecutar.

Cluster relacionado

Explora otros servicios WordPress y base de conocimiento

Refuerza tu negocio con soporte técnico profesional en áreas clave del ecosistema WordPress.

Preguntas frecuentes

Respuestas prácticas para aplicar el tema en la ejecución real.

¿Por qué usar DOMDocument en lugar de Regex para extraer enlaces?#
DOMDocument es más fiable y seguro para analizar HTML. Regex puede fallar con HTML malformado, atributos en orden diferente o HTML anidado. DOMDocument maneja estos casos correctamente.
¿Cómo manejar contenido con caracteres UTF-8?#
Antepón al HTML una declaración XML con codificación UTF-8 antes de loadHTML(). El antiguo truco mb_convert_encoding con HTML-ENTITIES está obsoleto desde PHP 8.2. En PHP 8.4 puedes usar Dom\HTMLDocument con la codificación indicada de forma explícita.
¿Se puede extraer la primera imagen en lugar del primer enlace?#
Sí. Simplemente cambia getElementsByTagName('a') por getElementsByTagName('img') y getAttribute('href') por getAttribute('src').
¿get_url_in_content() devuelve enlaces mailto o anclas?#
Devuelve el primer href no vacío, sea cual sea el esquema. Un mailto: o un #comentarios al principio del texto gana al enlace editorial; si solo necesitas http o https, escribe tu propia función.
¿Necesito WordPress para usar Dom\HTMLDocument?#
No. Dom\HTMLDocument forma parte de la extensión DOM de PHP 8.4 y funciona en cualquier script PHP, por ejemplo en un importador que se ejecuta fuera de WordPress.

¿Necesitas un FAQ adaptado a tu sector y mercado? Preparamos una versión alineada con tus objetivos de negocio.

Hablemos

Artículos Relacionados