部署到 Firebase Hosting 后,首页打开正常,但访问 /about 或刷新 /blog/some-post 就显示 404。这是 Firebase Hosting 最常见的坑之一,原因几乎从来不在 Firebase 本身,而在 firebase.json 的 rewrites 没配或配错了。
最快的修复(单页应用 SPA): 在 firebase.json 里加一条把所有请求兜底到 /index.html 的 catch-all rewrite,然后重新部署。仅此一步就能修掉大约 80% 的子页面 404。完整配置见下面的 SPA 一节。
先 5 秒判断你属于哪种情况
| 症状 | 最可能的原因 | 对应章节 |
|---|---|---|
| 首页 OK,刷新子页面就 404 | 缺 SPA fallback rewrite | SPA |
| 点链接能进子页,但刷新或直接访问就 404 | SSG 漏了该静态文件,或 trailing slash 不一致 | SSG |
某个子路径下全部 404(例如 /en/ 全挂) | 该子路径没有 index.html | 子路径 |
| 第一次部署就全部 404 | public 目录写错了 | 目录写错 |
为什么这么判断:Firebase Hosting 优先返回精确匹配的静态文件,只有当没有任何文件或目录匹配请求路径时,才会落到你的 rewrites(见 Hosting 配置文档)。所以出现 404 就意味着”既没有匹配的文件,也没有 rewrite 接住它”。
SPA:必须 rewrite 到 /index.html
Vue / React / Vite / SvelteKit-SPA 这类应用的路由由前端处理(React Router、Vue Router 等)。直接访问 /about 时,Firebase 去找 /about 或 /about/index.html 这个文件,找不到就返回 404,前端路由根本没机会运行。
修复方法 —— 加一条兜底到 index.html 的 catch-all rewrite:
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
source: "**" 会匹配任何无法对应到真实文件或目录的请求,并改为返回 index.html,让前端路由接管。
注意
"public"必须是你 build 后的实际输出目录:Vite / Vue / React(Vite)通常是dist;Create React App 是build;Angular CLI 是dist/<项目名>(要把public指向里面那个含index.html的子目录)。
多站点项目: 如果 firebase.json 用的是 hosting 配置数组,rewrites 必须写在每个站点对象内部,而不是顶层:
{
"hosting": [
{ "target": "app", "public": "dist", "rewrites": [{ "source": "**", "destination": "/index.html" }] },
{ "target": "docs", "public": "docs/dist", "rewrites": [{ "source": "**", "destination": "/index.html" }] }
]
}
SSG(Astro / Next.js static export / Hugo)
SSG 会为每个页面生成一个 .html 文件,例如 /about/index.html。一般不需要 SPA fallback —— 每条路由都是真实存在的文件。这里的坑在于文件的”形状”,而不是 rewrite。
坑 1:Astro 的 trailing slash 不一致
Astro 默认输出 /about/index.html。访问 /about(不带斜杠)时有时会 404。Firebase 通常会做一次”补斜杠”的 301,但有些边缘情况会失败。
修复 —— 在 astro.config.mjs 中:
export default defineConfig({
trailingSlash: 'always',
build: { format: 'directory' }
});
是 format: 'directory' 这一项保证了 subdir/index.html 的目录结构。这要和 firebase.json 保持一致(见坑 4)。
坑 2:Next.js static export 路径错位
独立的 next export 命令已在 Next.js 14 中移除(运行会看到 "next export" has been removed in favor of "output: export")。截至 2026 年 6 月,正确做法是在配置里设置输出模式,然后跑普通的 next build。如果不加 trailingSlash: true,Next 会生成 out/about.html 而不是 out/about/index.html,于是 Firebase 把 /about 当目录去找 index,找不到就 404。
修复 —— 在 next.config.js:
module.exports = {
output: 'export',
trailingSlash: true,
};
然后跑 next build(不再有单独的 export 步骤)。静态文件默认落在 out/ 目录 —— 把 firebase.json 的 "public" 指向 out。
需要真正的 SSR(用到了
getServerSideProps、route handler 或 middleware)?静态导出做不到,见下面 FAQ 里的 SSR 部分。
坑 3:动态路由没生成
Next.js 的 [slug].tsx 或 Astro 的 [slug].astro 必须在 build 时枚举出所有路径,否则那些 URL 在 SSG 输出里根本不存在,访问就 404。
修复:
- Astro:导出
getStaticPaths(),返回所有 slug。 - Next.js(App Router 静态导出):导出
generateStaticParams()。 - Next.js(Pages Router 静态导出):导出
getStaticPaths并设fallback: false,返回所有paths。
坑 4:firebase.json 和 build 互相矛盾
即使 SSG 输出正确,firebase.json 配错也会再次破坏路由:
{
"hosting": {
"public": "out",
"cleanUrls": true,
"trailingSlash": true
}
}
选定一种 trailing slash 策略,让框架和 firebase.json 一致。比如 Astro 用 trailingSlash: 'never' 而 Firebase 用 trailingSlash: true,就会产生重定向死循环或 404。
静态多页站(每个页面一个 HTML)
最简单的场景,通常不需要 rewrites:
{
"hosting": {
"public": "site",
"cleanUrls": true,
"trailingSlash": true
}
}
cleanUrls: true 让 /about.html 可以用 /about 访问;trailingSlash: true 强制统一,避免 /about 和 /about/ 行为不一致。
子路径 / 多语言部署
要让 /zh/ 和 /en/ 都正常打开:
public/zh/index.html和public/en/index.html必须都实际存在。- 如果每种语言各自是一个 SPA,要为每个子路径加一条 fallback,并放在全局 catch-all 之前(顺序很重要 —— Firebase 应用第一条匹配的规则):
"rewrites": [
{ "source": "/zh/**", "destination": "/zh/index.html" },
{ "source": "/en/**", "destination": "/en/index.html" },
{ "source": "**", "destination": "/index.html" }
]
部署目录写错了(低级但常见)
firebase.json写了"public": "public",但你的框架输出在dist/或out/。- 部署后 Firebase 只看到
public/里的 placeholder 内容,于是所有真实页面都 404。
修复: 把 "public" 改成你真正的 build 输出目录,再重新部署。
部署前自检清单
按顺序检查:
firebase.json的"public"指向真实的 build 输出目录。- 该目录里确实有
index.html以及各个子页面文件(打开看一眼)。 - SPA 项目:存在兜底到
/index.html的 catch-allrewrites。 - SSG 项目:框架配置和
firebase.json的 trailing slash 策略完全一致。 - 多语言子路径:每个子路径都有自己的
index.html。 - 跑完
firebase deploy --only hosting后,“X files deployed” 的数量是否合理(期望几十个文件却只显示 1、2 个就说明有问题)。
怎么确认已经修好
重新部署后,要在干净状态下测试 —— Firebase 会在 CDN 上缓存 HTML,所以用隐身窗口或 curl,而不是普通刷新:
# 应输出:HTTP/2 200
curl -I https://YOUR-SITE.web.app/about/
# 也测一个更深 / 需要刷新的路由
curl -I https://YOUR-SITE.web.app/blog/some-post/
直接访问非首页路由返回 200,说明 rewrite 或静态文件已经能正确解析。确认重新部署过之后仍然 404,几乎一定是 public 目录还是错的,或者缺了某个静态文件(再打开 build 目录核对一遍)。
最短修复路径
按命中率排序:
- 加 SPA fallback rewrite —— 修掉约 80% 的子页面 404。
- 打开 build 目录确认文件存在 —— 修掉剩下约 15%。
- 核对
public字段指向真实输出目录 —— 修掉最低级也最致命的 5%。
什么时候不是你的问题
- Firebase Hosting 故障 —— 查 Firebase 状态面板。
- CDN 缓存延迟 —— 刚部署完的几分钟内,旧的 404 响应可能还在缓存里,用隐身窗口测。
- 自定义域名 DNS 还没生效(添加域名后可能要 24-48 小时)。
如果 404 来自 rewrite 到函数的路径,而不是静态路由,那是另一层问题 —— 排查规则顺序和静态文件遮挡看rewrite 没触发,排查 firebase.json 与已部署函数的区域 / 名称不匹配看Firebase function not found。
容易误判的情况
- 以为 SSG 不需要 rewrite —— 大部分情况是的,但 trailing slash 不一致照样 404。
- 以为部署成功就万事大吉 —— 绿色部署只代表文件上传了,不代表路由对。
- 以为 emulator 跑通线上就跑通 ——
firebase emulators:start --only hosting很接近但不等同于真实 Hosting,一定要再测一遍线上 URL。 - 以为缓存没问题 —— Firebase 默认缓存 HTML,强刷不一定绕过;用隐身窗口测。
预防建议
- 有风险的改动,先部署到预览通道:
firebase hosting:channel:deploy preview。它会给你一个临时 URL,可以在不动生产的情况下先测。 - 改动
firebase.json后,部署前先本地跑firebase emulators:start --only hosting。(老的firebase serve --only hosting仍然可用,但 emulator suite 是目前推荐的工具。) - 不要把 build 输出目录命名为
public/—— 它会跟 Firebase 默认目录混淆,把错误藏起来。 - CI 里加一步:部署后用
curl -I检查几个关键的非首页路径返回 200。
常见问题(FAQ)
Q:Astro 站点部署到 Firebase 一直 404 怎么办?
A:大多是 astro.config.mjs 和 firebase.json 的 trailing slash 不一致。把 Astro 设成 trailingSlash: 'always' 加 build: { format: 'directory' },firebase.json 设成 "trailingSlash": true,重新 build 再部署。
Q:Next.js 一定要静态导出才能用 Firebase Hosting 吗?
A:纯 Firebase Hosting 只提供静态文件,所以在它上面跑必须做静态导出(output: 'export')。如果要真正的 SSR(getServerSideProps、middleware、route handler),用 Firebase App Hosting —— 它现在是 Firebase 官方推荐的运行服务端渲染 Next.js / Angular 的方式。截至 2026 年 6 月,老的 “web frameworks” Hosting 实验已不再接受新用户,现有用户被引导迁移到 App Hosting。传统的 Cloud Functions / Cloud Run 方案仍然可用,但更手动。
Q:rewrite 和 redirect 有什么区别? A:rewrite 保持 URL 不变,内部去拿另一个文件(浏览器地址栏不变);redirect 用 3xx 状态码让浏览器跳到新 URL。SPA fallback 用的是 rewrite,不是 redirect。
Q:部署后看到的还是旧页面。
A:浏览器缓存或 Firebase 的 CDN 缓存。用隐身窗口、强刷(Cmd+Shift+R / Ctrl+Shift+R),或在 URL 后加 ?v=1 绕过缓存。
Q:同一个 Firebase 项目里有多个 hosting site 怎么部署?
A:在 firebase.json 的 hosting 下用数组(每个 site 一项,各自有自己的 target、public 和 rewrites)。单独部署某个站用 firebase deploy --only hosting:siteId。
Q:为什么 /about 能开但 /about/ 404(或反过来)?
A:这是 trailing slash 分裂。你的框架生成的是一种形状,而 firebase.json 期望的是另一种。两边的 trailingSlash 对齐后重新部署即可。