新しい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を直接開いてテストし、問題がないことを確認したうえで参照してください。

予防チェックリスト

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に貼り付ければ、そのまま公開できます。