Next.js 静态托管 (nextstatic)
Next.js 静态托管 (nextstatic)
Next.js 15/16 export 产物托管,RSC 头处理、动态路由回退、SEO 注入
nextstatic
Next.js 静态导出(output: 'export')产物的 Go 托管处理器,兼容 Next.js 15 与 16 两代导出结构。由 Go 统一提供 /api 与静态页面时需要正确处理一组容易踩坑的细节,本包收敛为一个开箱即用的 http.Handler。
解决的问题
| 坑 | 处理 |
|---|---|
RSC payload .txt 走默认 text/plain | 设为 text/x-component; charset=utf-8,否则 Next.js 16 客户端路由回退硬导航,页面出现一片 RSC 文本;Next 15 的每目录 index.txt 同样处理 |
| CDN 把 HTML 与 RSC 缓存串台 | RSC 响应带 Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch |
robots.txt 被误判为 RSC | 按文件名特征区分,普通 .txt 保持 text/plain |
| Next 16 route group 点号路径 404 | /admin/users/__next.!<b64>.admin.users.__PAGE__.txt 自动还原为磁盘目录结构查找 |
| CDN 缓存旧 HTML 引用已覆盖的 chunk | HTML 用硬化 no-store, no-cache, must-revalidate, max-age=0;/_next/static 长期缓存 + immutable |
trailingSlash: true 刷新 404 | 无扩展名页面请求先 308 补斜杠(Location 强制相对路径,防开放重定向) |
| 动态路由直达 404 | /voddetail/123/ 回退占位符目录 /voddetail/_/index.html;RSC 数据文件同路回退;另支持"动态详情导出在父级"形态(/app/123/ → /app/index.html)的逐级父目录回退 |
| 未命中兜底根首页造成软 404 + RSC fetch 死循环 | 默认返回 404.html + 404 状态码;纯 SPA 项目显式开 SPAFallback 恢复"根 index + 200" |
| 未注册 API 路径被页面兜底吞掉 | 命中 APIPrefixes(默认 /api/)直接返回 JSON 404 |
| index.html 被浏览器当 Service Worker 注册 | /sw.js 强制 no-cache + Service-Worker-Allowed: /,缺失时硬 404 绝不回退 HTML |
| 动态 SEO | SEOInject 钩子在 HTML 写出前注入 __SEO_*__ 占位符内容 |
使用
h, err := nextstatic.New(nextstatic.Config{
Root: "./web/out",
TrailingSlash: true, // 与 next.config 对齐
SEOInject: func(r *http.Request, html []byte) []byte {
return seo.Render(r.URL.Path, html) // 注入内容须自行 HTML 转义
},
// APIPrefixes: 默认 ["/api/"]; 传空 slice 关闭
// SPAFallback: 纯 SPA 项目 (无 SSG 动态页) 设 true
})
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.Handle("/api/", apiRouter)
mux.Handle("/", h) // 兜底路由Gin 集成
返回值是标准 http.Handler,Gin 用 gin.WrapH 挂到 NoRoute:
r := gin.Default()
r.GET("/api/users", listUsers) // API 路由正常注册, 优先匹配
r.GET("/sitemap.xml", genSitemap) // 动态 sitemap 等特殊路由在外层先挂
r.NoRoute(gin.WrapH(h)) // 其余请求全部交给静态托管配合细节:
- 已注册的
/api路由不进 NoRoute;未注册的/api/*落进 NoRoute 后由APIPrefixes返回 JSON 404,不会被页面兜底吞掉,无需再手写这段防护 - Gin 的
RedirectTrailingSlash只对已注册路由生效,NoRoute 请求的补斜杠由本包的 308 处理,两者不冲突 r.Use(...)挂载的日志、限流、机器人拦截等中间件照常在 NoRoute 之前执行
解析顺序
API 前缀 JSON 404 → 直接静态文件 → RSC 点号路径还原 → RSC 占位符/父目录回退 → trailing slash 308 → 目录 index.html → <path>.html → 动态路由占位符 → 逐级父目录 index.html → 404.html(或 SPA 兜底)
sitemap、旧链接重定向等特殊路由应在外层路由先行匹配,再落到本 Handler。
限制
- 动态段占位符回退只处理单级动态段(最后一级目录替换为
_);多级嵌套动态段在外层路由先行改写 - HTML 模板每次请求读盘;高 QPS 的 SEO 注入场景建议在
SEOInject上层自行缓存模板
独立工具函数
nextstatic.IsRSCPayload("/about/__next._index.txt") // true
nextstatic.IsRSCPayload("/robots.txt") // false
nextstatic.SetHeaders(w, urlPath) // 按路径分类设 Content-Type/Vary/缓存头