Firebase Hosting 子页面 404 修复:SPA / SSG / 子路径全场景

首页正常,但访问 /about 或刷新 /blog/post 就 404?几乎都是 firebase.json 的 rewrites 问题。本文给出 SPA、SSG、Astro、Next.js 的修复方案(2026 年 6 月已核实)。

部署到 Firebase Hosting 后,首页打开正常,但访问 /about 或刷新 /blog/some-post 就显示 404。这是 Firebase Hosting 最常见的坑之一,原因几乎从来不在 Firebase 本身,而在 firebase.jsonrewrites 没配或配错了。

最快的修复(单页应用 SPA):firebase.json 里加一条把所有请求兜底到 /index.html 的 catch-all rewrite,然后重新部署。仅此一步就能修掉大约 80% 的子页面 404。完整配置见下面的 SPA 一节。

先 5 秒判断你属于哪种情况

症状最可能的原因对应章节
首页 OK,刷新子页面就 404缺 SPA fallback rewriteSPA
点链接能进子页,但刷新或直接访问就 404SSG 漏了该静态文件,或 trailing slash 不一致SSG
某个子路径下全部 404(例如 /en/ 全挂)该子路径没有 index.html子路径
第一次部署就全部 404public 目录写错了目录写错

为什么这么判断: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.htmlpublic/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 输出目录,再重新部署。

部署前自检清单

按顺序检查:

  1. firebase.json"public" 指向真实的 build 输出目录。
  2. 该目录里确实有 index.html 以及各个子页面文件(打开看一眼)。
  3. SPA 项目:存在兜底到 /index.html 的 catch-all rewrites
  4. SSG 项目:框架配置和 firebase.json 的 trailing slash 策略完全一致。
  5. 多语言子路径:每个子路径都有自己的 index.html
  6. 跑完 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 目录核对一遍)。

最短修复路径

按命中率排序:

  1. 加 SPA fallback rewrite —— 修掉约 80% 的子页面 404。
  2. 打开 build 目录确认文件存在 —— 修掉剩下约 15%。
  3. 核对 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.mjsfirebase.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.jsonhosting 下用数组(每个 site 一项,各自有自己的 targetpublicrewrites)。单独部署某个站用 firebase deploy --only hosting:siteId

Q:为什么 /about 能开但 /about/ 404(或反过来)? A:这是 trailing slash 分裂。你的框架生成的是一种形状,而 firebase.json 期望的是另一种。两边的 trailingSlash 对齐后重新部署即可。

相关问题

标签: #Firebase #部署 / 托管 #路由 404 #排查