列表页可以共用卡片组件,详情页也有同样的问题。
当前站点至少有两种详情页:
- 文章详情页
- 系列章节详情页
两者正文不同,但外层需求很接近:
- 返回上一级
- 显示标题和摘要
- 显示日期、分类或章节编号
- 在需要时给出相关文章
- 在系列中提供上一章、下一章
于是当前仓库把这层外壳抽成了 src/components/articles/ArticleShell.astro。
ArticleShell 是纯展示层
这个组件不自己查数据,也不自己推导上下章,只接收整理好的输入:
type ArticleLink = {
href: string;
label: string;
meta?: string;
description?: string;
};
以及这些 props:
backLinkeyebrowtitlesummarymetaItemspreviousLinknextLinkrelatedHeadingrelatedItems
这一步把组件边界定得很准。它只负责“详情页该怎么长”,不负责“这些数据从哪来”。
文章详情页直接吃它
ArticleDetailPageView.astro 里,页面先完成这些工作:
render(article)拿到正文内容getRelatedArticles(...)找同分类相关文章
然后把已经准备好的数据喂给 ArticleShell:
<ArticleShell
backLink={{ href: getArticlesPath(), label: copy.articles.backToList }}
eyebrow={copy.articles.heading}
title={article.data.title}
summary={article.data.description}
metaItems={[formatContentDate(article.data.pubDate), article.data.category]}
relatedHeading={copy.articles.relatedHeading}
relatedItems={...}
>
<Content />
</ArticleShell>
这让文章详情页的页面组件只做“数据准备”,而不是在页面里重新写一遍外层结构。
这层头部结构现在又继续向上复用了 PageHero,所以 ArticleShell 自己不再内联写标题区骨架,而是专注在“详情页外壳”这件事上。
系列章节页用的是同一个壳
SeriesChapterPageView.astro 也复用 ArticleShell,只是喂进去的数据不同:
backLink返回系列目录页eyebrow显示系列标题metaItems显示章节编号和发布日期previousLink / nextLink来自getSeriesChapterContext(...)
也就是说,系列章节页和文章页的差异被压缩到了数据,而不是模板。
章节前后导航因此自然成立
系列页里最重要的外层能力,是上下章导航。当前实现没有把这段导航写死在章节页面里,而是让 ArticleShell 接受两个可选链接:
previousLink={...}
nextLink={...}
当它们存在时,组件自动渲染底部导航区;没有时就不显示。
这个设计很干净,因为:
- 文章详情页通常不需要前后章
- 系列章节页天然需要
- 组件本身不用知道“你现在是不是系列”
元信息也统一成一条流水线
ArticleShell 内部会把 metaItems 过滤并拼成一行:
const metaLine = metaItems.filter(Boolean).join(" · ");
所以不同详情页只要准备好自己的元信息即可:
- 文章页:日期 + 分类
- 章节页:章节编号 + 日期
这种小接口看起来简单,但正好把详情页之间重复又略有差异的地方压平了。
为什么要把“相关文章”也放进去
因为相关文章和上下章一样,都是详情页尾部的补充导航。它们不属于正文,也不该散落到页面模板里。放进同一个壳组件之后,详情页的结构就固定成了:
- 返回链接
- 标题区
- 正文区
- 上下文导航区
- 相关推荐区
这比在不同详情页各自长出一份“尾部结构”更稳定。
当前实现里,相关文章本身也已经切回统一卡片组件:
- 最多 4 条
- 桌面双列
- 移动端单列
所以 ArticleShell 现在既统一了详情页骨架,也统一了详情页尾部的导航语言。
当前做法的收益
现在详情页这条线已经被拆成两层:
- page view 负责取内容、找翻译、算导航、组数据
ArticleShell负责统一渲染详情页外壳
这样之后,无论你继续扩写文章,还是再加新的系列类型,都不需要重新发明一套详情页模板。