ブックマークマネージャーやリンクディレクトリ、他のサイトを表示するダッシュボードを作ったことがある方も多いのではないでしょうか。リストの見栄えを良くするために各サイトの favicon を表示したいところですが、すべての URL に対して単純に /favicon.ico を取得しにいく方法には脆さがあります。多くのサイトは SVG アイコンを配信していたり、カスタムパスを使っていたりと、デフォルトのルートファイルではなく条件付きの <link> タグに依存しているためです。
このチュートリアルでは、PHP で URL から favicon を取得する一連のフローを順に解説します。HTML 内の <link rel="icon"> 宣言を解析し、レガシーな /favicon.ico エンドポイントへフォールバックし、レスポンスを検証した上でバイナリをローカルに保存します。
前提条件
PHP 7.4 以降と、以下の 2 つの拡張モジュールを有効化した環境が必要です。
curl— リモートページの取得とリダイレクトの追跡に使用dom— HTML の解析と<link>タグの抽出に使用
これらは公式の PHP Docker イメージとほとんどの Linux ディストリビューションに同梱されています。コーディングを始める前に php -m を実行して、両方が一覧に表示されていることを確認してください。
ステップ 1: ページの HTML を取得する
まずは基本的な cURL リクエストから始めます。User-Agent ヘッダは見た目以上に重要です。ブラウザ以外のクライアントに対しては HTML を簡素化して返すサイトもあり、その場合 favicon の <link> タグが完全に省略されてしまうことがあります。
<?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;
}
?>
リダイレクトとタイムアウトには必ず上限を設けてください。cURL リクエストが暴走して 30 秒も続けば、PHP-FPM のワーカープールをすぐに埋め尽くしてしまいます。
ステップ 2: rel="icon" タグを解析する
HTML 仕様では、favicon を宣言する方法として rel="icon" が定義されています(WHATWG のリンク関係リファレンス を参照)。ブラウザは一致する <link> のどれを選んでもよい仕様となっているため、ここではすべての出現箇所を走査し、利用可能な 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;
}
?>
shortcut icon も含めている点に注目してください。これは旧 Microsoft IE の名残ですが、意外にも多くのレガシーなサイトでこの形式で favicon が宣言されています。
Step 3: Resolve Relative URLs
favicon の href は、/static/icon.png や icons/favicon.svg のように相対パスであることが多いです。ダウンロードする前に parse_url と小さなヘルパー関数を使って、絶対 URL に変換しましょう。
<?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
HTML に <link rel="icon"> タグが含まれていない場合でも、すべてのブラウザは引き続き /favicon.ico を自動的にリクエストします。昔ながらのサイトで空のアイコンを返さないようにするため、このエンドポイントをフォールバックとして呼び出しておきましょう。
<?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
続いてアイコンをダウンロードし、実際に画像データが返されたかを確認した上で、ディスクに保存します。HTML のレスポンスを再利用せず、必ず cURL で再取得してください。favicon の URL は別のドメインや CDN 上に置かれていることがあり、キャッシュのルールが異なる場合があるためです。
<?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;
}
?>
PHPでURLからfaviconを取得する一連の流れをまとめる
全体の手順は次のとおりです。HTMLを取得し、linkタグを解析し、見つからなければルートのアイコンにフォールバックして保存します。
<?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);
}
?>
呼び出しは次のように行います。
<?php
$ok = getFaviconFromUrl('https://example.com', __DIR__ . '/example.ico');
echo $ok ? 'Saved.' : 'Failed.';
?>
よくある落とし穴
- アイコンになりすました HTML エラーページ。サーバーの設定ミスにより、favicon がない場合に HTML 本文で 200 ステータスが返されることがあります。常に content-type ヘッダーを確認し、
image/で始まらないものは破棄してください。 - CDN のレート制限。サーバーからサードパーティ CDN を繰り返し叩くと、IP がスロットルされたりブロックされたりする可能性があります。成功したダウンロードはディスクへキャッシュし、礼儀正しいフェッチ間隔を尊重してください(ホストごとに数秒程度が通常は安全です)。
- SSL 検証の無効化。デバッグ中に
CURLOPT_SSL_VERIFYPEERをfalseに設定したくなりますが、本番環境ではユーザーを MITM 攻撃にさらすことになります。本番では必ず ON のままにしてください。 - エンコーディングのエッジケース。DOMDocument は HTML に charset メタタグがない場合に ISO-8859-1 へフォールバックするため、UTF-8 サイトでは href 値が文字化けすることがあります。
<meta charset='UTF-8'>ブロックを先頭に付与するか、HTML5DomDocumentのような UTF-8 対応パーサへ切り替えてください。 - 同一オリジンポリシーはサーバーサイドには適用されません。PHP は CORS チェックなしで任意のパブリック favicon を取得できます。CORS のルールはフロントエンドの JavaScript にのみ適用されるものです。
専用ツールへ切り替えるべきタイミング
数千件の URL を扱う程度なら手書きの PHP 関数で十分です。しかし、マルチフォーマット対応(ICO、PNG、SVG、Apple Touch)、ダークモード対応、マスク対応のマニフェストアイコン、<link> 構造が一風変わったサイトへの確実な対応が必要になると、cURL まわりの配管処理は保守の負担になります。Mzu favicondl のようなツールであれば、あらゆるブラウザーとデバイス向けにパース・フォールバック・正規化・パッケージングをすべて処理してくれるため、定型処理を捨てて製品そのもの開発に集中できます。
また、この手作業のアプローチと、よく利用されている公開 favicon API エンドポイント を比較し、どちらのトレードオフがご自身のスタックに合っているか検討するのも良いでしょう。手動フェッチは完全な制御を得られる一方で、API は外部依存というコストを払う代わりに時間を節約できます。