Next.js 16 App Router 实战:从 About、Blog 动态路由到全局 404

Next.js 16 App Router 实战:从 About、Blog 动态路由到全局 404

一个 Next.js 学习项目,怎样从默认首页逐步变成结构完整的小型内容站?关键不在于堆页面,而在于把 layout.tsx、动态路由、共享数据、页面元数据和 404 串成一条闭环。本文基于一个 Next.js 16.3.1、React 19.2.8、Tailwind CSS 4 项目做静态源码分析,整理可直接复用的实现路线。项目代码未在本次写作流程中执行,运行未验证。

项目最终结构

text 复制代码
app/
├─ layout.tsx
├─ page.tsx
├─ not-found.tsx
├─ about/
│  └─ page.tsx
└─ blog/
   ├─ page.tsx
   ├─ posts.ts
   └─ [slug]/
      └─ page.tsx
components/ui/button.tsx

这组文件形成五类路由能力:共享布局、静态页面、文章列表、动态详情和未找到兜底。顶部导航连接 //about/blog,所有后代页面复用同一个根布局。

用 layout.tsx 管理共享导航

根布局的职责是提供所有页面共同需要的外壳。项目在这里加载 Geist 字体、声明根 metadata、设置全局背景,并放置导航:

tsx 复制代码
export default function RootLayout({ children }: LayoutProps<"/">) {
  return (
    <html lang="en">
      <body>
        <header>
          <Link href="/">NEXT.JS</Link>
          <nav aria-label="主导航">
            <Link href="/about">About</Link>
            <Link href="/blog">Blog</Link>
          </nav>
        </header>
        {children}
      </body>
    </html>
  );
}

共享内容放进 layout 后,About、Blog 和 404 不必重复顶部导航。页面只维护自己的主体内容,路由树也更容易理解。

About 页面:静态元数据与数据驱动 UI

About 页面直接导出静态 metadata

tsx 复制代码
export const metadata: Metadata = {
  title: "关于 Next.js",
  description: "了解 Next.js 的核心能力、优势和适用场景。",
};

页面中的四项核心能力和四个适用场景没有逐块复制 JSX,而是由数组映射。这种方式适合结构一致、内容不同的模块:新增一项只改数据,不改页面骨架。

判断标准很简单:页面标题和描述不依赖 URL 参数时,静态 metadata 足够;需要根据 slug 改变时,再使用 generateMetadata

Blog 列表:让共享数据成为单一事实源

项目把文章字段集中在 posts.ts

ts 复制代码
export type BlogPost = {
  slug: string;
  index: string;
  category: string;
  title: string;
  description: string;
  publishedAt: string;
  sourceUrl: string;
  topics: string[];
  takeaways: string[];
};

当前数据中有两篇文章。Blog 列表遍历 blogPosts,每张卡片链接到动态地址:

tsx 复制代码
{blogPosts.map((post) => (
  <Link key={post.slug} href={`/blog/${post.slug}`}>
    <h3>{post.title}</h3>
    <p>{post.description}</p>
  </Link>
))}

共享数据的价值在于,列表、详情、构建参数和元数据都依赖同一份 slug、标题与摘要,避免多处硬编码产生漂移。

动态路由:从 slug 到文章详情

app/blog/[slug]/page.tsx 捕获文章路径。这个项目使用 Next.js 16.3.1,页面 props 中的 params 是 Promise:

tsx 复制代码
type BlogPostPageProps = {
  params: Promise<{ slug: string }>;
};

export default async function BlogPostPage({ params }: BlogPostPageProps) {
  const { slug } = await params;
  const post = getBlogPost(slug);

  if (!post) {
    notFound();
  }

  return <article>{post.title}</article>;
}

完整链路是:URL 中的动态段进入 params,页面 await params 取得 slug,再通过 getBlogPostArray.find 查找文章。框架 API 会随版本变化,因此应优先查看项目安装版本的文档和类型。

generateStaticParams:提前声明已知文章

文章数据在构建时已知,因此可以从同一个数组生成动态参数:

tsx 复制代码
export function generateStaticParams() {
  return blogPosts.map((post) => ({ slug: post.slug }));
}

这段代码把"有哪些详情页"与文章数据绑定。新增第三篇文章时,只要 slug 进入 blogPosts,参数生成逻辑无需增加分支。

注意:返回对象的键必须与 [slug] 的名称一致。如果目录叫 [id],对象也应返回 { id: ... }

generateMetadata:为每篇文章生成标题与摘要

动态详情不能共享一个固定标题。项目在 metadata 阶段再次使用 slug 查询文章:

tsx 复制代码
export async function generateMetadata({
  params,
}: BlogPostPageProps): Promise<Metadata> {
  const { slug } = await params;
  const post = getBlogPost(slug);

  if (!post) return { title: "文章未找到" };

  return {
    title: post.title,
    description: post.description,
  };
}

页面渲染和 metadata 使用相同查询函数,能减少标题与正文不一致的风险。

notFound 与根级 404

找不到文章时,详情页调用 notFound(),而不是返回 null 或临时错误文本。根级 app/not-found.tsx 负责统一展示大号 404、说明文字和返回入口。

这形成明确的失败分支:

text 复制代码
/blog/slug
  → getBlogPost(slug)
  → 命中:渲染详情
  → 未命中:notFound()
  → app/not-found.tsx

404 不是最后补上的装饰页面,而是动态内容模型的一部分。

shadcn/ui Button 如何保持链接语义

项目的 Button 基于 @base-ui/react/button,使用 class-variance-authority 管理 defaultoutline 等变体。最新版生成组件使用 render 把 Button 渲染为链接:

tsx 复制代码
<Button
  variant="outline"
  render={<Link href="/blog" />}
>
  返回 Blog
</Button>

外链则传入 <a>,继续保留:

tsx 复制代码
<Button
  render={
    <a
      href={post.sourceUrl}
      target="_blank"
      rel="noopener noreferrer"
    />
  }
>
  阅读原文
</Button>

这样既复用按钮样式,又不写出 <button><a> 这种交互元素嵌套。

App Router 内容站实现清单

  • layout.tsx 只放共享字体、导航与全局外壳。
  • 静态页面使用 page.tsx 和静态 metadata
  • 动态目录名与数据键统一,例如 [slug] 对应 post.slug
  • 列表、详情、静态参数和动态元数据读取同一数据源。
  • 当前版本若把 params 定义为 Promise,页面和元数据函数都要 await
  • 未知资源调用 notFound(),并提供根级 not-found.tsx
  • 站内导航使用 Link,外链保留安全属性。
  • 链接式按钮保持链接语义,不嵌套交互元素。
  • 新增文章后检查列表 URL、数据 slug 与动态参数键是否完全一致。
  • 按项目安装版本核对 Next.js API,避免照搬旧教程。

总结

这个项目最值得复用的不是某个页面样式,而是围绕 blogPosts 建立的闭环:列表负责发现内容,动态段负责定位内容,静态参数负责声明内容,动态元数据负责描述内容,notFound() 负责处理缺失内容。

掌握这条链路后,把静态数组替换成文件系统、CMS 或数据库时,页面边界仍然可以保持稳定。下一步可以为合法 slug 和未知 slug 增加测试,再把同步查询替换为异步数据访问。当前项目未提供测试框架,本次运行未验证。

标签:

  • Next.js
  • React
  • App Router
  • TypeScript
  • shadcn/ui
相关推荐
quantdash_cc1 小时前
历史数据断层与REST请求慢到崩溃?QuantDash高性能量化数据API终极解决方案
开发语言·python·缓存·php·量化·quantdash
熊野君1 小时前
Prompt / RAG / 规则 / Skills —— 定位、协作与实现路线
开发语言·python
山甫aa2 小时前
JavaWeb后端开发学习手册
java·开发语言·数据库·学习·mysql·springboot·web
zlinear数据采集卡2 小时前
数据采集卡从入门到精通(42):项目实战三——设备预测性维护系统,从布点到预警
开发语言·单片机·嵌入式硬件·安全·fpga开发
csdn_aspnet2 小时前
C# 高效便利的处理数据
开发语言·windows·c#
Chester_19992 小时前
CSP202209.B何以包邮?
c语言·开发语言
caimouse2 小时前
ReactOS 窗口系统分析(13):滚动条 — scrollbar.c + scrollex.c
c语言·开发语言·reactos
Sagittarius_A*2 小时前
【好靶场】PHP反序列化入门练习2
开发语言·web安全·信息安全·php·代码审计·反序列化
zh_xuan2 小时前
个人主页左侧菜单支持分组,以及菜单显示和隐藏
前端·javascript·css