2026 01 22 nextjs15 blog route 404 params type
本地来源:Knowledge/World/项目/文档/BUG_SOLUTIONS/2026-01-22-nextjs15-blog-route-404-params-type.md
Next.js 15 博客路由 404 问题 - params 类型不兼容导致默认语言路由构建失败
TL;DR 速查表
问题速览
| # | 问题 | 原因 | 解决方案 |
|---|---|---|---|
| 1 | 博客文章访问 404 | Next.js 15 params 类型要求改为 Promise,但代码未更新 | 更新类型定义为 Promise<{ slug: string[] }> 并使用 await params |
| 2 | 默认语言路由未生成 | TypeScript 类型错误导致构建失败 | 修复类型后重新构建生成路由 |
| 3 | sitemap 缺少博客链接 | sitemap.ts 未包含博客动态路由 | 添加 blogSource.getPages() 到 sitemap 生成逻辑 |
关键代码修复
// ❌ 错误:Next.js 14 的写法
interface DefaultBlogPostPageProps {
params: {
slug: string[];
};
}
export async function generateMetadata({ params }: DefaultBlogPostPageProps) {
return generateLocaleMetadata({
params: Promise.resolve({
locale: DEFAULT_LOCALE,
slug: params.slug, // ❌ params 是同步的
}),
});
}
// ✅ 正确:Next.js 15 的写法
interface DefaultBlogPostPageProps {
params: Promise<{
slug: string[];
}>;
}
export async function generateMetadata({ params }: DefaultBlogPostPageProps) {
const { slug } = await params; // ✅ 使用 await 解构
return generateLocaleMetadata({
params: Promise.resolve({
locale: DEFAULT_LOCALE,
slug,
}),
});
}
决策流程图
用户访问博客 URL
↓
Next.js 路由匹配
↓
检查 generateStaticParams
↓
[TypeScript 类型检查]
├─ ❌ 类型错误 → 构建失败 → 路由未生成 → 404
└─ ✅ 类型正确 → 构建成功 → 路由生成 → 页面显示
修改文件清单
| 文件路径 | 修改内容 |
|---|---|
src/app/(default)/(marketing)/blog/[...slug]/page.tsx |
更新 params 类型为 Promise,添加 await 解构 |
src/app/sitemap.ts |
添加博客文章动态路由到 sitemap |
项目信息
- 项目名称: SeedVR2.net
- 涉及模块: Blog 系统、多语言路由、静态站点生成
- 技术栈: Next.js 15, next-intl, Fumadocs, TypeScript
- 报告日期: 2026-01-22
问题 #1: 博客文章路由返回 404
具体问题
用户访问 https://seedvr2.net/blog/tutorials/tutorials-seedvr2-complete-guide-2026 返回 404 页面
现象
- 访问博客文章 URL 显示 404 错误页面
- 浏览器控制台显示:
GET https://seedvr2.net/blog/tutorials/tutorials-seedvr2-complete-guide-2026 404 (Not Found) - 本地构建显示路由已生成,但生产环境无法访问
- 带语言前缀的 URL(如
/en/blog/...)可以正常访问
本质
Next.js 15 改变了动态路由的 params 类型系统:
- Next.js 14: params: { slug: string[] }(同步对象)
- Next.js 15: params: Promise<{ slug: string[] }>(异步 Promise)
项目升级到 Next.js 15 后,src/app/(default)/(marketing)/blog/[...slug]/page.tsx 文件仍使用旧的类型定义,导致 TypeScript 类型错误。
罪魁祸首
文件: src/app/(default)/(marketing)/blog/[...slug]/page.tsx
错误代码:
// 第 11-15 行
interface DefaultBlogPostPageProps {
params: { // ❌ 错误:应该是 Promise
slug: string[];
};
}
// 第 26-34 行
export async function generateMetadata({
params,
}: DefaultBlogPostPageProps) {
return generateLocaleMetadata({
params: Promise.resolve({
locale: DEFAULT_LOCALE,
slug: params.slug, // ❌ 错误:params 不是 Promise,无法直接访问 .slug
}),
});
}
错误表现:
Type error: Type 'DefaultBlogPostPageProps' does not satisfy the constraint 'PageProps'.
Types of property 'params' are incompatible.
Type '{ slug: string[]; }' is missing the following properties from type 'Promise<any>': then, catch, finally, [Symbol.toStringTag]
影响:
- 构建时 TypeScript 检查失败
- 默认语言路由(/blog/[...slug])未生成
- 用户访问不带语言前缀的博客 URL 返回 404
解决办法
步骤 1: 更新类型定义
// src/app/(default)/(marketing)/blog/[...slug]/page.tsx
interface DefaultBlogPostPageProps {
params: Promise<{ // ✅ 改为 Promise
slug: string[];
}>;
}
步骤 2: 更新 generateMetadata 函数
export async function generateMetadata({ params }: DefaultBlogPostPageProps) {
const { slug } = await params; // ✅ 使用 await 解构
return generateLocaleMetadata({
params: Promise.resolve({
locale: DEFAULT_LOCALE,
slug, // ✅ 使用解构后的 slug
}),
});
}
步骤 3: 更新页面组件
export default async function DefaultBlogPostPage({
params,
}: DefaultBlogPostPageProps) {
const { slug } = await params; // ✅ 添加 async 和 await
return (
<BlogPostPage
params={Promise.resolve({
locale: DEFAULT_LOCALE,
slug,
})}
/>
);
}
验证构建:
pnpm build 2>&1 | grep -E "(blog|tutorials)"
输出应显示:
├ ● /blog/[...slug] 792 B 117 kB
├ └ /blog/tutorials/tutorials-seedvr2-complete-guide-2026
├ ● /[locale]/blog/[...slug] 792 B 117 kB
├ ├ /en/blog/tutorials/tutorials-seedvr2-complete-guide-2026
├ ├ /zh/blog/tutorials/tutorials-seedvr2-complete-guide-2026
├ └ /es/blog/tutorials/tutorials-seedvr2-complete-guide-2026
问题 #2: Sitemap 缺少博客文章链接
具体问题
https://seedvr2.net/sitemap.xml 不包含博客文章的动态路由
现象
- sitemap.xml 只包含静态路由(如
/blog),不包含具体文章路由 - 搜索引擎无法通过 sitemap 发现博客内容
- 可能影响 Vercel 的静态页面部署
本质
src/app/sitemap.ts 文件只生成了预定义的静态路由列表,没有动态获取博客文章并生成对应的 sitemap 条目。
罪魁祸首
文件: src/app/sitemap.ts
缺失内容: 没有从 blogSource 获取文章列表并生成 sitemap 条目
解决办法
步骤 1: 导入 blogSource
import { blogSource } from '@/lib/source';
步骤 2: 在 sitemap 生成函数中添加博客文章
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const sitemapList: MetadataRoute.Sitemap = [];
// ... 添加静态路由 ...
// ✅ 添加博客文章到 sitemap
const blogPages = blogSource.getPages();
const publishedBlogPages = blogPages.filter(
(page) => page.data.published !== false
);
sitemapList.push(
...publishedBlogPages.flatMap((page) => {
return routing.locales.map((locale) => {
const blogPath = `/blog/${page.slugs.join('/')}`;
const url = getUrl(blogPath as Href, locale);
const languages = Object.fromEntries(
routing.locales.map((cur) => [cur, getUrl(blogPath as Href, cur)])
) as Record<string, string>;
languages['x-default'] = getUrl(
blogPath as Href,
routing.defaultLocale
);
return {
url,
lastModified: page.data.date ? new Date(page.data.date) : new Date(),
priority: 0.8,
changeFrequency: 'monthly' as const,
alternates: {
languages,
},
};
});
})
);
return sitemapList;
}
效果:
- 每篇博客文章生成 3 个语言版本的 sitemap 条目(en/zh/es)
- 包含正确的 lastModified、priority、changeFrequency 元数据
- 包含多语言 alternates 链接,有利于国际化 SEO
查漏补缺
已确认不受影响的场景
| 场景 | 状态 | 说明 |
|---|---|---|
带语言前缀的博客路由(/en/blog/...) |
✅ 正常 | src/app/[locale]/(marketing)/blog/[...slug]/page.tsx 类型正确 |
其他博客文章(cookbook-ora2、sora-2) |
✅ 正常 | 这些文章在根目录下,使用不同的路由模式 |
| 静态路由(首页、定价等) | ✅ 正常 | 不受 params 类型变更影响 |
| API 路由 | ✅ 正常 | 不涉及页面 params |
潜在改进点
-
全局搜索类似问题 - 检查项目中是否还有其他动态路由使用了旧的 params 类型 - 建议搜索:
params: {并检查是否应该改为params: Promise<{ -
CI/CD 增强 - 在 pre-commit hook 中添加类型检查失败时的明确提示 - 考虑添加路由可访问性测试
-
文档更新 - 在项目 README 中添加 Next.js 15 升级注意事项 - 记录 params 类型变更的最佳实践
-
监控告警 - 为博客路由添加可用性监控 - 404 错误达到阈值时发送告警
修改文件清单
| 文件路径 | 修改内容 | 代码行数 |
|---|---|---|
src/app/(default)/(marketing)/blog/[...slug]/page.tsx |
更新 params 类型为 Promise,添加 async/await | ~48 行 |
src/app/sitemap.ts |
添加博客文章动态路由生成逻辑 | +31 行 |
详细变更
文件 1: src/app/(default)/(marketing)/blog/[...slug]/page.tsx
interface DefaultBlogPostPageProps {
- params: {
+ params: Promise<{
slug: string[];
- };
+ }>;
}
export async function generateMetadata({ params }: DefaultBlogPostPageProps) {
+ const { slug } = await params;
return generateLocaleMetadata({
params: Promise.resolve({
locale: DEFAULT_LOCALE,
- slug: params.slug,
+ slug,
}),
});
}
-export default function DefaultBlogPostPage({
+export default async function DefaultBlogPostPage({
params,
}: DefaultBlogPostPageProps) {
+ const { slug } = await params;
return (
<BlogPostPage
params={Promise.resolve({
locale: DEFAULT_LOCALE,
- slug: params.slug,
+ slug,
})}
/>
);
}
文件 2: src/app/sitemap.ts
+import { blogSource } from '@/lib/source';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// ... 静态路由 ...
+ // Add blog posts to sitemap
+ const blogPages = blogSource.getPages();
+ const publishedBlogPages = blogPages.filter(
+ (page) => page.data.published !== false
+ );
+
+ sitemapList.push(
+ ...publishedBlogPages.flatMap((page) => {
+ return routing.locales.map((locale) => {
+ const blogPath = `/blog/${page.slugs.join('/')}`;
+ const url = getUrl(blogPath as Href, locale);
+ const languages = Object.fromEntries(
+ routing.locales.map((cur) => [cur, getUrl(blogPath as Href, cur)])
+ ) as Record<string, string>;
+ languages['x-default'] = getUrl(
+ blogPath as Href,
+ routing.defaultLocale
+ );
+
+ return {
+ url,
+ lastModified: page.data.date ? new Date(page.data.date) : new Date(),
+ priority: 0.8,
+ changeFrequency: 'monthly' as const,
+ alternates: {
+ languages,
+ },
+ };
+ });
+ })
+ );
return sitemapList;
}
测试建议
本地测试
- 构建测试
bash pnpm build
验证点:
- ✅ 构建成功无 TypeScript 错误
- ✅ 输出中包含 /blog/tutorials/tutorials-seedvr2-complete-guide-2026
- ✅ 生成 4 个语言版本(默认 + en/zh/es)
- 本地预览
bash pnpm build && pnpm start
测试 URL:
- http://localhost:3000/blog/tutorials/tutorials-seedvr2-complete-guide-2026
- http://localhost:3000/en/blog/tutorials/tutorials-seedvr2-complete-guide-2026
- http://localhost:3000/zh/blog/tutorials/tutorials-seedvr2-complete-guide-2026
- Sitemap 测试
bash curl http://localhost:3000/sitemap.xml | grep "blog/tutorials"
验证点: - ✅ sitemap 包含博客文章 URL - ✅ 每篇文章有 3 个语言版本
生产环境测试
- 部署后验证
等待 Vercel 部署完成后(约 2-3 分钟):
```bash # 测试主 URL curl -I https://seedvr2.net/blog/tutorials/tutorials-seedvr2-complete-guide-2026
# 测试语言版本 curl -I https://seedvr2.net/en/blog/tutorials/tutorials-seedvr2-complete-guide-2026 ```
期望结果: HTTP 200 OK
- Sitemap 验证
bash curl https://seedvr2.net/sitemap.xml | grep "tutorials-seedvr2-complete-guide"
期望结果: 至少 3 个匹配项(3 种语言)
- SEO 验证 - 访问 Google Search Console - 提交新 sitemap URL - 验证博客文章可被索引
回归测试
| 测试场景 | 测试方法 | 期望结果 |
|---|---|---|
| 其他博客文章 | 访问 /blog/cookbook-ora2-prompting-guide |
HTTP 200 |
| 博客列表页 | 访问 /blog |
显示文章列表 |
| 语言切换 | 切换到中文并访问博客 | URL 变为 /zh/blog/... |
| 404 处理 | 访问不存在的文章 /blog/nonexistent |
显示 404 页面 |
| 静态资源 | 检查博客文章中的图片 | 正常加载 |
SOP 检查清单
部署前检查
- [x] 编译检查
- [x]
pnpm build成功 - [x] 无 TypeScript 错误
-
[x] 无 ESLint 警告
-
[x] 类型检查
- [x] params 类型符合 Next.js 15 要求
-
[x] 所有动态路由组件已更新
-
[x] 环境变量
- [x] 生产环境变量齐全(
NEXT_PUBLIC_BASE_URL等) -
[x] 无敏感信息硬编码
-
[x] 依赖检查
- [x] Next.js 版本:15.3.6
- [x] next-intl 与 Next.js 15 兼容
- [x] fumadocs 正常工作
功能验证清单
博客系统
- [ ] 博客列表页加载正常
- [ ] 博客文章详情页显示正确
- [ ] 博客分页功能正常
- [ ] 博客分类筛选有效
路由系统
- [ ] 默认语言路由可访问(无前缀)
- [ ] 带语言前缀路由可访问(
/en、/zh、/es) - [ ] 语言切换功能正常
- [ ] 404 页面正确显示
SEO
- [ ] sitemap.xml 包含所有博客文章
- [ ] 每篇文章有多语言 alternates
- [ ] meta 标签正确生成
- [ ] Open Graph 图片显示
多语言
- [ ] 英文内容显示正确
- [ ] 中文内容显示正确
- [ ] 西班牙文内容显示正确
- [ ] 语言检测和切换流畅
监控与告警
关键监控指标: - 博客路由 404 错误率 < 1% - 页面加载时间 < 3s - 构建成功率 = 100%
告警关键词:
- 404 Not Found + blog/tutorials
- Type error + params
- PageNotFoundError
- build failed
监控工具: - Vercel Analytics - 页面访问统计 - Sentry - 错误追踪 - Google Search Console - SEO 健康度
回滚方案
如果部署后出现问题:
-
立即回滚到上一个稳定版本
bash git revert HEAD git push -
Vercel 平台回滚 - 访问 Vercel Dashboard - 选择上一次成功的部署 - 点击 "Promote to Production"
-
紧急修复流程 - 创建 hotfix 分支 - 修复问题 - 快速测试 - 合并并重新部署
变更记录表
| 日期 | 版本 | 变更内容 | 影响范围 | 负责人 |
|---|---|---|---|---|
| 2026-01-22 | 1.0.0 | 修复 Next.js 15 params 类型错误 | 博客路由系统 | Claude |
| 2026-01-22 | 1.0.0 | 添加博客文章到 sitemap | SEO / 路由生成 | Claude |
总结
此次问题的根本原因是 Next.js 15 框架升级导致的 API 破坏性变更。params 从同步对象改为异步 Promise,但项目代码未同步更新,导致类型检查失败和路由生成错误。
关键教训: 1. 框架大版本升级需要仔细阅读迁移指南 2. TypeScript 类型错误不仅是警告,会影响构建结果 3. 动态路由的静态生成依赖正确的类型定义 4. sitemap 对 SEO 和某些部署平台至关重要
预防措施: - 建立框架升级检查清单 - 增强 CI/CD 中的类型检查 - 定期审查构建日志 - 监控生产环境路由可用性
报告生成时间: 2026-01-22 01:00:00
报告版本: 1.0
相关 Commit:
- 2182d77 - fix: update blog params type for Next.js 15 async params
- db9d1d2 - feat: add blog posts to sitemap.xml
本文档为站内渲染。原始文件本地路径:saas/source/knowledge-world/Knowledge-World-项目-文档-BUG_SOLUTIONS-2026-01-22-nextjs15-blog-10884f.md(仅本地保留,不入库不部署)