这是散篇内容层的入口。首页、标签页、文章页几乎都通过它拿数据,而不是自己 fs.readFile。
系列小册走平行的 lib/series.ts(见本册其它章与源码),但字数估算与目录抽取与散篇共用同一套函数,保证行为一致。
类型:列表薄、详情厚§
export type TocItem = { id: string; text: string; depth: 2 | 3 | 4 };
export type PostMeta = {
slug: string; // 路径名:文件名去掉 .mdx
title: string;
description: string;
date: string;
tags: string[];
minutes: number; // 估读分钟
words: number; // 字词量(中文按字、英文按词)
issue: string; // 刊号字符串,如 "004"
};
export type Post = PostMeta & { content: string; toc: TocItem[] };首页、标签页只需要 PostMeta;文章页才需要 content 和 toc。toMeta 会显式丢掉正文,避免把整篇 MDX 当 props 传进列表。标签聚合(含系列)见 lib/tags.ts。
阅读时长怎么估§
export function readingStats(source: string) {
const cjk = source.match(/[\u3400-\u9fff\uf900-\ufaff]/g)?.length ?? 0;
const latin = source.match(/[A-Za-z0-9]+/g)?.length ?? 0;
const words = cjk + latin;
const minutes = Math.max(1, Math.round(cjk / 400 + latin / 200));
return { words, minutes };
}中文按「字」、拉丁按「连续单词」计数;阅读速度按约 400 汉字/分、200 英文词/分估算,至少显示 1 分钟。这是启发式,不是精确计时。
文章目录为何单独抽一遍 AST§
侧栏目录需要 id,正文标题也需要同一个 id(由 rehype-slug 写入)。两边必须用同一套 slug 算法,否则点击目录会跳错。
extractToc 用 unified + remark-parse + remark-gfm 解析正文,再 visit 所有 2~4 级标题,用 GithubSlugger 生成 id。这与 GitHub / rehype-slug 的常见行为对齐。
注意:这里只为了抽目录,并不做完整 MDX 编译;真正的组件化渲染在 MdxContent。
getAllPosts:扫盘、过滤、排序、编号§
核心流程:
- 读
content/posts下所有.mdx gray-matter拆开文首 YAML 与正文draft: true→ 跳过;非草稿若缺title/date/description→ 构建失败(lib/content-validate.ts)- 按
date新文在前排序 - 给每篇赋
issue:最旧的一篇是001,最新的是最大号——实现上是posts.length - i再padStart(3, "0") - 结果放进模块级
cache;仅 production 复用(lib/content-cache.ts)。开发态每次请求重新扫盘,改 MDX 立即生效
周边 API§
| 函数 | 用途 |
|---|---|
getPostBySlug | 文章页取单篇 |
getPostSurroundings | 上一篇 / 下一篇。数组是新→旧,所以 next 是更靠前的新文,prev 是更旧的一篇 |
toMeta | Post → PostMeta |
getPostSurroundings 的命名容易和「时间上一篇」搞混:这里的「上一篇 / 下一篇」是按站点列表顺序(新文在前)做的阅读导航,不是按写作日历的「更早一天」。
作者侧约定(content/posts/)§
文首必须类似:
title: "标题"
description: "列表与 SEO 用的一句话"
date: "2026-10-01"
tags: [架构, Next.js]
draft: false文件名里的 001- 只是命名习惯,真正的路径名是完整文件名去掉后缀;刊号 issue 由日期排序计算,不读文件名里的数字。
readingStats 与 extractToc 已导出,供 lib/series.ts 直接复用——系列章节的估读与侧栏目录算法与散篇完全一致。
下一章专门讲系列小册的内容模型、series-path 拆分,以及章节页三栏如何组装。之后再进入共用的 MDX 编译链。