本地跑得好好的。firebase deploy 一发,开线上 URL,礼貌地 404。Firebase Hosting 的 404 几乎都是五种原因之一,知道是哪种之后大约十分钟能修好。关键是别再瞎猜:本地复现一次、拿 firebase.json 对照 build 产物、用 curl 看响应,而不是看一个被缓存毒化的浏览器标签页。
一句话结论
- 先复现:
npm run build && firebase serve --only hosting,再curl -sI http://localhost:5000/about/。这里 404 说明是 build 问题;返回 200 说明是部署或配置问题。 - 最常见的两个原因:
firebase.json的public目录配错,以及cleanUrls/trailingSlash和 framework 真实产出的文件名对不上。 - 经典 Hosting 不会自动 SSR。需要服务端渲染的路由得 rewrite 到 Cloud Function 或 Cloud Run,或者把这部分迁到 Firebase App Hosting。
- 残留的
**兜底 rewrite 到/index.html会把每个 404 都伪装成首页 HTML。内容站要删掉它。 - Firebase Hosting 的 Spark 套餐免费、允许商用,所以这些修法都不需要升级付费。
Firebase 是怎么决定返回什么的
Firebase Hosting 基于开源引擎 Superstatic,每个请求按一套固定优先级处理。搞清楚这个顺序,就知道你的 404 到底卡在哪。根据官方配置文档,顺序是:
- 保留的
/__/*命名空间(Firebase 自己的路径)。 - 配置的
redirects,第一条匹配的规则生效。 public目录里精确匹配的静态文件。- 配置的
rewrites,第一条匹配的规则生效。 - 你的自定义
404.html(如果有)。 - Firebase 默认 404 页。
所以一个 404 意味着请求一路走到了第 6 步:没有静态文件匹配,也没有 rewrite 接住。下面每个修法都是为了让请求落到第 3 或第 4 步。
是哪种原因?
| 现象 | 最可能的原因 |
|---|---|
| 部分路由能开、部分 404 | trailingSlash / cleanUrls 不一致(原因 2) |
只有 / 能开、其它都 404 | public 目录配错(原因 1) |
| 只有该 SSR 的路由 404 | 缺 rewrite 到 Function / Cloud Run(原因 3) |
| 只有今天新加的路由 404 | build 没跑,或部署了旧产物(原因 4) |
/_astro/... 等带 hash 的资源 404 | build 产物里缺对应 hash 文件(原因 5) |
动手改之前先本地复现
这一步把每个 404 都切成「build 问题」或「平台问题」两类,省得你盲改配置:
npm run build
firebase serve --only hosting
# 另一个 shell:
curl -sI http://localhost:5000/about/ | head -3
# HTTP/1.1 404 ... ← build 输出问题(文件根本不在)
# HTTP/1.1 200 ... ← Firebase 配置 / 部署问题(文件在)
用 curl -sI(如果同时跑 Functions,就用 firebase emulators:start),这样浏览器缓存完全不掺和进来。一个在 curl 里复现不了的失败 URL,通常是缓存遗留,不是真的 404。
原因 1:public 指向了错误的目录
firebase init 默认把 public 设成 public/,但大多数 framework 输出到别处:Astro 和 Vite 输出到 dist/,Next.js 静态导出输出到 out/。public 配错,上面的第 3 步就永远匹配不上,除了碰巧的 /index.html,每条路由都 404。
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"cleanUrls": true,
"trailingSlash": true
}
}
确认这条路由的文件确实在那个目录里:
ls -la dist/about/
# index.html ← 有
# (空) ← framework 没生成该路由
find dist -name 'about*' -type f
# dist/about.html ← 配 cleanUrls: true
# dist/about/index.html ← 配 trailingSlash: true
原因 2:cleanUrls / trailingSlash 对不上
这个最隐蔽,会出现经典的「一半页面能开」现象。这两个设置是 301 跳转规则,不只是好看:
cleanUrls: true把已存在页面的.html后缀去掉,并把任何/about.html请求 301 跳到/about。截至 2026 年 6 月,它只对存在的页面生效;不存在的页面必须精确命中某条redirects规则。trailingSlash: true用 301 跳转补上末尾斜杠;trailingSlash: false则跳转去掉斜杠。
把设置和 framework 写到磁盘上的文件名对齐。四个合法组合:
framework 产出 firebase.json 线上 URL
about.html cleanUrls: true, trailingSlash: false → /about
about.html + /about/ 跳转 cleanUrls: true, trailingSlash: false → /about
about/index.html cleanUrls: false, trailingSlash: true → /about/
about/index.html cleanUrls: true, trailingSlash: true → /about/ (最干净)
Astro 的 build.format: 'directory'(默认)产出 about/index.html,所以要配 trailingSlash: true。一个值得点名的坑:trailingSlash: false 加 cleanUrls: false 这个组合曾经在 Superstatic 里触发无限跳转,没测过就别用这对组合。
原因 3:SSR 路由没有 rewrite
经典 Firebase Hosting 只服务静态文件,不跑你的服务端代码,所以任何该服务端渲染的路由,如果不 rewrite 到后端就会 404。截至 2026 年 6 月你有两条路。
保留经典 Hosting,加一条 rewrite 到 Cloud Function 或 Cloud Run:
{
"hosting": {
"rewrites": [
{ "source": "/api/**", "function": "api" },
{ "source": "/render/**", "run": { "serviceId": "ssr-render", "region": "us-central1" } }
]
}
}
rewrite 写了仍然 404 或 Function not found 的常见坑:
serviceId或region和已部署服务对不上。跑firebase functions:list(或去 Cloud Run 看),确认两个字段都精确一致。- 后端根本没部署。两个一起部署:
firebase deploy --only functions,hosting。Cloud Run 要先部署服务、再部署 Hosting 配置,这样 rewrite 才有活的目标可指。 - 对外公开的 Cloud Run 服务需要
--allow-unauthenticated,否则调用方拿到的是报错而不是你的页面。
或者迁到 Firebase App Hosting——它在 2025 年 4 月正式 GA,现在是 Google 为 Next.js、Angular 等 SSR framework 推荐的落脚点。注意:旧的 Hosting「web frameworks」实验已对新的 Next.js 用户关闭,所以新的 SSR 项目应该直接从 App Hosting 起步,而不是手写一堆 rewrite。
原因 4:你部署了旧产物
如果只有今天新加的路由 404,多半是 build 没跑,或者 CI 部署了旧的 dist/。确认线上 release 和你最新一次 build 对得上:
firebase hosting:releases:list
# release 2026-05-22 14:02 <hash> ← 时间戳对照你最近一次 build
curl -sI https://yourdomain.com/about/ | grep -i x-served-by
# 通常带 release hash,可与列表对照
修法几乎都是 CI 顺序问题:确保流水线先 npm run build 再 firebase deploy,并且部署的是 build 输出目录、不是源码。
原因 5:带 hash 的资源 404
/_astro/... 等内容 hash 文件 404,说明部署的 HTML 引用了一个上传包里没有的 hash,通常是部分部署,或者 CDN 缓存了旧的 index.html、它指向的文件名已被新 build 改掉。重新跑一次干净 build 加完整部署,测试时绕开缓存:
curl --header 'Cache-Control: no-cache' -sI https://yourdomain.com/_astro/<file>.js | head -3
那条藏住所有 404 的兜底 rewrite
留意这条遗留规则,SPA 模板里很常见:
{ "source": "**", "destination": "/index.html" }
它会静默地把每个没匹配上的路径都变成首页 HTML。技术上不算 404,但它会毁掉 SEO 和深链,因为每个 URL 都返回同一份壳。Astro 和大多数静态内容 framework 都不需要它。内容站要删掉;只有真正客户端路由的 SPA 才保留。
验证修好了
- 之前 404 的每个 URL 现在
curl -sI都返回 200。 - 故意访问一个不存在的 URL 仍然返回 404、不是 200,证明你的
404.html生效。 - Search Console URL Inspection 对受影响页面显示 “URL is on Google” 或至少 “Page fetched”。
常见问题
/ 能开、/about 404 为什么?
十有八九是 trailingSlash 或 cleanUrls。要么文件叫 about.html 但没开 cleanUrls,要么文件是 about/index.html 但 trailingSlash 为 false。把设置和 build 写到磁盘的文件名对齐就行。
该开 cleanUrls 吗?
大多数内容站建议开:URL 更干净,还会帮你把 .html 形式 301 跳转过去。只要内部链接和这个设置保持一致,别串成连环跳转就好。
我的路由本该 SSR,为什么 404?
经典 Firebase Hosting 不会自动渲染。你需要把该路径 rewrite 到 Cloud Function 或 Cloud Run,或者把应用放到 Firebase App Hosting。如果 firebase.json 里 rewrite 配了,响应仍然是 404 或「function not found」,几乎都是 serviceId/区域和已部署后端对不上(Firebase function not found)。
rewrite 看起来没问题,但同一个 URL 仍然走静态文件。 同路径有真实文件遮挡,因为精确匹配的静态内容(第 3 步)在 rewrite(第 4 步)之前评估。这条规则也可能排在更宽泛的规则下面。按rewrite 没触发排查。
dist/ 里能看到文件,线上还是 404?
多半 deploy 没把它上传上去。用 firebase hosting:releases:list 确认,把时间戳和你最近一次 build 对照;CI 可能跳过了 build,或者部署了错误的目录。
修这个需要 Blaze 套餐吗? 不需要。Firebase Hosting 的 Spark 套餐免费、允许商用,所以静态内容的 404 修法不花钱。只有加 Cloud Functions 或 Cloud Run 做 SSR 时才需要 Blaze。