暂不支持移动端访问

文章列表、分页与详情页数据流

1660 字 8 分钟
AI 摘要

从内容查询函数到 PostPage、PostCard 和文章详情页,拆解 AstroBlog 的文章数据流。

文章列表和文章详情虽然是两个路由,数据都来自同一套内容查询函数。正确的分层是:集合提供原始 entry,查询层负责公开性和排序,路由负责分页或 slug 查找,布局和组件负责显示。这样文章归档、分类、标签、搜索和上一篇/下一篇都可以复用相同的数据规则。

查询层#

src/utils/content-utils.ts 是文章数据的主要入口。getPublicPosts()getCollection("posts") 读取集合,排除草稿和 fixture,并通过模块级 Promise 缓存避免重复读取。getSortedPosts() 在公开文章上执行排序并补充前后文章字段,getSortedPostsList() 则只保留列表需要的 iddata,不把正文带到卡片层。

排序规则是业务规则,而不是组件样式:先比较 pinned,置顶文章排在前面;置顶状态相同时按 published 从新到旧;日期相同时按 order 从小到大。

TypeScript
const sorted = allBlogPosts.sort((a, b) => {
// 置顶优先级高于发布时间排序。
if (a.data.pinned && !b.data.pinned) return -1;
if (!a.data.pinned && b.data.pinned) return 1;
const dateA = new Date(a.data.published);
const dateB = new Date(b.data.published);
if (dateA.getTime() === dateB.getTime()) {
// `order` 确保同日发布的文章顺序稳定。
return (a.data.order ?? 0) - (b.data.order ?? 0);
}
return dateA > dateB ? -1 : 1;
});

函数随后把相邻 entry 的 idtitle 写入 prevSlugprevTitlenextSlugnextTitle。这些字段属于构建时派生数据,不应该写回 Markdown,也不应该由 PostCard 自己计算。

分页路由#

文章列表路由位于 src/pages/posts/[...page].astro。它在 getStaticPaths() 中取得排序后的文章,使用 Astro 的分页对象生成 /posts//posts/2/ 等静态页面。分页大小来自 siteConfig.pagination.postsPerPage,当前默认值为 10。路由只负责 page 参数、标题和布局,具体卡片由 src/components/layout/PostPage.astro 渲染。

PostPage.astro 将 page.data 映射为 PostCard 的 props:标题、标签、分类、发布时间、更新时间、封面、摘要、URL、草稿和置顶状态。前两张卡片使用 eager 加载,其他卡片 lazy 加载。列表没有数据时显示空状态,而不是让分页组件访问不存在的数组元素。

列表布局和瀑布流#

文章列表支持 list 和 grid 两种模式,默认值和列数来自 siteConfig.postListLayout,用户选择保存在 localStorage。小屏幕强制使用列表模式,避免三列卡片挤压正文。网格开启瀑布流时,PostPage.astro 的内联脚本读取每张卡片的实际高度,把卡片放入当前最短的列:

text
读取容器宽度和列数
计算单列宽度
按文档顺序读取卡片高度
放入当前高度最小的列
设置 absolute top/left 和容器高度

图片加载完成、窗口改变、布局切换和 Swup 替换后都要重新计算。脚本会在 astro:before-swap 取消旧的监听器,避免切换页面后同一个 resize 处理器叠加。瀑布流是客户端增强,不改变服务端输出;JavaScript 未执行时仍然有普通列表结构。

详情页#

src/pages/posts/[...slug].astro 根据 slug 找到 entry,并调用 render(entry) 得到正文组件和标题节点。详情页把数据交给 MainGridLayout.astroArticleHero.astroArticleContent.astro 和页脚组件。Hero 使用标题、日期、阅读时间和封面,Content 负责渲染 Markdown 结果,目录根据 headings 生成,评论组件根据 comment 字段决定是否挂载。

详情页还生成文章类型的 Open Graph 元数据、JSON-LD、canonical URL、上一篇/下一篇链接、相关文章和许可证信息。页面组件不应该重新读取 Frontmatter 文本,否则会绕过 Content Collections 的类型和 Markdown 管线。

列表和详情的边界#

列表页面只需要元数据,详情页面才需要正文。因此 getSortedPostsList() 明确删除 post.body,减少生成列表时的数据量;PostCard 不知道 Markdown 从哪里来,也不负责计算阅读时间。反过来,详情页不应该为了显示文章标题再调用一次列表查询。

如果新增一个字段,先判断它属于查询层还是显示层:影响排序、公开性和前后关系的字段改 content-utils.ts;只影响卡片的字段改 PostCard.astro 和列表 props;只影响正文 Hero 或 SEO 的字段改详情路由和文章组件。这样修改不会把业务规则散落到多个页面。

常见问题定位#

文章没有出现在列表:检查文件是否通过 posts loader、Frontmatter 日期是否有效、draft 是否为 true、是否位于 _fixtures。文章在列表但详情 404:检查 slug 是否来自 entry.id,以及动态路由的 getStaticPaths() 是否使用同一套查询。卡片高度错乱:检查图片 load 事件、瀑布流初始化和 Swup cleanup。上一篇/下一篇顺序不对:检查排序规则和同日期文章的 order,不要在详情模板里临时交换字段。

修改文章列表后运行:

Terminal windowpowershell
# 检查文章集合、分页路由和组件 props 类型。
pnpm check
# 重新生成所有静态文章页、分页页和 Pagefind 搜索索引。
pnpm build

构建会同时验证静态路由、文章渲染、图片处理和 Pagefind 索引,单独打开一张卡片不能证明分页和搜索已经正确。

归档、分类和标签复用查询层#

归档页面、分类页面和标签页面都从 getPublicPosts() 获取公开文章,再在各自页面按日期或 taxonomy 过滤。它们不应该自己实现一套“草稿判断”和排序,否则文章在列表可见、分类不可见的情况很快会出现。getCategoryList()getTagList() 使用同一套规范化函数,分类名称在生成 URL 前还要经过 getCategoryUrl() 编码。

搜索的输入来自 Pagefind 对构建后 HTML 的索引。列表组件只负责输出 data-pagefind-body 范围内的标题、摘要和正文链接,搜索结果不会重新调用内容集合。修改文章卡片的 Pagefind 标记时,要检查搜索是否把后台、导航或隐藏摘要误收录。

分页边界#

分页页数由公开文章数量和 postsPerPage 计算。新增文章后可能产生新的 /posts/2/ 或更高页码,旧页仍然保持静态输出。删除文章后,最后一页可能变为空,路由需要依照 Astro 分页生成结果处理,而不是手工保留一个不存在的页码。

文章列表中的链接必须通过 getPostUrlBySlug() 生成,不能直接拼接 entry.id。该工具负责尾斜杠、编码和基础路径,尤其是中文 slug、嵌套目录和部署在非根路径时。

[ 公告 ]

如果你喜欢,那么欢迎来到我的世界!

了解更多
[ 音乐 ]
封面

音乐

找不到相关结果。
[ 目录 ]
[ 全部文章 ]