Has construido un gestor de marcadores, un directorio de enlaces o un panel que muestra otros sitios web. Quieres que el favicon de cada sitio haga que la lista se vea pulida — pero lanzar a ciegas una petición a /favicon.ico por cada URL es frágil. Muchos sitios publican iconos SVG, usan rutas personalizadas o dependen de etiquetas <link> condicionales en lugar del archivo raíz por defecto.

Este tutorial te guía paso a paso por un flujo de PHP para obtener el favicon a partir de una URL: analizar el HTML en busca de declaraciones <link rel="icon">, recurrir al endpoint heredado /favicon.ico como fallback, validar la respuesta y guardar el binario en local.

Requisitos previos

Necesitas PHP 7.4 o superior con dos extensiones habilitadas:

Ambas vienen incluidas en la imagen oficial de PHP para Docker y en la mayoría de distribuciones de Linux. Ejecuta php -m para confirmar que aparecen listadas antes de empezar a programar.

Paso 1: Obtener el HTML de la página

Empieza con una petición cURL básica. La cabecera User-Agent importa más de lo que parece — algunos sitios devuelven un HTML simplificado a clientes que no son navegadores, lo que puede omitir por completo las etiquetas <link> del favicon.

<?php
function fetchHtml(string $url): string|false {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (compatible; FaviconFetcher/1.0)',
        CURLOPT_SSL_VERIFYPEER => true,
    ]);
    $html = curl_exec($ch);
    curl_close($ch);
    return $html;
}
?>

Limita siempre las redirecciones y los timeouts. Una petición cURL descontrolada de 30 segundos es la forma más rápida de saturar tu pool de workers de PHP-FPM.

Paso 2: Analizar el HTML en busca de etiquetas rel="icon"

La especificación de HTML define rel="icon" como la forma de declarar un favicon (consulta la referencia de relaciones de enlace del WHATWG). Los navegadores pueden elegir cualquier <link> que coincida, así que recorremos todas las ocurrencias y preferimos la primera que tenga un href utilizable.

<?php
function findIconUrl(string $baseUrl, string $html): ?string {
    libxml_use_internal_errors(true);
    $dom = new DOMDocument();
    $dom->loadHTML($html);
    libxml_clear_errors();

    $xpath = new DOMXPath($dom);
    $links = $xpath->query('//link[@rel="icon" or @rel="shortcut icon"]');

    foreach ($links as $link) {
        $href = $link->getAttribute('href');
        if ($href === '') continue;
        return resolveUrl($baseUrl, $href);
    }
    return null;
}
?>

Fíjate que incluimos también shortcut icon: es una reliquia de Microsoft IE, pero sorprendentemente muchos sitios legacy todavía declaran su favicon de esa forma.

Paso 3: Resolver URLs relativas

Los href del favicon suelen ser rutas relativas como /static/icon.png o icons/favicon.svg. Usa parse_url junto con un pequeño helper para convertirlas en URLs absolutas antes de descargarlas.

<?php
function resolveUrl(string $base, string $ref): string {
    if (parse_url($ref, PHP_URL_SCHEME) !== null) {
        return $ref; // ya es absoluta
    }
    $parts = parse_url($base);
    $origin = ($parts['scheme'] ?? 'https') . '://' . ($parts['host'] ?? '');
    if (str_starts_with($ref, '/')) {
        return $origin . $ref;
    }
    $path = $parts['path'] ?? '/';
    $dir = substr($path, 0, strrpos($path, '/') + 1);
    return $origin . $dir . $ref;
}
?>

Paso 4: Recurrir a /favicon.ico como fallback

Si el HTML no contiene etiquetas <link rel="icon">, todos los navegadores siguen solicitando /favicon.ico automáticamente. Accede a ese endpoint como fallback para no devolver iconos en blanco en sitios antiguos.

<?php
function fallbackIconUrl(string $pageUrl): string {
    $parts = parse_url($pageUrl);
    return ($parts['scheme'] ?? 'https') . '://' . ($parts['host'] ?? '') . '/favicon.ico';
}
?>

Paso 5: Descargar y validar el binario

Ahora descarga el icono, verifica que realmente devolvió una imagen y guárdalo en disco. Vuelve a solicitarlo siempre con cURL en lugar de reutilizar la respuesta HTML: la URL del favicon puede estar en un dominio o CDN diferente con reglas de caché distintas.

<?php
function downloadFavicon(string $iconUrl, string $savePath): bool {
    $ch = curl_init($iconUrl);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (compatible; FaviconFetcher/1.0)',
        CURLOPT_SSL_VERIFYPEER => true,
    ]);
    $data = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?? '';
    curl_close($ch);

    if ($data === false || $code !== 200 || $data === '') {
        return false;
    }
    // Reject obvious non-image responses (HTML error pages, JSON, etc.)
    if (stripos($type, 'image/') !== 0 && stripos($type, 'application/octet-stream') !== 0) {
        return false;
    }
    return file_put_contents($savePath, $data) !== false;
}
?>

Ensamblando el pipeline de PHP para obtener el favicon desde una URL

Este es el flujo completo: obtén el HTML, parsea los link tags, recurre al favicon de la raíz y guárdalo:

<?php
function getFaviconFromUrl(string $pageUrl, string $savePath): bool {
    $html = fetchHtml($pageUrl);
    if ($html === false) return false;

    $iconUrl = findIconUrl($pageUrl, $html) ?? fallbackIconUrl($pageUrl);
    return downloadFavicon($iconUrl, $savePath);
}
?>

Lo invocas así:

<?php
$ok = getFaviconFromUrl('https://example.com', __DIR__ . '/example.ico');
echo $ok ? 'Saved.' : 'Failed.';
?>

Errores habituales

Cuándo pasar a una herramienta dedicada

Una función PHP hecha a mano es suficiente para unos pocos miles de URLs. Cuando necesitas salida multi-formato (ICO, PNG, SVG, Apple Touch), soporte de modo oscuro, iconos maskable para manifest o cobertura fiable de sitios con estructuras de <link> peculiares, el cableado de cURL se convierte en una carga de mantenimiento. Una herramienta como Mzu favicondl se encarga del parsing, el fallback, la normalización y el empaquetado en cada navegador y dispositivo, para que puedas olvidarte del boilerplate y centrarte en tu producto.

También puedes comparar este enfoque manual con los populares endpoints públicos de API de favicon para ver qué trade-off encaja mejor con tu stack: el fetching manual te da control total, mientras que las APIs ahorran tiempo a costa de una dependencia externa.