从零开始学前端 | 第四十七章:第五阶段综合实战:文章详情页、动态路由参数与正文渲染
2026/7/23 13:05:48 网站建设 项目流程

本章定位

上一章,我们已经把第五阶段综合实战从“项目骨架搭建”推进到了“内容主线真正开始跑起来”的阶段。

你已经完成了这些非常关键的事情:

  1. 博客项目已经有了首页、文章列表页、关于页、联系页和基础布局。
  2. 本地文章数据已经开始接入项目。
  3. 首页已经能展示推荐文章和最近文章。
  4. 文章列表页已经能展示全部文章。
  5. slug、文章数据和getPostBySlug这条主线已经准备好了。

也就是说,到现在为止,你的博客项目已经不是“空壳网站”了,而是已经开始有内容入口、有阅读入口、有站点感。

接下来,我们要继续推进一条非常关键的主线:

让用户真的点进一篇文章,并把这篇文章完整读下来。

这一章非常重要。

因为博客项目和很多工具型项目不一样,它有一个天然非常核心的页面:

文章详情页

如果没有详情页,用户只能:

  • 看到首页入口
  • 看到文章列表
  • 看到标题和摘要

但还不能真正进入:

阅读

所以这一篇我们要解决的核心问题是:

  1. 动态路由参数到底在解决什么问题?
  2. 详情页怎样通过slug找到对应文章?
  3. 如果这篇文章不存在,页面应该怎么办?
  4. 文章标题、分类、日期、封面和正文,应该怎样组织到详情页里?
  5. 为什么正文第一版先用string[]渲染就是一个很稳的方案?

你可以把这一章理解成:

第五阶段综合实战里,第一次让博客项目真正具备“可点击进入阅读”的主线篇。

本章学习目标

学完这一章后,你应该能做到:

  1. 理解为什么文章详情页是博客项目里最核心的阅读页面之一。
  2. 理解动态路由参数在博客详情页里的作用。
  3. 学会把路由地址、slug和文章数据之间的关系真正串起来。
  4. 知道src/app/posts/[slug]/page.tsx在当前项目里扮演什么角色。
  5. 学会在详情页中通过getPostBySlug查找对应文章。
  6. 理解为什么文章不存在时更适合走 404 流程,而不是默默显示空白页。
  7. 学会使用notFound()处理不存在文章的场景。
  8. 学会把标题、分类、日期、封面、正文段落组织成第一版详情页结构。
  9. 理解为什么正文第一版先用string[]渲染是一个适合当前阶段的折中方案。
  10. 知道generateStaticParams在本地内容型博客里可以先怎么理解。
  11. 理解详情页和列表页的职责区别。
  12. 为下一篇继续进入关于页、联系页与表单交互做好准备。

一、这一篇到底要打通哪条阅读主线

上一章,我们已经把:

  • 数据
  • 首页
  • 列表页

这条主线接起来了。

这一章要接的,是博客项目里另一条同样关键的主线:

阅读主线

它可以先被理解成这样一条路径:

  1. 用户在首页或列表页看到某篇文章。
  2. 用户点击这篇文章的标题或“阅读全文”入口。
  3. 浏览器进入这篇文章的详情地址。
  4. 页面根据slug找到对应文章。
  5. 页面把这篇文章的完整内容渲染出来。

这条主线一旦成立,博客项目就会从:

有内容入口的网站

继续走向:

有完整阅读路径的网站

二、为什么文章详情页在博客项目里这么关键

很多内容型网站真正最核心的价值,往往不在首页。

首页更像:

入口

文章列表页更像:

总览

而文章详情页更像:

真正承载内容价值的地方

1. 列表页负责“让你看见”

它会告诉用户:

  • 有哪些内容
  • 这些内容大概是什么
  • 哪篇文章值得点进去

2. 详情页负责“让你读进去”

它会真正承载:

  • 标题
  • 摘要
  • 正文
  • 阅读节奏
  • 深入理解

3. 为什么博客项目不能没有它

因为如果没有详情页,博客项目很容易停留在:

一组文章卡片展示页

而不是一个真正的内容网站。

三、动态路由参数到底在解决什么问题

这一节非常重要。

很多初学者第一次看到:

[slug]

时,会觉得它只是个“文件夹写法”。

其实它解决的是一个很实在的问题:

多篇文章如何共用一套详情页结构,但又显示不同内容?

1. 如果不用动态路由会怎样

你可能就要为每篇文章都写一份页面文件。

例如:

  • react-basic/page.tsx
  • html-semantic/page.tsx
  • typescript-generics/page.tsx

这显然既重复,也很难维护。

2. 动态路由在做什么

它的核心作用是:

用一个“变量位置”去匹配不同文章地址。

例如:

/posts/react-basic /posts/html-semantic-basics /posts/typescript-generics

这些地址虽然最后一段不同,但它们都可以共用同一套详情页模板。

3. 当前阶段最值得先记住什么

你可以先记住一句话:

动态路由参数解决的不是“怎么写目录更炫”,而是“怎么让一类内容共用一套页面结构”。

四、先把目录结构和地址关系再看一遍

当前阶段博客详情页最关键的目录关系,是这一段:

src/app/ └── posts/ ├── page.tsx └── [slug]/ └── page.tsx

它可以先这样理解:

  • src/app/posts/page.tsx对应文章列表页/posts
  • src/app/posts/[slug]/page.tsx对应文章详情页/posts/某篇文章标识

例如:

/posts/html-semantic-basics /posts/react-state-basics /posts/nextjs-routing-introduction

1. 为什么这个结构很适合当前博客项目

因为它正好对应了两类页面:

  1. 一组文章的总览页
  2. 单篇文章的详情页

2. 这和上一章的数据设计为什么刚好能接上

因为我们上一章已经给每篇文章准备了:

slug

所以现在路由层和数据层已经开始自然对上了。

五、params.slug可以先怎么理解

当用户访问:

/posts/html-semantic-basics

时,详情页可以拿到这样一份参数:

params.slug==="html-semantic-basics"

你可以先把它理解成:

当前 URL 里,属于文章标识的那一段内容。

1. 为什么这个值非常关键

因为它就是详情页去查文章的钥匙。

详情页不是随便显示一篇文章,而是要根据:

当前访问的是哪一个slug

来决定显示哪一篇。

2. 不同版本的 Next.js 看起来会不会略有差异

会有可能。

例如某些版本在类型提示上会让params的写法稍微不一样。

但当前阶段最重要的核心理解并不会变:

详情页会拿到一个slug,再用这个slug去找到对应文章。

只要这条主线你抓住了,后面版本差异就不会太难适应。

六、详情页第一版至少应该展示哪些内容

在开始写页面前,先别急着立刻敲代码。

先想清楚:

第一版详情页最值得先展示什么。

当前阶段我建议至少先包含下面这些内容:

  1. 返回文章列表入口
  2. 文章分类和发布日期
  3. 文章标题
  4. 文章摘要
  5. 文章封面图
  6. 文章正文段落

1. 为什么这几个部分已经很够用了

因为它们已经能撑起一条完整阅读主线:

  • 用户知道自己在哪
  • 用户知道这篇文章是什么
  • 用户能看到正文

2. 当前阶段先不必急着加什么

例如:

  • 评论
  • 目录锚点
  • 上一篇 / 下一篇
  • 阅读时长
  • 代码高亮

这些后面都可以扩展。

现在更重要的是:

阅读主线先成立。

七、为什么getPostBySlug是这一篇的核心桥梁

上一章我们已经在:

src/lib/posts.ts

准备了这个函数:

exportfunctiongetPostBySlug(slug:string){returnblogPostList.find(function(postItem){returnpostItem.slug===slug;});}

这一章它会成为真正的关键桥梁。

为什么?

因为它刚好把两层内容连起来了:

  1. 路由层拿到的slug
  2. 数据层维护的文章数组

1. 如果没有这个函数会怎样

页面里就会开始自己写很多:

  • find
  • 判空
  • 字段处理

这会让页面越来越像数据脚本。

2. 现在把它单独抽出来的价值是什么

它让详情页可以更专注地表达:

当前页面怎样展示当前文章

而不是一边写页面,一边到处散落数据查找逻辑。

八、先从最小的详情页查找逻辑开始

在真正写完整详情页之前,我们可以先把最小逻辑跑通。

当前页面文件是:

src/app/posts/[slug]/page.tsx

它最小可以先写成这样:

import { getPostBySlug } from "@/lib/posts"; interface PostDetailPageProps { params: { slug: string; }; } export default function PostDetailPage({ params }: PostDetailPageProps) { const post = getPostBySlug(params.slug); return <section>{post?.title}</section>; }

1. 这一段代码在做什么

它只做了两件事:

  1. 拿到当前访问地址里的slug
  2. slug去文章数组里找对应内容

2. 为什么要先跑最小逻辑

因为这样你可以先确认:

路由参数到底有没有真的和文章数据接上。

这一步跑通以后,再补页面结构会稳很多。

九、文章找不到时,为什么更适合 404,而不是普通空状态

这一点非常值得现在就建立。

列表页和详情页虽然都可能“没有内容”,但它们表达的问题通常不同。

1. 列表页为空,更适合空状态

因为它往往在表达:

当前还没有任何文章,或者当前筛选结果为空。

2. 详情页找不到文章,更适合 404

因为它往往在表达:

用户访问了一个本来应该存在、但当前并不存在的具体地址。

3. 为什么这两者不要混为一谈

因为它们背后的场景完全不同。

当前阶段更稳的理解是:

  • 列表页“没有一组内容”更像空状态
  • 详情页“没有这一个具体对象”更像 404

十、先用notFound()处理不存在文章

Next.js 提供了一个非常适合当前场景的能力:

notFound()

你可以先把它理解成:

当当前详情页对应的文章不存在时,直接交给 Next.js 走 404 页面流程。

一个很典型的写法是:

import { notFound } from "next/navigation"; import { getPostBySlug } from "@/lib/posts"; interface PostDetailPageProps { params: { slug: string; }; } export default function PostDetailPage({ params }: PostDetailPageProps) { const post = getPostBySlug(params.slug); if (!post) { notFound(); } return <section>{post.title}</section>; }

1. 为什么这种处理方式很合适

因为它更符合详情页的语义:

这不是“当前没有内容可以展示”,而是“你访问的这篇文章不存在”。

2. 后面还能不能做更友好的 404 页面

当然可以。

后面你完全可以继续补:

src/app/not-found.tsx

让 404 页面更友好。

但当前阶段先把:

找不到文章 -> 正确走 404

这条主线跑通,就已经很有价值了。

十一、第一版详情页完整结构可以怎么写

当最小查找逻辑和 404 主线都成立以后,就可以开始把详情页真正写完整了。

一个适合当前阶段的第一版详情页,可以先写成下面这样:

import Image from "next/image"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getPostBySlug, getSortedPostList } from "@/lib/posts"; interface PostDetailPageProps { params: { slug: string; }; } // 这里提前把所有本地文章的 slug 告诉 Next.js,方便当前阶段的内容型路由更稳地工作 export function generateStaticParams() { return getSortedPostList().map(function (postItem) { return { slug: postItem.slug }; }); } export default function PostDetailPage({ params }: PostDetailPageProps) { const post = getPostBySlug(params.slug); // 当前 slug 找不到文章时,直接交给 Next.js 走 404 流程 if (!post) { notFound(); } return ( <article className="site-shell post-detail-page"> <Link href="/posts" className="back-link"> 返回文章列表 </Link> <p className="post-detail-meta"> {post.category} · {post.publishedAt} </p> <h1>{post.title}</h1> <p className="post-detail-summary">{post.summary}</p> <Image src={post.coverImage} alt={`${post.title} 的封面图`} width={1200} height={630} /> <div className="post-content"> {/* 这里把正文数组稳定渲染成段落列表,先让阅读主线跑通 */} {post.content.map(function (paragraph, index) { return <p key={`${post.slug}-${index}`}>{paragraph}</p>; })} </div> </article> ); }

1. 为什么这份结构已经很够用了

因为它已经把当前阶段最关键的阅读元素都接起来了:

  • 返回入口
  • 元信息
  • 标题
  • 摘要
  • 封面图
  • 正文内容

2. 这一整段代码最关键的主线是什么

你可以把它压缩理解成:

路由参数拿到slug-> 用slug找文章 -> 找不到就 404 -> 找到了就渲染整篇文章

这就是本章最核心的主线。

十二、为什么generateStaticParams值得现在就加上

这一节对当前这种“本地内容型博客项目”来说很有价值。

你可以先把:

generateStaticParams

理解成:

先把所有已知文章的slug告诉 Next.js。

1. 这在当前项目里为什么特别自然

因为我们现在的文章数据是本地可知的。

也就是说:

  • 有哪些文章
  • 每篇文章的slug是什么

在项目构建时就已经能知道。

2. 它和详情页是什么关系

它们的关系很简单:

  1. generateStaticParams()先给出一组可用slug
  2. 详情页再根据这些slug渲染不同页面

3. 当前阶段如果你一时没完全吃透怎么办

也完全正常。

你先记住这句就够了:

对本地内容型页面来说,generateStaticParams可以帮助 Next.js 提前知道会有哪些详情页地址。

十三、为什么正文第一版先用string[]渲染就够用了

这一点我们上一章其实已经埋过伏笔。

但到了详情页,这件事会真正落地。

1. 为什么它对当前阶段特别合适

因为它足够简单,同时又足够有用。

你可以很自然地做这件事:

一个字符串数组项,渲染成一个正文段落。

例如:

content:["HTML 标签不只是长得不一样,它们本身就在表达结构。","当你理解了结构,页面和内容的关系就会更清楚。"]

然后详情页里直接:

{post.content.map(function (paragraph) { return <p>{paragraph}</p>; })}

2. 这样做的核心价值是什么

它能让你先把:

  • 正文来源
  • 正文渲染
  • 正文结构

这三件事跑顺。

3. 现在为什么还不急着上 Markdown

不是 Markdown 不重要,而是当前阶段更重要的是:

先把阅读主线跑通。

如果你一上来就把重点切到 Markdown 解析、插件配置、样式适配,反而容易把详情页主线打散。

十四、详情页里的元信息在解决什么问题

元信息虽然看起来只是几行小字,但它对阅读体验很重要。

例如:

  • 分类
  • 日期

这类内容会让用户更快知道:

  1. 这篇文章大概属于哪个主题
  2. 它是什么时候发布的

1. 为什么这不是装饰

因为内容型页面里,读者经常会先判断:

  • 主题是否相关
  • 时间是否新近

这些信息能帮助用户快速建立阅读预期。

2. 当前阶段元信息先做到什么程度就够了

先做到:

  • 分类
  • 日期

这两个基础项就已经很有价值。

后面可以再逐步扩展:

  • 阅读时长
  • 标签
  • 作者

十五、返回列表入口为什么现在就值得加上

很多初学者写详情页时,会先把返回入口忘掉。

但从真实阅读路径看,这个入口非常自然。

1. 它解决什么问题

它解决的是:

用户看完一篇文章后,怎么方便地回到文章总览页。

2. 为什么它很适合现在就加

因为它既简单,又很符合真实使用路径。

你只要先在详情页顶部加一个:

<Link href="/posts">返回文章列表</Link>

阅读路径就会顺很多。

3. 当前阶段先不用把它做得太花

不需要一开始就:

  • 带图标
  • 带复杂悬停动画
  • 带面包屑导航

先把阅读路径理顺最重要。

十六、详情页和列表页的职责为什么不能混掉

这一点在博客项目里非常关键。

1. 列表页更像什么

更像:

内容总览页

它的重点是:

  • 看有哪些内容
  • 快速浏览摘要
  • 决定下一步点哪篇

2. 详情页更像什么

更像:

内容沉浸页

它的重点是:

  • 读完整内容
  • 理解一篇文章的展开逻辑
  • 获得完整信息

3. 为什么这两个角色一定要分开

因为如果详情页还像列表页一样只讲摘要,或者列表页像详情页一样塞满正文,整个阅读结构就会混乱。

当前阶段更稳的理解是:

  • 列表页负责帮你选
  • 详情页负责帮你读

十七、第一版详情页样式应该先追求什么

这章的重点虽然不是样式,但详情页一旦开始显示正文,页面节奏就已经开始变重要了。

当前阶段我建议第一版详情页样式先追求三件事:

  1. 文字容易读
  2. 信息层级清楚
  3. 内容宽度舒服

1. 什么叫“文字容易读”

例如:

  • 段落不要太挤
  • 行高不要太小
  • 正文宽度不要过长

2. 什么叫“层级清楚”

例如:

  • 标题最突出
  • 元信息更轻
  • 摘要和正文有明确区分

3. 当前阶段不必急着做到什么程度

不必一开始就追求:

  • 杂志级排版
  • 很复杂的目录锚点
  • 丰富的富文本样式系统

现在先让:

阅读舒服、主次分明

就已经很有价值。

十八、这一章先不急着做哪些细节

虽然详情页现在已经开始真正成立,但依然有很多内容可以先放到后面。

例如:

  1. Markdown / MDX 渲染
  2. 代码高亮
  3. 目录导航
  4. 上一篇 / 下一篇
  5. 相关文章推荐
  6. 评论系统

1. 为什么现在先不急

因为这一章真正要解决的是:

详情页能不能根据slug稳定找到文章,并把正文稳稳渲染出来。

只要这条主线成立了,后面的增强能力就都有了落点。

2. 当前阶段最稳的节奏是什么

先跑通:

  • 动态路由参数
  • 单篇文章查找
  • 404 处理
  • 正文渲染

再继续往上叠:

  • 样式优化
  • 阅读体验增强
  • 内容管理升级

十九、开发后怎么验证这一篇有没有搭好

这一篇完成后,非常适合做一轮小验证。

你可以按下面顺序检查。

1. 先检查一篇存在的文章

例如访问:

/posts/html-semantic-basics

确认:

  1. 页面能正常打开
  2. 标题和摘要正确显示
  3. 正文段落正确显示
  4. 返回列表入口可点击

2. 再检查另一篇存在的文章

例如访问:

/posts/react-state-basics

确认:

页面内容确实会随着slug变化。

3. 再检查一个不存在的地址

例如访问:

/posts/not-exist-post

确认:

页面不是静默空白,而是走到了 404 流程。

4. 为什么这种验证特别重要

因为它能直接帮你确认:

详情页最关键的“存在时可读、不存在时有反馈”两条主线,是否都成立了。

二十、这一章最容易踩的几个坑

这一节建议你认真看。

因为详情页阶段很容易出现一些“现在能跑、后面很难改”的问题。

1. 坑一:详情页不读slug,而是直接写死某篇文章

这样页面看起来能打开,但其实动态路由根本没有真正成立。

2. 坑二:找不到文章时什么都不处理

这样用户访问错误地址时,页面可能是:

  • 空白
  • 报错
  • 显示半截结构

这都不够稳。

3. 坑三:正文和摘要混成一层

这样详情页就会很像放大版列表卡片,而不是一个真正的阅读页。

4. 坑四:正文数据结构还没稳,就急着做复杂富文本系统

这会让你过早把注意力从:

阅读主线

转移到:

内容系统技术细节

5. 坑五:详情页没有返回路径

这样用户看完一篇文章后,路径会有点断。

6. 坑六:不同文章的slug命名不统一

这样后面数据、图片、链接和路由很容易对不上。

二十一、本章实践练习

这一章的练习重点,是把“动态路由详情页”这条主线真正跑顺。

1. 练习 1:完成详情页动态路由页面

请你完成:

src/app/posts/[slug]/page.tsx

并做到:

  1. 能读取params.slug
  2. 能通过getPostBySlug找到对应文章
  3. 能把文章标题渲染出来

这个练习的重点是:

让路由参数和数据查找真正接上。

2. 练习 2:给不存在的文章加上 404 处理

请你在详情页里补上:

notFound()

并测试一个不存在的地址。

这个练习会帮助你真正理解:

详情页找不到内容时,更适合走 404 语义。

3. 练习 3:把正文段落真正渲染出来

请你让详情页至少展示:

  1. 标题
  2. 分类与日期
  3. 摘要
  4. 正文段落列表

这个练习的重点是:

让详情页真正开始承担“阅读页”的角色。

4. 练习 4:加上返回列表入口

请你为详情页补一个:

<Link href="/posts">返回文章列表</Link>

这个练习会帮助你真正提升:

阅读路径的完整度。

二十二、学习重点提示

这一章请你重点记住下面这些话:

  1. 文章详情页是博客项目真正承载内容价值的关键页面之一。
  2. 动态路由参数的核心价值,是让一类内容共用一套详情页结构。
  3. slug是路由层和数据层之间最重要的连接点。
  4. 列表页更像内容总览页,详情页更像内容沉浸页,两者职责不能混掉。
  5. 详情页文章不存在时,更适合走 404 语义,而不是普通空状态。
  6. notFound()是当前阶段处理不存在详情内容的很稳方案。
  7. 正文第一版先用string[]渲染,是一个简单但有效的起点。
  8. 当前阶段最重要的不是立刻做复杂内容系统,而是先把详情页阅读主线跑通。

如果你只记一句话,请记住:

第五阶段综合实战真正开始具备“可阅读性”的那一刻,往往不是你加了多少高级功能,而是你先把slug、文章查找、404 处理和正文渲染这条详情页主线稳定地跑通了。

二十三、本章小结

这一章,我们正式把第五阶段综合实战从“首页和列表页内容接入”推进到了“详情阅读主线真正成立”的阶段。

你已经理解了:

  • 为什么文章详情页在博客项目里这么关键
  • 动态路由参数到底在解决什么问题
  • params.slug、文章数据和getPostBySlug之间怎样真正串起来
  • 为什么文章不存在时更适合用 404 处理
  • notFound()在当前阶段可以怎样使用
  • 第一版详情页最值得先展示哪些内容
  • 为什么正文第一版先用string[]渲染就已经很有价值
  • generateStaticParams在本地内容型项目里可以先怎样理解

更重要的是,你开始真正建立一种非常关键的阅读型项目意识:

内容网站真正成立,不只是因为有内容入口,还因为它能把用户顺利带进一篇文章、稳稳读完整篇文章。

这一步非常关键。

因为从这里开始,你已经不只是在搭内容展示网站,而是在开始真正进入:

个人博客项目第一版完整阅读路径的实现阶段。

二十四、课后思考题

请你认真思考下面这些问题:

  1. 为什么动态路由特别适合博客文章详情页?
  2. 为什么slug是详情页里最关键的连接点之一?
  3. 为什么详情页找不到文章时,更适合走 404,而不是普通空状态?
  4. 为什么正文第一版先用string[]渲染,是一个适合当前阶段的折中方案?
  5. 为什么详情页和列表页的职责一定要分开?
  6. 为什么generateStaticParams对本地内容型项目尤其自然?
  7. 你觉得详情页里最容易在一开始被忽略的细节是什么?为什么?

建议你把这些问题用自己的话写下来。

只要你能把这些问题讲清楚,说明你已经真正开始进入第五阶段综合实战的详情阅读主线了。

二十五、下一篇预告

下一篇我们会继续推进第五阶段综合实战,进入:

第五阶段综合实战:关于页、联系页与表单提交流程

到那时,你会开始真正把博客项目从“能读内容”继续推进到:

  • 能更清楚地介绍作者和站点
  • 能让别人和你建立联系
  • 能跑通最基础的表单交互与提交反馈

也就是说,下一篇开始,我们会从“文章详情页、动态路由参数与正文渲染”继续走到:

个人博客项目第一版站点完整度和联系交互主线的实现阶段。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询