RSS 生成管线

ContentIndex 插件 · cleanHtml 定制清洗 · follow.it 消费

动机

站点需要 RSS 来支撑两种消费场景:用户通过 RSS 阅读器订阅更新,以及 follow.it 定时抓取 RSS 触发邮件群发。Quartz v4 内置了 RSS 生成能力(ContentIndex 插件),但它的输出是「原样照搬页面 HTML」,不处理页内导航元素——这些元素在邮件客户端中毫无意义且干扰阅读。

因此需要一条定制的 RSS 管线:在 Quartz 内置流程中插入一步清洗,去除页面专属的锚点标记,输出干净的、适合邮件和 RSS 阅读器消费的全文内容。

架构总览

⛶ 全屏 Quartz 内置 — ContentIndex 插件 content/*.md Markdown + Frontmatter Transformers remark → rehype → hast ContentIndex Emitter (内置) ① toHtml() hast → 原始 HTML(含锚点 SVG) ★ ② cleanHtml() 定制 正则剥除 <a role="anchor"> SVG ③ escape XML 安全编码 public/index.xml 部署 & 消费 Vercel (HKG) 静态托管 · kb.wilsonhandbook.online RSS Reader Feedly · NetNewsWire · … follow.it 定时抓取 RSS → 邮件群发 订阅管理 · 退订 · 分析 📧 订阅者邮箱 HTML 邮件 · 正文 + 原文链接 deploy GET index.xml RSS polling Quartz 内置 ★ 定制扩展 外部服务

归属:Quartz 内置 vs 定制

管线绝大部分是 Quartz 原生能力,定制的只有一步清洗。

组件来源做了什么
ContentIndex 插件 Quartz 内置 按配置生成 RSS XML + Sitemap。RSS 输出「原样照搬页面 HTML」——包括 rehype-autolink-headings 生成的 heading 锚点 SVG
generateRSSFeed() Quartz 内置 生成 RSS 2.0 XML 结构(titlelinkpubDatedescription
rssFullHtml / rssLimit Quartz 内置 配置项:开启全文输出(true 时 description 含完整 HTML)、控制条数
escapeHTML() Quartz 内置 HTML 实体编码,确保 XML 安全(<&lt;
cleanHtml() ★ 定制 正则剥除 <a role="anchor">...</a> 锚点 SVG。Quartz 不做任何内容清洗,邮件渠道的清洗完全是我们加的
关键认知:Quartz 的 RSS 不是为邮件设计的——它的设计目标是「页面内容的 XML 序列化」。heading 后的锚点 SVG 是页内导航元素,在页面浏览中有用,但在邮件客户端中完全无意义。邮件渠道的内容清洗是我们在 Quartz 之上的定制层。

定制核心:cleanHtml()

唯一的一步定制:在 toHtml() 之后、escapeHTML() 之前,插入正则清洗。

// quartz/plugins/emitters/contentIndex.tsx
const cleanHtml = (html: string): string => {
  return html.replace(/<a\s+role="anchor"[^>]*>.*?<\/a>/g, "")
}

// emit() 中构建 RSS description
richContent: opts?.rssFullHtml
  ? escapeHTML(cleanHtml(toHtml(tree, { allowDangerousHtml: true })))
  : undefined,
//           ^^^^^^^^
//           先清洗原始 HTML,再编码。清洗必须在编码前——编码后的 &lt;a 和原始 <a> 是不同的字符串。

配置层面,在 quartz.config.ts 中开启全文 RSS:

Plugin.ContentIndex({
  enableSiteMap: true,
  enableRSS: true,
  rssLimit: 50,
  rssFullHtml: true,   // 输出完整 HTML 正文
})

设计决策

决策选择原因
清洗方式正则剥除锚点 SVG最小改动,不修改 Quartz 源码流程,仅在构建管线中插入一步
清洗时机toHtml() 之后、escapeHTML() 之前正则匹配的是原始 HTML 字符序列,编码后的实体无法匹配
RSS 内容全文 HTML(rssFullHtml: true)邮件订阅需要完整正文,摘要 RSS 没有订阅价值
RSS 条数50 条覆盖足够长的历史,新订阅者可以回溯