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:
curl— para obtener páginas remotas y seguir redireccionesdom— para analizar el HTML y extraer las etiquetas<link>
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
- Páginas de error HTML haciéndose pasar por iconos. Un servidor mal configurado puede devolver un estado 200 con un cuerpo HTML cuando falta el favicon. Comprueba siempre la cabecera content-type y descarta cualquier cosa que no empiece por
image/. - Límites de tasa del CDN. Si realizas muchas peticiones a un CDN de terceros desde tu servidor, pueden limitarte o banearte la IP. Guarda en disco las descargas exitosas y respeta un intervalo de fetch educado (unos pocos segundos por host suele ser seguro).
- Verificación SSL deshabilitada. Poner
CURLOPT_SSL_VERIFYPEERafalsees tentador durante la depuración, pero expone a tus usuarios a ataques MITM. Mantenlo activado en producción. - Casos límite de codificación. DOMDocument recurre a ISO-8859-1 cuando el HTML carece de una meta tag de charset, lo que puede estropear los valores de href en sitios UTF-8. Para evitarlo, antepón un bloque
<meta charset='UTF-8'>o cambia a un parser con soporte UTF-8 comoHTML5DomDocument. - La política de mismo origen no se aplica en el lado del servidor. PHP puede obtener cualquier favicon público sin comprobaciones CORS: esas reglas solo aplican al JavaScript de tu front-end.
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.