知识库首页 知识库-世界 2026-01-22-nextjs15-blog-route-404-params-type.md

2026 01 22 nextjs15 blog route 404 params type

本地来源:Knowledge/World/项目/Practice/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 页面

现象

  1. 访问博客文章 URL 显示 404 错误页面
  2. 浏览器控制台显示:GET https://seedvr2.net/blog/tutorials/tutorials-seedvr2-complete-guide-2026 404 (Not Found)
  3. 本地构建显示路由已生成,但生产环境无法访问
  4. 带语言前缀的 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 不包含博客文章的动态路由

现象

  1. sitemap.xml 只包含静态路由(如 /blog),不包含具体文章路由
  2. 搜索引擎无法通过 sitemap 发现博客内容
  3. 可能影响 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) - 包含正确的 lastModifiedprioritychangeFrequency 元数据 - 包含多语言 alternates 链接,有利于国际化 SEO


查漏补缺

已确认不受影响的场景

场景 状态 说明
带语言前缀的博客路由(/en/blog/... ✅ 正常 src/app/[locale]/(marketing)/blog/[...slug]/page.tsx 类型正确
其他博客文章(cookbook-ora2sora-2 ✅ 正常 这些文章在根目录下,使用不同的路由模式
静态路由(首页、定价等) ✅ 正常 不受 params 类型变更影响
API 路由 ✅ 正常 不涉及页面 params

潜在改进点

  1. 全局搜索类似问题 - 检查项目中是否还有其他动态路由使用了旧的 params 类型 - 建议搜索:params: { 并检查是否应该改为 params: Promise<{

  2. CI/CD 增强 - 在 pre-commit hook 中添加类型检查失败时的明确提示 - 考虑添加路由可访问性测试

  3. 文档更新 - 在项目 README 中添加 Next.js 15 升级注意事项 - 记录 params 类型变更的最佳实践

  4. 监控告警 - 为博客路由添加可用性监控 - 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;
 }

测试建议

本地测试

  1. 构建测试 bash pnpm build

验证点: - ✅ 构建成功无 TypeScript 错误 - ✅ 输出中包含 /blog/tutorials/tutorials-seedvr2-complete-guide-2026 - ✅ 生成 4 个语言版本(默认 + en/zh/es)

  1. 本地预览 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

  1. Sitemap 测试 bash curl http://localhost:3000/sitemap.xml | grep "blog/tutorials"

验证点: - ✅ sitemap 包含博客文章 URL - ✅ 每篇文章有 3 个语言版本

生产环境测试

  1. 部署后验证

等待 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

  1. Sitemap 验证 bash curl https://seedvr2.net/sitemap.xml | grep "tutorials-seedvr2-complete-guide"

期望结果: 至少 3 个匹配项(3 种语言)

  1. 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 健康度

回滚方案

如果部署后出现问题:

  1. 立即回滚到上一个稳定版本 bash git revert HEAD git push

  2. Vercel 平台回滚 - 访问 Vercel Dashboard - 选择上一次成功的部署 - 点击 "Promote to Production"

  3. 紧急修复流程 - 创建 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-项目-Practice-BUG-SOLUTIONS-2026-01-22-nextjs1-6e11b2.md(仅本地保留,不入库不部署)