文章

内容规范化脚本怎么写:从 frontmatter 迁移到批量清洗

把旧文章迁到新项目时,如何设计一个可重复执行的内容规范化脚本:定字段、做映射、补缺失、删废字段,并保留 check 模式。

当一个内容站运行一段时间之后,文章字段几乎一定会慢慢失控。

最常见的情况不是“没有字段”,而是“字段越来越多,但并不统一”:

  • 老文章用 date
  • 新文章用 pubDate
  • 有的文件写 categories
  • 有的文件写 category
  • 有的文章带 permalink
  • 有的文章还保留着上一套系统遗留的 layoutslugarticleId

如果这时只是靠手改,短期能撑住,文件一多就会失去一致性。更稳的办法,是写一个专门的整理脚本,把旧内容批量迁到当前项目真正需要的字段上。

这类脚本没有统一的唯一名字,但常见叫法通常是这些:

  • 内容规范化脚本
  • frontmatter 迁移脚本
  • 内容迁移脚本
  • metadata normalization script
  • migration script

它本质上不是页面运行代码,而是一种维护工具。你可以把它理解成“给内容做数据清洗和结构迁移”的小程序。

它解决的到底是什么问题

这类脚本最适合处理三种情况:

  1. 从旧博客、旧静态站、旧 CMS 批量迁内容到新项目
  2. 同一个仓库里,历史文章 frontmatter 长期不统一
  3. 内容 schema 已经定下来了,但旧文件还停留在上一套结构

比如一个项目现在真正需要的字段只有:

title:
description:
pubDate:
category:
draft:

但旧文件里可能同时混着这些:

layout: post
date: 2024-08-21
permalink: /blog/example
categories: 随笔
tags: [示例]
articleId: example
publishedAt: 2024-08-21
storyDate: 2024-08-22

这时就不应该继续“兼容一切”,而应该明确一件事:

当前项目最终要认哪一套字段。

只要这件事确定了,后面的迁移脚本就有了目标。

通用流程其实很固定

无论你是在 Astro、Jekyll、Hugo、Eleventy,还是任意一个 Markdown 内容项目里,这类脚本通常都遵循同一个流程:

  1. 定目标字段
  2. 读取旧文件 frontmatter
  3. 把旧字段映射到新字段
  4. 补缺失值
  5. 删除废字段
  6. 批量写回
  7. 留一个 --check 或 dry-run 模式

真正重要的是第 1 步,而不是第 6 步。

如果目标字段没先定清楚,脚本只会把混乱自动化。

先定义“最终只保留什么”

迁移脚本最怕的是一边迁、一边犹豫。最稳的做法是先写下最终 schema。

例如:

const canonicalFields = ["title", "description", "pubDate", "category", "draft"];

这句话看起来简单,但它决定了后面所有行为:

  • 哪些字段必须留下
  • 哪些字段要被映射
  • 哪些字段应该删除
  • 哪些信息要从正文推断

一旦你已经明确当前项目不会再消费 layoutpermalinktagsarticleId 这类字段,就不要继续留着它们“以防万一”。

“暂时保留”通常只会让 schema 重新发散。

第二步是映射,而不是照搬

迁移脚本的关键不是复制字段,而是把旧字段折叠到新结构里。

比如:

  • datepublishedAt 统一折成 pubDate
  • categories 统一折成单一的 category
  • draft 不存在时,按当前目录或当前批次默认补上

一个很常见的映射函数会长这样:

function pickPubDate(data, file) {
  return data.pubDate
    ?? data.publishedAt
    ?? data.date
    ?? file.match(/\b(\d{4}-\d{2}-\d{2})\b/)?.[1];
}

这类函数的好处是很直接:

  • 先认新字段
  • 再向下兼容旧字段
  • 最后才用文件名兜底

顺序本身就是一条策略。

缺字段时,不要立刻手工补

批量迁移最耗人的地方,通常不是字段删除,而是缺失值。

尤其是 description

很多老文章会出现这几种情况:

  • 没有描述
  • 描述只有一个 ....
  • 描述是上一代系统留下的占位内容

这时脚本不应该停下来等人工,而应该有一个自动补值策略。最常见的办法有三种:

  1. 从正文第一段提取摘要
  2. 从标题生成一个简单描述
  3. 给出明显的占位值,后续人工再改

如果内容量大,第一种通常最实用。

比如:

function pickDescription(rawDescription, title, body) {
  const existing = sanitizeDescription(rawDescription);
  if (existing) return existing;

  const excerpt = bodyToPlainText(body).trim();
  return shortenText(excerpt || title, 72);
}

这样做的目的不是生成“完美摘要”,而是先让 schema 不缺字段,再决定哪些文章值得二次精修。

删除废字段,才算真正完成迁移

很多人写迁移脚本时,只做“新增字段”,不做“删除废字段”。

这会留下一个后遗症:文件看起来好像已经规范了,但旧字段还在,后面的人又会忍不住继续使用它们。

所以真正的迁移不只是:

  • 加上 pubDate
  • 加上 category

还应该包含:

  • 删掉 layout
  • 删掉 permalink
  • 删掉 categories
  • 删掉不再被当前项目消费的历史字段

脚本最后写回文件时,最稳的方式不是“在原 frontmatter 上修补”,而是直接按目标字段重建一份新的 frontmatter。

例如:

const nextFrontmatter = [
  "---",
  `title: ${quoteYamlString(title)}`,
  `description: ${quoteYamlString(description)}`,
  `pubDate: ${pubDate}`,
  `category: ${quoteYamlString(category)}`,
  "draft: true",
  "---",
].join("\n");

这比局部替换更可靠,因为你不会不小心漏掉旧字段。

一定要保留 --check

如果这类脚本只能“直接写”,后面维护会很难受。

真正适合长期保留的版本,一定要支持只检查、不写入,比如:

node scripts/normalize-content.mjs --check

它的作用至少有三个:

  • 看看当前仓库还有多少文件不符合规则
  • 在 CI 里阻止新旧格式混用
  • 验证脚本是否幂等,也就是重复执行后不应该继续产生变化

幂等很重要。

一个好的规范化脚本,第一次运行可能会改 500 篇文件;第二次运行应该尽量是 0 变更。

如果第二次还持续改,通常说明脚本本身的映射顺序有问题,或者它没有优先认已经规范好的字段。

这种脚本到底该删还是该留

这也是很多项目里会反复出现的问题。

答案很实际:

  • 如果这次迁移结束后,后面不会再导入旧内容,可以删
  • 如果后面还可能继续导入旧文章、旧草稿、旧 CMS 数据,建议保留
  • 如果你希望仓库更干净,但又不想彻底丢掉它,可以保留在 scripts/tools/ 下,明确标注它是维护脚本

判断标准不是“它现在会不会被页面 import”,而是:

以后是否还有重复整理内容的可能。

所以它通常有两种身份:

  • 一次性迁移工具
  • 可重复使用的整理工具

这两种都合理,取决于项目还会不会继续迁内容。

如果换一个平台,思路会变吗

基本不会。

你把这套思路搬到别的项目时,真正要替换的通常只有三样东西:

  1. 目标 schema
  2. 旧字段到新字段的映射规则
  3. 读写 frontmatter 的具体实现

但脚本骨架往往几乎不变:

  • 扫描文件
  • 读取 frontmatter
  • 转成对象
  • 计算标准化后的字段
  • 重建 frontmatter
  • 写回或只检查

也就是说,这类脚本的“平台差异”主要在输入输出,核心逻辑其实很稳定。

如果以后还要跟别人解释,这样说最清楚

一句比较准确的话可以是:

“这是一个内容迁移/规范化脚本,用来把旧 frontmatter 映射到当前 schema,自动补齐缺失字段,删除废字段,并支持 check 模式。”

这个说法有几个好处:

  • 它说明了这不是运行时代码
  • 它说明了目标是当前 schema,而不是无限兼容历史
  • 它说明了脚本既能批量写回,也能只做校验

如果要再短一点,可以直接说:

“这是一个 frontmatter migration script,不是站点功能代码,是内容维护工具。”

最后

内容项目里,代码经常比内容更容易保持整洁。

因为代码会被 lint、build、typecheck 反复约束;内容 frontmatter 如果没有明确规则,往往会在几年里悄悄长成另一套系统。

所以一旦你发现文章字段开始分叉,最省力的办法通常不是继续手改,而是尽快写一个规范化脚本,把当前项目真正需要的字段固定下来。

脚本不一定要永久保留,但“先定 schema,再做迁移,再保留 check 模式”这套方法,几乎可以复用到任何 Markdown 内容项目里。

相关文章

Astro 内容站:PageHero 组件与页面头部统一

把列表页、首页、系列页和详情页里重复的标题区抽成 PageHero,让返回链接、eyebrow、标题和状态文案共用一套骨架。

2026-04-16

Astro 内容站:PageSectionHeader 组件与区块标题复用

把 section 标题、补充说明、右侧动作和分组计数统一到一个组件里,让首页、归档和索引列表共享稳定的区块头结构。

2026-04-16

Astro 内容站:组件组合重构、重复代码清理与文档同步

一次收口页面骨架、分页辅助、概览卡和旧文档漂移,把页面组合整理成更稳定的单组件文件结构。

2026-04-16

Astro 内容站:SummaryStatGrid 组件与归档总览卡

把归档页里重复的统计卡结构抽成 SummaryStatGrid,为总条目、文章数、章节数和最近更新时间提供统一的数值卡组件。

2026-04-16