首页›文章›Next.js 内容站构建:内容与配置分离的架构实践

Next.js 内容站构建:内容与配置分离的架构实践

计算机技术 · web 开发2026-09-194次浏览0 个评论

本篇是《手写一个内容型技术博客站》系列第 2 篇。上一篇聊了技术选型,这一篇讲内容层的架构核心:配置与内容分离。

很多内容站做大了以后会遇到两个问题:一是编辑改一篇文章要小心翼翼,生怕碰到配置;二是分类的排序、别名、SEO 关键字散落在代码里,改一处要 grep 全站。jiusi-notes 在 v3.25.0 做了一次重构,把这两类问题一起解决:所有元数据配置集中到 src/config/*.yaml,内容纯 Markdown 放在 src/content/,两者通过一个通用加载器对接。

这篇讲清楚三件事:分层的动机、加载器的实现、重构踩过的坑。

一、为什么要把配置和内容分开

早期版本沿用很多静态站的默认做法:配置文件和内容放在一起。比如这样:

text
src/content/categories/区块链/config.yaml   # 分类配置
src/content/categories/区块链/erc20.md      # 文章正文

问题在"配置"和"内容"是两个生命周期完全不同的东西:

维度配置(config)内容(content)
变更频率低,偶尔调整排序/关键词高,日常写作
变更影响面全局(导航/SEO/路由)单篇
出错后果可能整站崩只影响一篇文章

当编辑(或用户自己)往 categories 里新增一篇文章时,他大概率只关心"文章能不能发出去",不应该被旁边的 config.yaml 牵制。而配置一旦被内容操作误伤——比如一次批量脚本把 categories/config.yaml 动了——可能就是全站导航错乱。

更实际的一个痛点:排序、别名、SEO 关键字这些"元数据",天然应该是结构化的、可被程序校验的,而不是散落在内容文件的 frontmatter 里、或者硬编码在代码里。

v3.25.0 之后的边界非常清晰:

text
src/config/    ← 7 个 YAML:site/categories/pages/tutorials/hero/comments/albums
src/content/   ← 纯 Markdown 仓库:categories/pages/tutorials 三个目录

配置怎么改都不碰内容,内容怎么加都不动配置。

二、配置驱动元数据:categories.yaml 设计

以分类为例。分类是内容站的骨架:导航、文章列表、SEO 都依赖它。这份配置要承载的可不只是"名字"。

yaml
# src/config/categories.yaml
categories:
  - name: 基础理论
    order: 10
    slug: theory
    description: 计算机基础、算法、数据结构、网络原理
    keywords: [算法, 数据结构]
    subcategories: []

  - name: 计算机技术
    order: 20
    slug: computer-technology
    subcategories:
      - name: 操作系统
        slug: os
        order: 10
      - name: 软件工程
        slug: software-engineering
        order: 40

几个字段的设计意图:

  • order:展示排序。没配的自动排最后(用 ?? 9999 兜底,见下)。
  • slug:路由标识。覆盖默认的 slugify 结果——比如"基础理论"默认拼音是 jichulilun,但我想让它显示成 theory,就在这里配。这是"配置覆盖约定"(convention-over-configuration)的关键:有配置用配置,没配置用约定。
  • description / keywords:喂给 SEO。
  • subcategories:二级分类内嵌。v3.13 起的推荐写法,直接缩进关联到所属一级分类,比旧的顶层 Record<"L1/L2", ...> 清晰得多。

三、通用加载器:src/lib/content-config.ts

配置是外部的 YAML,代码怎么读它?核心是一个通用函数:

ts
// src/lib/content-config.ts
import fs from "fs";
import path from "path";
import { load } from "js-yaml";

const CONFIG_ROOT = () => path.resolve(process.cwd(), "src/config");

/** 通用 YAML 读取:config 目录下的单文件,如 categories.yaml */
function loadYaml(configFile: string): Record<string, unknown> | null {
  const configPath = path.resolve(CONFIG_ROOT(), configFile);
  if (!fs.existsSync(configPath)) return null;
  try {
    const content = fs.readFileSync(configPath, "utf-8");
    return load(content) as Record<string, unknown>;
  } catch {
    return null;
  }
}

然后针对每种配置导出强类型的加载函数:

ts
export function loadCategoriesConfig(): CategoriesConfig {
  const data = loadYaml("categories.yaml");
  return {
    categories: (data?.categories as CategoryConfig[]) || [],
    subcategories: (data?.subcategories as Record<string, SubcategoryConfig>) || undefined,
  };
}

几个要点:

  1. 统一入口。调用方只 import loadSiteConfig() / loadCategoriesConfig(),绝不直接碰 src/config/xxx.yaml 路径。这样以后目录再变,只改加载器一处。
  2. 容错。loadYaml 文件不存在返回 null,解析失败返回 null,调用方用 || [] / || {} 兜底。配置坏了不应该让整站 500,降级到空值是内容站的底线。
  3. 类型化。每个配置都有对应的 TS 接口(SiteConfig / CategoriesConfig / TutorialsConfig …),YAML 的弱类型在边界上被收口成强类型。

加载器最终把"磁盘上的 YAML"和"代码里的强类型"对接起来,页面层拿到的都是干净的配置对象。

四、order 排序的实现:?? 9999 兜底

排序是整个配置体系里最容易出 bug 的地方。一级分类和二级分类都要排:

ts
// categories.ts 内部
const withOrder = (children, lookup) =>
  children.map((node) => {
    const cfg = lookup.get(`${node.name}/${node.name}`) /* ... */;
    return node;
  });

// 关键:未配置 order 的一律排最后
children.sort((a, b) =>
  (subcatByKey.get(`${node.name}/${a.name}`)?.order ?? 9999) -
  (subcatByKey.get(`${node.name}/${b.name}`)?.order ?? 9999)
);

这个 ?? 9999 很关键:新分类忘了配 order,不会挤到最前面,而是安静地排到最后。这比"没配就报错"对编辑友好得多——配置是渐进完善的,不是一次全配齐。

踩坑记录:v3.24 之前,二级分类从不按配置的 order 排序。applyCategoryConfig 只对一级分类做了 sort,二级分类的 children 原样保留了 readdirSync 的文件系统顺序(安全→软件工程→python→web 开发这种乱序)。因为"文件系统顺序"在开发机上是稳定的,问题被掩盖了很久。后来新增分类才发现排序是错的。修法:children.map 之后补一个按子分类 order 的 sort。这类 bug 的教训是:配置里写了 order,代码就一定要真的消费它,别假设文件系统顺序等于配置顺序。

五、重构踩过的坑(v3.25.0 实修)

这次重构看起来是"搬文件 + 加加载器",但牵扯到几个容易漏的连带点:

坑1:签名缓存必须纳入配置文件

内容树有 computeContentSignature 做签名缓存——通过比对文件 mtime/size 判断内容有没有变,没变就用缓存,避免每次请求重建整棵内容树(文章多的时候有性能意义)。

迁移后配置文件路径变了,但签名函数只监听了 src/content/ 的 md/mdx。结果:改了 categories.yaml 的 order,签名没变,缓存不重建,排序不生效。**

修法:签名额外纳入 src/config/categories.yaml 的 stat。

ts
const sigCat = statSync(pathToConfigCategories); // 加这一行
// computeContentSignature: [ ...contentStats, sigCat ]

这类 bug 极其隐蔽——配置改了、代码没崩、页面用旧数据。验证方法:改完 order 起服务看导航顺序变没变。

坑2:磁盘目录和工作区的孤儿检测

配置和内容分开后,出现了一个新的不一致形态:磁盘有目录、config 没注册,或 config 注册了、磁盘目录名对不上。后果是文章被静默忽略(列表页不显示、SEO 失效),但 build 不报错。

这个靠 preflight 校验脚本兜底(本系列后面有专门一篇讲),核心逻辑:

ts
// preflight-check.mjs 第 4 步:目录与 config 孤儿检查
// 遍历磁盘目录 vs config 注册的分类名,两边对不上就 WARN

模块化是一个不断"把隐式约定变成显式校验"的过程。目录和配置的一致性,从"靠人记住"变成了"脚本检查"。

坑3:旧 config 路径的残留引用

迁移后 src/content/xxx/config.yaml 的老路径还散落在注释、README、甚至个别代码里。grep 全站 src/content/.*config\.yaml 逐处清掉。这个比不上前两个"功能性"坑,但会误导后来维护的人——注释里写错的路径比没有注释更危险。

六、这套架构的收益(以真实数据结算)

写这篇文章时站点实际状态:

  • 7 个配置文件(site/categories/pages/tutorials/hero/comments/albums),合计约 500 行 YAML
  • 200+ 篇 Markdown 内容(文章 + 教程 + 静态页)
  • 编辑日常操作:往对应分类目录丢一个 .md,preflight-check.mjs 校验通过即可;不需要碰任何配置
  • 调整站点名称 / 排序 / SEO 关键字:改对应 YAML,auto_rebuild.py 检测到变化自动触发重建

内容与配置分离带来的最大改变是操作边界清晰:写内容的人不需要理解配置,调配置的人不需要关心内容。加上 preflight 校验把"静默错误"提前暴露,站点从"能跑"进化到"可维护"。


给普通人的一句话:把"经常变的"和"偶尔变的"分开管理,是让一个项目长期不乱的基础——写代码是这样,做内容站也是。

参考

  • Next.js App Router 文档:https://nextjs.org/docs/app
  • js-yaml:https://github.com/nodeca/js-yaml

评论

0

评论加载中…

发表评论

0/2000