新しいReact + Viteアプリを公開し、favicon.icoをpublic/に置いてdevサーバーを再起動したのに、ブラウザのタブにはViteの雷マークが表示され続ける——あるいは何も表示されない。こうした状況に遭遇したことはないでしょうか。これはReact Viteでfaviconが表示されないという典型的な問題であり、ほとんどの場合は特定の設定ミスが原因です。
本記事ではまず2分でできる対処法を説明し、その後に根本原因を一つずつ掘り下げていきます。
2分でできる対処法
プロジェクトルートのindex.htmlを開き、linkタグのパスが/public/ではなく/で始まる絶対パスになっているか確認してください。Viteでは、public/ディレクトリ内のファイルはビルド出力にそのままコピーされ、ルートパスで配信されます(Viteのpublicディレクトリに関するドキュメント参照)。したがって、public/favicon.icoにあるfaviconは/favicon.icoとして参照します。
<link rel='icon' type='image/x-icon' href='/favicon.ico' />
<link rel='icon' type='image/svg+xml' href='/icon.svg' />
<link rel='apple-touch-icon' href='/apple-touch-icon.png' />
ファイルを保存してください。Viteが起動していればHMRが自動で反映します。それでもアイコンが表示されない場合は、ハードリロード(Ctrl+Shift+RまたはCmd+Shift+R)を実施し、Networkタブでfavicon URLが200を返しているかを確認しましょう。
根本原因の詳細
1. ファイルがsrc/にあり、public/にない
これが最も多いミスです。src/内のファイルはViteのバンドラーを経由するため、モジュールとしてimportする必要があり、最終的にはハッシュ付きのファイル名になります。一方、public/内のファイルはルートパスでそのまま配信されます。src/assets/にfaviconを置いて/favicon.icoで参照しようとしても、そのパスには何も存在しません。public/に移動してください。
2. linkタグのパスが誤っている
Create React Appから移行してきた開発者の中には、href='%PUBLIC_URL%/favicon.ico'やhref='/public/favicon.ico'と書くケースが見られます。Viteには%PUBLIC_URL%という変数は存在せず、/public/はランタイムには存在しないディレクトリ名です。素直に/favicon.icoと指定してください。
3. baseパスの不一致
vite.config.jsでbase: '/my-app/'を設定している場合、すべてのassetパスにそのプレフィックスを含める必要があります。href='/favicon.ico'のままでは、https://example.com/my-app/配下でアプリが配信されると404になります。相対参照(href='./favicon.ico')にするか、すべてのパスを設定済みのbaseに合わせて修正してください。
4. キャッシュによる誤表示
ブラウザはficonを非常に積極的にキャッシュします。Chromeは通常のキャッシュクリアでは消えない独立したSQLiteデータベースにfaviconを保存しています(詳しくは当ブログのfaviconキャッシュクリアガイドを参照)。パスを修正しても、壊れた状態の古いアイコンが何時間も残り続けることがあります。開発中はクエリ文字列でバージョンを付与して、強制的に再リクエストさせると効果的です。
<link rel='icon' href='/favicon.ico?v=2' />
5. SPAのルーティングが/favicon.icoを横取りしている
React Routerでcatch-allルートを使用している場合、/favicon.icoへのリクエストがReactアプリ側で処理され、アイコンの代わりにHTMLが返されることがあります。Viteのdevサーバーはpublic/のファイルを優先するため、この問題は主に本番ビルドで発生します。デプロイ先のURLをcurlで叩き、Content-Typeヘッダーを確認して検証してください。
6. Reactコンポーネント内のlinkタグ
JSXコンポーネント内に配置された<link>タグは、必ずしも期待どおりに<head>へレンダリングされるわけではありません。Reactはこれらをホイストせず、コンポーネントがマウントされた時点で初めてDOMにアタッチされます。動的にアイコンを切り替える場合はreact-helmet-asyncを使用し、静的なfaviconタグはindex.html内に直接配置してください。
7. SVG faviconの構文エラー
ChromeはSVG faviconをrel='icon'のHTML仕様に厳密準拠でパースします。namespace宣言の欠落や、不正なviewBox属性、外部リソースへの参照があると、サイレントに失敗します。ブラウザタブでSVGを直接開いてテストし、問題がないことを確認したうえで参照してください。
予防チェックリスト
- faviconファイルは必ず
public/だけに置く —src/には置かない - 参照はルートから始まる絶対パス(
/favicon.ico)を使う <link rel='icon'>タグはすべてindex.html内に配置し、JSXの中には入れない- 開発中はキャッシュバスター用にクエリ文字列をバージョン付けする
curl -I https://yoursite.com/favicon.icoでデプロイ後のビルドを検証する- アイコンが壊れていると判断する前に、シークレットウィンドウでテストする
Viteのファイルベースのメタデータアプローチについて詳しくは、当ブログのVite publicフォルダfavicon完全ガイドを参照してください。React特有の問題(Helmet、動的アイコンなど)については、React faviconセットアップガイドがそれらのパターンをカバーしています。
まとめ
React Viteでfaviconが表示されないという問題は、ほとんどの場合、ファイルの配置場所、パス参照、キャッシュの3点に集約されます。この3つを押さえれば90%は解決します。残りの10%は通常、baseパスの不一致か、Reactコンポーネントがlinkタグを飲み込んでいるケースで、いずれもDevToolsにははっきり痕跡が残ります。
Mzu favicondlでアイコン一式を生成し、ファイルをpublic/に配置、linkタグをindex.htmlに貼り付ければ、そのまま公開できます。