You've built a bookmark manager, a link directory, or a dashboard that displays other websites. You want each site's favicon to make the list look polished — but blindly hitting /favicon.ico for every URL is fragile. Many sites ship SVG icons, use custom paths, or rely on conditional <link> tags instead of the default root file.

This tutorial walks through a PHP get favicon from URL workflow: parse the HTML for <link rel="icon"> declarations, fall back to the legacy /favicon.ico endpoint, validate the response, and save the binary locally.

Prerequisites

You need PHP 7.4 or newer with two extensions enabled:

Both ship with the official PHP Docker image and most Linux distributions. Run php -m to confirm they're listed before you start coding.

Step 1: Fetch the Page HTML

Start with a basic cURL request. The User-Agent header matters more than it looks — some sites return a stripped-down HTML to non-browser clients, which can omit favicon <link> tags entirely.

<?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;
}
?>

Always cap redirects and timeouts. A runaway 30-second cURL request is a fast way to fill your PHP-FPM worker pool.

Step 2: Parse the HTML for rel="icon" Tags

The HTML spec defines rel="icon" as the way to declare a favicon (see the WHATWG link relation reference). Browsers may pick any matching <link>, so we scan every occurrence and prefer the first one with a usable href.

<?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;
}
?>

Notice we include shortcut icon as well — it's an old Microsoft IE relic, but a surprising number of legacy sites still declare their favicon that way.

Step 3: Resolve Relative URLs

Favicon hrefs are often relative paths like /static/icon.png or icons/favicon.svg. Use parse_url plus a small helper to turn them into absolute URLs before downloading.

<?php
function resolveUrl(string $base, string $ref): string {
    if (parse_url($ref, PHP_URL_SCHEME) !== null) {
        return $ref; // already absolute
    }
    $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;
}
?>

Step 4: Fall Back to /favicon.ico

If the HTML contains no <link rel="icon"> tags, every browser still requests /favicon.ico automatically. Hit that endpoint as a fallback so you don't return blank icons for old-school sites.

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

Step 5: Download and Validate the Binary

Now download the icon, verify it actually returned an image, and save it to disk. Always re-fetch with cURL rather than reusing the HTML response — the favicon URL may live on a different domain or CDN with different caching rules.

<?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;
}
?>

Putting the PHP Get Favicon from URL Pipeline Together

Here's the full flow — fetch the HTML, parse the link tags, fall back to the root icon, and save:

<?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);
}
?>

Call it like this:

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

Common Pitfalls

When to Switch to a Dedicated Tool

A hand-rolled PHP function is fine for a few thousand URLs. Once you need multi-format output (ICO, PNG, SVG, Apple Touch), dark-mode awareness, maskable manifest icons, or reliable coverage of sites with quirky <link> structures, the cURL plumbing becomes a maintenance burden. A tool like Mzu favicondl handles the parsing, fallback, normalization, and packaging across every browser and device, so you can drop the boilerplate and focus on your product.

You can also compare this manual approach with the popular public favicon API endpoints to see which trade-off fits your stack better — manual fetching gives you full control, while APIs save time at the cost of an external dependency.