favicon.svgをViteのpublicフォルダに入れて保存したのに、ブラウザのタブには白い四角が表示されたまま——そんな経験はありませんか。ビルドは成功し、ファイルもその場に存在するにもかかわらず、ブラウザはそれを見つけられません。これは、開発者がViteのpublicディレクトリを単なる静的フォルダのように扱い、Viteがそのファイルを配信する仕組みに関する重要なルールを見落としたときに起こります。

publicフォルダは、安定してハッシュ化されないURLを必要とするアセットにとってのViteの抜け道です。faviconはまさにその典型例であり、Vite初心者が最もつまずきやすいポイントでもあります。

Viteのpublicフォルダにfaviconを置くべき理由

Viteはキャッシュバスティングのためにsrc/内のimportされたアセットをハッシュ化します(assets/logo-a3f9c2.svgのような名前になります)。JSやCSSには効果的ですが、faviconには向いていません。ブラウザはfaviconを積極的にキャッシュしますし、検索エンジンは検索結果にアイコンを表示するために予測可能なURLを必要とします。

public/内のファイルはバンドラーを完全に経由しません。Viteの公式ドキュメントによれば、このディレクトリ内のファイルはすべてビルド出力のルートへそのままコピーされ、ルートパスで配信されます。つまりpublic/favicon.svgは、/public/favicon.svgではなく/favicon.svgとして配信されます。ファイル名の先頭にスラッシュを付けただけです。

前提条件

ステップ1:faviconファイルをpublic/に配置する

アイコンファイルをプロジェクトルートのpublic/ディレクトリに直接配置します。ほとんどのViteのスキャフォールドでは、このフォルダは自動で生成されます。

my-vite-app/
├── public/
│   ├── favicon.svg
│   ├── favicon.ico
│   └── apple-touch-icon.png
├── src/
├── index.html
└── package.json

シンプルで慣用的なファイル名を使用してください。ブラウザは追加の設定なしで、ルートにあるfavicon.svg、favicon.ico、apple-touch-icon.pngを見つけます。

ステップ2:ルート相対パスでファイルを参照する

プロジェクトルートのindex.htmlを開いてください(public/index.htmlではありません — そのファイルはViteには存在しません)。<head>内に、/で始まる絶対パスを使ったlinkタグを追加します。publicフォルダは意図的にimportリゾルバーをスキップするため、Viteプロジェクトにおいてアセットパスをハードコードで記述するのはこの場面だけです。

<head>
  <link rel='icon' type='image/svg+xml' href='/favicon.svg'>
  <link rel='icon' type='image/x-icon' href='/favicon.ico'>
  <link rel='apple-touch-icon' href='/apple-touch-icon.png'>
</head>

HTML仕様では、rel='icon'がfaviconを宣言する標準的な方法として定義されています(WHATWG HTML Living Standard)。ブラウザはtypeとsizes属性を基に最適なものを選択します。

ステップ3:ダークモード用バリアントを追加する

最新のブラウザでは、ダークテーマ使用時に自動的に切り替わる2つ目のSVGがサポートされています。media属性付きのlinkタグを追加することで、ユーザーのシステムがダークモードに切り替わったときに適切なアイコンが表示されるようになります。

<link rel='icon' type='image/svg+xml' href='/favicon.svg' media='(prefers-color-scheme: light)'>
<link rel='icon' type='image/svg+xml' href='/favicon-dark.svg' media='(prefers-color-scheme: dark)'>

ダークモード用SVG faviconについて詳しくは、faviconのダークモードガイドをご覧ください。

ステップ4:ビルド出力を確認する

npm run buildを実行し、dist/を確認します。faviconファイルがルートに、バンドラーの手を介さずそのまま出力されているはずです。

dist/
├── assets/
├── favicon.svg
├── favicon.ico
└── index.html

npm run previewを起動してDevToolsのNetworkタブを開けば確認できます。favicon.svg、favicon.ico、apple-touch-icon.pngへのリクエストが404ではなく200を返しているはずです。

よくある落とし穴

Viteのfaviconに関するバグの多くは、以下のいずれかのミスが原因です。タブが空白だったり、汎用アイコンが表示されたりしている場合は、まずこのリストを確認してください。

publicフォルダだけでは不十分なケース

publicフォルダは、静的で変動しないfaviconにとって最適な置き場所です。ブラウザが安定してキャッシュでき、検索エンジンがインデックス可能な安定したURLが得られます。後から動的なfavicon(通知バッジ、ユーザーアバター、ライブステータス表示など)が必要になった場合は、実行時にhref属性を切り替えるJavaScriptが必要になります。動的faviconのJavaScriptに関するガイドで、そのパターンを詳しく解説しています。

まずは適切なアイコンファイルを生成したいとお考えですか? Mzu favicondlは、1枚のソース画像からSVG、PNG、ICO、Apple Touchの各バリアントを生成します。それらをpublic/に配置し、linkタグを追加すれば、数分でViteアプリにプロフェッショナルなタブ表示を実装できます。