Vue 3 のプロジェクトを Vite で新規作成して、開発 server を立ち上げ、ブラウザのタブを見てみます。ブランドアイコンを置くべき場所に、デフォルトの Vite ロゴが陣取っているはずです。修正はプロジェクトの public/ フォルダで行います。Vite 特有の落とし穴さえ押さえれば、設定は 3 分程度で完了します。

前提条件

必要になるのは、Vite で動作している Vue 3 プロジェクトと、追加したい favicon ファイル一式だけです。まだ用意していない場合は、Mzu favicondl を使って ICO、PNG、SVG を含む完全なバンドルを生成し、そのまま使える状態でダウンロードしてください。Webpack 特有の挙動や copy-webpack-plugin のルールは一切不要です。Vite は public フォルダを static な asset として扱い、設定なしで処理します。

ステップ 1:public/ フォルダを開く

Vue + Vite のスキャフォールドには、プロジェクトルートに必ず public/ ディレクトリが用意されています。Vite の public ディレクトリに関するドキュメントによれば、ここに置いたファイルは変更されずにビルド出力へコピーされ、ルートパスから配信されます。

つまり public/favicon.ico は /favicon.ico としてアクセスでき、/public/favicon.ico ではありません。この違いを見落とすことが、Vue アプリで本番環境のタブアイコンが壊れる最もよくある原因です。

ステップ 2:favicon ファイルを配置する

favicon 関連ファイル一式を public/ 直下に置いてください。

public/
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png
├── site.webmanifest
└── favicon.svg

Vite はフォルダごと dist/ 出力へそのままコピーします。import や変換は一切かかりません。これらのファイルを Vue コンポーネントから import しないでください。これらはバンドルの外側に置くことを前提としています。現在意味のあるサイズについては、Favicon Sizes Guide で詳しく解説しています。

ステップ 3:index.html に link タグを追加する

Vite が Vue アプリをマウントする index.html をプロジェクトルートで開きます。<head> の中に link rel="icon" の宣言を追加してください。

&lt;head&gt;
  &lt;meta charset="UTF-8" /&gt;
  &lt;link rel="icon" href="/favicon.svg" type="image/svg+xml" /&gt;
  &lt;link rel="icon" href="/favicon-32x32.png" type="image/png" sizes="32x32" /&gt;
  &lt;link rel="icon" href="/favicon.ico" sizes="any" /&gt;
  &lt;link rel="apple-touch-icon" href="/apple-touch-icon.png" /&gt;
  &lt;link rel="manifest" href="/site.webmanifest" /&gt;
&lt;/head&gt;

記述の順序は重要です。ブラウザはサポートしている最初の rel="icon" エントリを採用するため、まず SVG を置き、続いて PNG のフォールバック、最後に ICO という構成にします。すべてのパスは /public/ ではなく / から始めてください。

ステップ 4:ダークモード対応の SVG を追加する

ダークモード時にアイコンを切り替えたい場合は、Vue のリアクティビティを使うのではなく、SVG 自体にメディアクエリを記述してください。

&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"&gt;
  &lt;style&gt;
    circle { fill: #1a1a1a; }
    @media (prefers-color-scheme: dark) {
      circle { fill: #fafafa; }
    }
  &lt;/style&gt;
  &lt;circle cx="16" cy="16" r="14" /&gt;
&lt;/svg&gt;

ブラウザが自動的に fill を切り替えるため、タブ表示で watch() を呼んだりライフサイクルフックを使う必要はありません。より詳しいパターンについては、Favicon Dark Mode Guide を参照してください。

ステップ 5:開発環境と本番環境で検証する

npm run dev を実行し、以下の 3 点を確認します。

  1. DevTools → Network で "favicon" でフィルタし、すべてのファイルが 200 OK を返すこと。
  2. ブラウザのタブにデフォルトの Vite ロゴではなく、自分のアイコンが表示されていること。
  3. curl http://localhost:5173/favicon.svg が SVG のバイト列を返すこと。

続いて npm run build を実行し、dist/ を確認します。public/ に置いたすべてのファイルが、変更されず dist/ のルートにそのまま出力されているはずです。本番環境で何かが欠けている場合は、ホスティングのリライトルールが原因の可能性が高いので、vercel.json や _redirects を確認してください。

よくある落とし穴

まとめ

Vue + Vite の public フォルダを使ったワークフローは、モダンな Web 開発の中でも特にクリーンな favicon 設定の一つです。ファイルを配置し、index.html に link タグを記述すれば、Vite がすべてをそのまま dist/ にコピーします。プラグインの設定も、head への注入の挙動も、Vue Router によるリクエストの割り込みも一切なく、ルートパスから配信される static な asset として扱われます。

Mzu favicondl で favicon パッケージを生成し、index.html に link タグを貼り付ければ、あらゆるブラウザタブで Vue アプリがきちんと見えるようになります。