npm run dev 一切正常,部署到 Vercel、Netlify 或 Cloudflare Pages 后访问 /about 直接返回 404,或者只有首页能打开、其它路由全军覆没——这是 Astro 部署里最常见的一类陷阱。几乎都不是 Astro 本身的 bug,而是 build.format、trailingSlash、output 三者中至少一个和宿主的默认行为不对齐。
最快的修复办法:先在本地跑 npm run build && npm run preview,打开一条非首页路由。如果本地也 404,问题在 astro.config.mjs——把 build.format 设成 'directory',trailingSlash 选一个值(大多数宿主用 'ignore' 最省事)。如果本地正常、只有线上 404,问题在宿主的 redirect / rewrite 层,而不是 Astro。
先判断你属于哪一类
改任何东西之前,先跑这一条命令——它能把所有情况一刀切成两类:
npm run build && npm run preview
# 打开 http://localhost:4321/about (任意非首页路由)
本地 preview 结果 | 线上结果 | bug 在哪 | 跳到 |
|---|---|---|---|
| 404 | 404 | Astro 配置(build.format / output / base) | 下文 Step 2、4、5 |
| 200 | 404 | 宿主的 redirect / rewrite / 缓存层 | 下文 Step 3、6 |
| 200 | 200 | 已经修好,或 CDN 缓存陈旧——清缓存 | 下文 Step 6 |
npm run preview serve 的是真实的 dist/ 产物,方式和大多数静态宿主一致,所以它比 npm run dev 更接近生产环境(dev 用的是 Vite 开发服务器,路由解析方式完全不同)。
常见原因
按命中率从高到低排。
1. build.format 和宿主的默认 trailing slash 不一致
Astro 的 build.format 决定每个页面输出 about/index.html(directory)还是 about.html(file)。Vercel 和 Netlify 对两种形式都能正常 serve /about,但默认期望 directory;Cloudflare Pages 访问 /about 时会自动找 about.html(所以 file 也行);自托管 nginx 如果没配 try_files,通常需要 file 形式。
具体表现:build 完产物里只有 dist/about.html,但宿主在 /about 找的是 about/index.html,于是 404(反过来也一样)。
如何判断:ls dist/ 看是 about.html 还是 about/index.html,再对照宿主实际请求的 URL。
2. Trailing slash 重定向和 Astro 路由打架
宿主层加了 /about/ 到 /about 的 301(或反向),但你的 Astro 站只生成了其中一种形式,请求被重定向到不存在的那一种,最终落到 404 页。Astro 6.1.x(trailing slash 的处理在 2026 年初被重写,修复随 6.1.3 在 2026 年 4 月 1 日发布)比旧版更严格地遵守你的 trailingSlash 配置,所以一个和它不一致的宿主重定向,现在更容易直接表现成硬 404。
如何判断:curl -I https://yoursite.com/about 看是 200、301 还是 404。如果看到 301,用 curl -IL https://yoursite.com/about 跟到底,确认最后一跳是 200 还是 404。
3. output: "server" 但宿主只接受静态产物
astro.config.mjs 里设了 output: "server",build 出来的是需要运行环境(或 serverless 函数)的 SSR 产物。如果宿主只能 serve 静态文件(GitHub Pages、纯静态 S3、裸对象存储),所有路由直接 404,可能只有 index.html 能命中。
注意:从 Astro v5(2024 年底)开始,旧的 output: "hybrid" 已经被移除。默认的 output: "static" 现在本身就允许单个页面通过 export const prerender = false 退出预渲染,这正是当年 hybrid 做的事。如果你的配置里还写着 output: "hybrid",build 会直接报错——改成 "static" 即可。
如何判断:看 dist/ 里有没有 _worker.js、.vercel/ 目录或 server/ 目录;有就是 server 产物,需要对应 adapter 加上能运行它的宿主。
4. 缺少 SPA fallback(以及那条会搞砸一切的 catch-all)
真正的 Astro 静态站不需要 SPA fallback——每条路由都是一个真实的 HTML 文件。只有当你在 Astro 之上手写了 client-side router 才需要 fallback。这里常见的错误恰恰相反:有人从 SPA 教程里抄了一条 catch-all rewrite,把所有请求都回退到 index.html,结果悄悄搞砸了其它所有页面,连 CSS/JS 也一起坏掉。
如何判断:访问 /anything-that-doesnt-exist。静态 Astro 站应该 serve 你的 404 页。如果反而每个 URL 都渲染出首页,说明你有一条过宽的 catch-all rewrite(修法见 Step 3)。
5. base 路径配置错
仓库部署到子路径(如 https://user.github.io/repo/)需要在 astro.config.mjs 里设 base: "/repo",否则所有内部链接和资源 URL 都指向错误路径。表现常常是页面 HTML 能加载,但每个链接和样式表都 404。
如何判断:打开 DevTools 切到 Network 标签,刷新,看 HTML 里资源路径是否带正确的 /repo/ 前缀。
最短修复路径
Step 1:本地 npm run build && npm run preview 复现 404
先把问题在本地复现,避免和宿主的 rewrite 层混在一起:
npm run build
npm run preview
# 然后开浏览器访问 http://localhost:4321/about
如果本地 preview 已经 404,说明是 Astro 配置问题(继续 Step 2);如果本地 OK、线上才 404,问题在宿主层(跳到 Step 3)。
Step 2:检查 dist/ 产物结构
ls dist/
# 期望:about/index.html (directory 模式)
# 或: about.html (file 模式)
对照 astro.config.mjs:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://yoursite.com',
trailingSlash: 'ignore', // 'always' | 'never' | 'ignore'
build: {
format: 'directory', // 'directory' | 'file'
},
output: 'static', // 'static' | 'server' (Astro v5 起没有 'hybrid')
});
| 宿主 | 推荐 build.format | trailingSlash |
|---|---|---|
| Vercel | directory | ignore |
| Netlify | directory | ignore |
| Cloudflare Pages | directory | ignore |
| GitHub Pages | directory | always |
| 纯静态 S3 + CloudFront | file | never |
对三大托管宿主来说,trailingSlash: 'ignore' 是最省心的选择,因为它让 Astro 带不带斜杠都能 serve,而不是强行做一次宿主可能不认的重定向。只有当你有明确理由(canonical URL 一致性、GitHub Pages 的怪癖)并且已经把宿主调成一致时,才去用 'always' 或 'never'。
Step 3:对齐(或删掉)宿主的 rewrite 层
这一步的修法取决于宿主。最常见的祸根是从 SPA 教程抄来的 catch-all rewrite。
Vercel —— 在 vercel.json 里控制 trailing slash,路由交给框架预设处理。静态站不要加 catch-all rewrite:
{
"trailingSlash": false
}
Netlify —— 对静态多页的 Astro 站,千万不要加 /* -> /index.html 200。这条规则会拦下所有请求(包括 JS 和 CSS),全部返回首页 HTML,让其它每个页面都 404。直接 publish dist/ 就好;Netlify 会自动用你生成的 404.html 处理未匹配的路径:
# netlify.toml
[build]
publish = "dist"
command = "npm run build"
# 静态多页站不要加 catch-all redirect。
# 只为真正的路由变更加 redirect,例如:
# [[redirects]]
# from = "/old-page"
# to = "/new-page"
# status = 301
/* -> /index.html 200 这条 rewrite 只对真正的单页应用(一个 HTML 文件启动 client-side router)才正确。
Cloudflare Pages —— 它访问 /about 时会自动 serve about.html,所以静态 Astro 站通常完全不需要 _redirects。如果你产出了 404.html(Astro 在有自定义 404 页时会生成),Pages 会用它处理未匹配路径。如果你没有产出 404.html,Pages 会把项目当成 SPA,对每个未匹配路径都返回根页面,这会把真正的路由问题掩盖掉。
改完之后,确认宿主的 trailing slash 设置和 Astro 的 trailingSlash 一致。一边 always 一边 never,正是经典的「重定向到 404」死循环。
Step 4:如果用了 output: "server",确认装了对应 adapter
# Vercel
npm install @astrojs/vercel
# Netlify
npm install @astrojs/netlify
# Cloudflare
npm install @astrojs/cloudflare
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'server',
adapter: vercel(),
});
有两个变化常把人绊倒:现在的 import 是 @astrojs/vercel,不再是旧的 @astrojs/vercel/serverless 子路径(那个 export 已被移除,现在再 import 会直接抛错);另外,output: "server" 不装 adapter 就部署,路由表根本生成不出来,所有动态路径都会 404。
Step 5:部署到子路径就检查 base
只对 GitHub Pages 项目站之类的子路径部署有关:
// astro.config.mjs
export default defineConfig({
site: 'https://user.github.io',
base: '/repo', // 必须和子路径一致;结尾不带斜杠
});
设了 base 之后,所有内部链接都应使用 import.meta.env.BASE_URL 前缀(或 Astro 的 <a href={...}> 写法),这样才能正确落在 /repo/ 下。
Step 6:清宿主的 deploy 缓存并重新部署
改完配置后,宿主有时会复用上次的 build 产物或陈旧的 CDN 副本:
- Vercel:dashboard,Deployments,最新 deploy,Redeploy,取消勾选 “Use existing Build Cache”。
- Cloudflare Pages:Workers & Pages,进项目触发一次全新部署;如果某条旧路由还在被缓存,到 Caching,Configuration 里清 CDN 缓存。
- Netlify:Deploys,Trigger deploy,“Clear cache and deploy site”。
如何确认已经修好
重新部署后,在终端对线上做一次快速冒烟测试:
for p in / /about /blog /this-should-404; do
echo -n "$p -> "; curl -s -o /dev/null -w "%{http_code}\n" "https://yoursite.com$p"
done
你希望真实路由返回 200,不存在的路径返回 404(不是 200,也不是没完没了的 301 链)。如果 /this-should-404 返回 200,说明你还有一条过宽的 catch-all rewrite;如果某条真实路由返回 301,用 curl -IL 跟到底,确认最后一跳是 200。
预防建议
- 把
build.format、trailingSlash、output三项以及宿主 rewrite 规则写进 README,新成员上手能直接对照。 - 本地
npm run preview模拟生产构建,至少访问 3 条非首页路由(嵌套路由、动态路由,以及一条故意写错的路径用来确认 404 页)。 - CI 跑
npx astro check提前抓 routing 和配置错误。 - 加一个部署后冒烟测试(上面那段
curl循环),状态码不符合预期就报警。 - 切到
output: "server"之前,先确认宿主 adapter 已经装好,且 build 产物里有 server 产物(.vercel/、_worker.js或server/)。
常见问题
为什么 /about 在 npm run dev 下正常,部署后却 404?
dev 服务器用 Vite,路由是即时解析的,从不走真实的静态文件布局。npm run build && npm run preview serve 的是真实 dist/ 产物,能复现生产环境。如果 preview 也 404,问题在 Astro 配置;如果只有线上 404,问题在宿主。
trailingSlash 应该用 'always'、'never' 还是 'ignore'?
对 Vercel、Netlify、Cloudflare Pages,'ignore' 是最安全的默认值——它避免强行做一次宿主可能不认的重定向。只有当你需要 canonical URL 一致、并且已经把宿主调成完全匹配时,才用 'always' 或 'never'。
我的配置写着 output: "hybrid",现在 build 报错了,发生了什么?
output: "hybrid" 在 Astro v5 已被移除,改成 output: "static" 即可。static 现在就是默认预渲染、同时允许单个页面用 export const prerender = false 退出预渲染的模式,这正是当年 hybrid 的行为。
import @astrojs/vercel/serverless 报错,怎么修?
那个子路径 export 已被移除。改成 import vercel from '@astrojs/vercel',用 adapter: vercel()。原来放在子路径里的配置,现在作为选项传给 vercel({ ... })。
Netlify 站上每个 URL 都显示首页,为什么?
你几乎肯定在 netlify.toml 或 public/_redirects 里有一条 /* -> /index.html 200。这条 catch-all 是给单页应用用的;在静态多页 Astro 站上,它会把首页 serve 给所有路径,连资源请求都一起坏掉。删掉它,让 Netlify 用你生成的 404.html 处理未匹配路径。
Cloudflare Pages 把本该 404 的路由 serve 成了首页,为什么?
如果你的 build 没有产出顶层 404.html,Cloudflare Pages 会把项目当成单页应用,对每个未匹配路径都返回根页面。加一个自定义 404 页(Astro 会从 src/pages/404.astro 产出 404.html),Pages 就会返回真正的 404。
相关阅读
- Firebase 路由 404
- Cloudflare Pages 缓存陈旧
- canonical 写错会怎样
- RSS feed 返回 404:3 个原因 + 修复路径
- Astro adapter 与 SSR/SSG 模式不匹配 —— 排查与修复
- 部署 preview URL 被搜索引擎收录 —— 排查与修复
- GitHub Actions 部署步骤跑了 6 小时被强杀 —— 排查与修复
- monorepo 部署只发了一个 app —— 排查与修复
- Netlify Function 冷启动 10 秒超时 —— 排查与修复
- Service Worker 部署后还在发旧 bundle —— 排查与修复
- Vercel 构建超过 45 分钟被强杀 —— 排查与修复