Astro 部署后页面 404:3 个原因 + 修复路径

dev 正常、线上 404——多半是 build.format / trailingSlash / output 三者和宿主不对齐。给一条可照抄的修复路径。

npm run dev 一切正常,部署到 Vercel、Netlify 或 Cloudflare Pages 后访问 /about 直接返回 404,或者只有首页能打开、其它路由全军覆没——这是 Astro 部署里最常见的一类陷阱。几乎都不是 Astro 本身的 bug,而是 build.formattrailingSlashoutput 三者中至少一个和宿主的默认行为不对齐。

最快的修复办法:先在本地跑 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 在哪跳到
404404Astro 配置(build.format / output / base下文 Step 2、4、5
200404宿主的 redirect / rewrite / 缓存层下文 Step 3、6
200200已经修好,或 CDN 缓存陈旧——清缓存下文 Step 6

npm run preview serve 的是真实的 dist/ 产物,方式和大多数静态宿主一致,所以它比 npm run dev 更接近生产环境(dev 用的是 Vite 开发服务器,路由解析方式完全不同)。

常见原因

按命中率从高到低排。

1. build.format 和宿主的默认 trailing slash 不一致

Astro 的 build.format 决定每个页面输出 about/index.htmldirectory)还是 about.htmlfile)。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.formattrailingSlash
Verceldirectoryignore
Netlifydirectoryignore
Cloudflare Pagesdirectoryignore
GitHub Pagesdirectoryalways
纯静态 S3 + CloudFrontfilenever

对三大托管宿主来说,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.formattrailingSlashoutput 三项以及宿主 rewrite 规则写进 README,新成员上手能直接对照。
  • 本地 npm run preview 模拟生产构建,至少访问 3 条非首页路由(嵌套路由、动态路由,以及一条故意写错的路径用来确认 404 页)。
  • CI 跑 npx astro check 提前抓 routing 和配置错误。
  • 加一个部署后冒烟测试(上面那段 curl 循环),状态码不符合预期就报警。
  • 切到 output: "server" 之前,先确认宿主 adapter 已经装好,且 build 产物里有 server 产物(.vercel/_worker.jsserver/)。

常见问题

为什么 /aboutnpm 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.tomlpublic/_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。

相关阅读

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