个人站的文章系统:frontmatter、栏目校验和静态生成

10 篇文章背后没有 CMS:一个 content/posts 目录、gray-matter、栏目枚举校验和构建时静态生成,坏数据会在 build 时直接报错。

  • #Next.js
  • #Markdown
  • #内容系统
本页目录

这个站有 10 篇文章,没有 CMS,没有数据库。文章就是 content/posts 下的 markdown,构建时读出来,生成静态页面。

这套东西不复杂,但有几个地方必须做对,否则坏数据会静默跑到线上。

目录与命名

content/posts/*.md,文件名就是 slug。_template.md 以下划线开头,不会被收录。详情页通过 slug 找文件。

frontmatter 只有五个字段:title、date、summary、category、tags。category 必须命中枚举,否则构建直接失败。

读取:三件必须做的事

  1. 过滤掉 _ 和 . 开头的文件。下划线是模板,点是 macOS 的 AppleDouble 残留(._foo.md)。之前没过滤,/articles 直接 500。
  2. 把 gray-matter 解析出来的 Date 规范成 YYYY-MM-DD。YAML 里的日期是 Date 对象,不处理会在排序和显示上出问题。
  3. 栏目校验。category 不在枚举里就抛错,并提示允许值。
function validateCategory(raw: unknown, slug: string) {
  if (typeof raw === "string" && CATEGORY_IDS.includes(raw)) return raw;
  throw new Error(
    `[posts] ${slug}.md 的 category 无效或缺失(得到:${String(raw)})`,
  );
}

构建时炸,比上线后 500 好。

渲染

react-markdown 加 remark-gfm,纯 JSX,没有 dangerouslySetInnerHTML。正文样式在 globals.css 的 .article-content,不用 prose。

文章详情页是 Server Component,generateStaticParams 在构建时生成全部 slug,generateMetadata 按文章生成 title、description、canonical。

排序与相关

getAllPosts 按日期倒序。getRelatedPosts 取同栏目、排除当前文章,最多两篇。栏目不只是标签,它决定详情页底部的推荐。

为什么不用 CMS

  • 纯静态,构建产物可以直接丢服务器
  • 文章在 git 里,改动有历史
  • 没有数据库和后台,攻击面小
  • 新文章就是新建一个 md 文件

代价是没有在线预览和草稿状态,写作要经过本地和 git。对我来说可以接受。

加一篇新文章

复制 _template.md,填 frontmatter,写正文,本地构建,部署。文件名即 slug,category 写错会当场报错。

学到的

内容系统最重要的不是功能多,是坏数据能被拦住。栏目枚举、日期规范、隐藏文件过滤,这三件事都不难,但漏一个就会在线上出问题。

10 篇文章不需要 CMS。等到需要多人协作、定时发布或草稿流,再考虑也不迟。

本文同步发布于公众号「傲慢鱼偏见」,欢迎关注。