Firebase Hosting で Vue SPA + ランディングページを共存させる構成
導入:SPAとランディングの共存が必要な理由
Firebase Hosting 上で、Vue製SPAとAdSense審査用のランディングページを共存させたい場面って多いよね。SPAがログイン必須だったりすると、ルート直下にそのまま置くと審査が通らない問題もある。
最初ってどのファイルに広告タグ入れたらいいの?
その場合、SPAを /app/ に配置して、ルート / には誰でも見られるランディングHTMLを置く構成にすればOK。Firebase Hosting の rewrite 機能を活用するのがポイント。
Firebase の rewrite と Vite のビルド出力をうまく合わせるのがコツだぞ。
ディレクトリ構成とビルド戦略
Vite の出力を dist/app に固定。ルート配信用のHTML(landing.html、privacy.htmlなど)は public/ に置いて、ビルド後に dist/ にコピーする。package.json の postbuild スクリプトでそれを自動化する。
ビルドごとに手でコピーするの面倒だった!
この構成なら npm run build 一発で、
dist/app/に SPAdist/にランディングや静的HTML
が自動でそろう。
コピー処理は Nodeスクリプトでやってる。import/export形式(type: module)ならESMで書こう。
firebase.json の設定(rewrite)
以下が最小で鉄壁な設定:
{
"hosting": {
"public": "frontend/dist",
"rewrites": [
{ "source": "/", "destination": "/landing.html" },
{ "source": "/about", "destination": "/about.html" },
{ "source": "/privacy", "destination": "/privacy.html" },
{ "source": "/terms", "destination": "/terms.html" },
{ "source": "/contact", "destination": "/contact.html" },
{ "source": "/app", "destination": "/app/index.html" },
{ "source": "/app/**", "destination": "/app/index.html" },
{ "source": "**", "destination": "/landing.html" }
]
}
}
/app にアクセスしたら404になるのってこのルールで直るの?
そう。"/app" と "/app/**" の両方をカバーしておくのが重要。リダイレクトではなくリライトでSPAのエントリに飛ばす。
忘れずにRouterの createWebHistory(‘/app/’) にスラを付けよう。
よくあるトラブルと対処
症状:/ で勝手に /app に遷移
→ それ、landing.html でSPA資産を読み込んでるか、古いService Workerが働いてる。
scriptやlinkで/app/assets読んでたらアウトってこと?
そう。ランディングHTMLは完全に独立した静的HTMLにして、<script type="module" src="/app/assets/..."> みたいな行は絶対に入れない。
キャッシュ残ってる時は DevTools → Application → Service Workers → Unregister で一度切るといい。
ビルドスクリプトの設定(frontend/package.json)
postbuild で public/ → dist/ コピー:
{
"scripts": {
"build": "vue-tsc -b && vite build",
"postbuild": "node ./scripts/copy-public-to-dist.js"
},
"type": "module"
}
ViteのpublicDirはfalseにするんだよね?
正解!publicDir: false にしないと、Viteが dist/app にコピーしちゃってズレる。
構成が安定すれば、あとは deploy 前に dist の中身を確認するだけ。
まとめ
Firebase Hosting で SPA + ランディング構成を安全に共存させるには:
- Vite: base=/app/, outDir=dist/app, publicDir=false
- ルート用HTMLは public に置き、postbuild で dist にコピー
- firebase.json は /app/** → /app/index.html でSPAフォールバック
- Router: createWebHistory(‘/app/’)(末尾スラ付き)
- landing.html ではSPA資産を読み込まない!
これで AdSense の審査も通るし、PWAとしても快適に使える構成になるぞ!
この構成、もうゴリ押しで鉄板だ。未来の自分にもこのまま残せ!


コメント