首页只显示已经发布的文章,RSS 却读取整个内容集合。这种错误在肉眼检查页面时很难发现:导航没有草稿链接,但订阅器已经拿到了标题、摘要甚至全文。公开边界应由数据层定义,不能分别交给每个页面记住。
假设站点是定时构建的静态博客,文章包含 draft 和明确发布时间。所有公开出口都应该消费同一个 getPublishedArticles(),包括详情路由、列表、标签、搜索索引、RSS 和 sitemap。
让订阅源消费已经决策完成的记录
RSS 生成器应收到公开记录,而不是原始草稿后自行决定哪些能发。上游处理身份、审核与排期,下游只负责把标题、地址、时间和摘要转换成订阅格式。这样 RSS 不必理解私人草稿状态,也不会在一个新字段加入时遗漏过滤。函数名称叫“已发布”还不够,测试应证明其返回值确实不包含未公开记录。
假设每天上午九点发布两篇文章。排期系统在九点生成公开文件,构建同时读取这些文件和其他既有文章。RSS 不需要从私人目录挑选“时间已经到”的稿件,因为时间到了不代表审核仍然有效。发布批准、内容指纹与排期条件都满足后才转换,能避免编辑者深夜修改文章却在第二天被旧批准自动发布。
构建失败时也不能先让 RSS 更新,再等待详情页部署。应让列表、详情和 RSS 属于同一个完整产物,使用平台支持的版本化发布能力切换;是否能够整体切换需要核对具体托管平台,不能推导为所有存储与缓存天然原子,浏览器和边缘旧副本仍可能暂时并存。运行时生成 RSS 的系统则需要稳定的已发布版本标识,不能在数据库写入一半时读取。这里关注的是发布一致性,格式正确的 XML 无法替你保证条目所指页面已经存在。
type PublicationRecord = {
data: { draft: boolean; publishedAt: string };
};
export function publishedArticles<T extends PublicationRecord>(items: T[], now: Date): T[] {
const cutoff = now.getTime();
return items.filter(({ data }) => {
const timestamp = Date.parse(data.publishedAt);
return data.draft === false
&& Number.isFinite(timestamp)
&& timestamp <= cutoff;
});
}
这段代码假设 publishedAt 含明确时区,例如 2026-10-12T09:00:00+08:00。标准 ISO date-only YYYY-MM-DD 被 Date.parse 解释为 UTC 零点,不随构建机本地时区变化,但仍不能表达“香港上午九点”;没有偏移的 date-time 则按本地时区解释,会受机器设置影响。先做严格 Schema 校验,拒绝这两种不符合发布时刻协议的输入,再执行公开过滤,不能把无效时间静默理解为现在。Date.parse 在这里只转换已校验记录,不承担严格格式与有效日历校验。
构建开始时创建一次 now,传给所有出口。否则发布边界恰好落在构建过程时,不同页面可能包含不同文章。更重要的是,静态部署不会因为时间到了自动更新,必须有定时构建和部署机制;过滤条件只是必要条件,不是调度器。
RSS 的稳定标识不要随标题变化
RSS 2.0 的 guid 可以使用永久链接,也可以使用稳定的不透明值;使用非 URL 标识时,应按格式约定明确 isPermaLink="false",不能把内部编号伪装成可访问地址。验收解析生成器真正输出的字段,而不是仅查看传入的 ID。链接负责让读者到达正文,guid 负责表示条目身份,两者恰好相同是一种选择,不是所有订阅格式的必然要求。
摘要订阅与全文订阅有不同取舍。摘要让正文渲染更简单,全文让离线阅读更完整。采用全文时,要处理相对图片地址、代码块与 HTML 清洗;不能把编辑预览里的脚本或交互组件直接塞进 XML。RSS 解析严格,未经转义的字符可能使整个源无效。
条目标识决定订阅器识别同一篇文章的方式,发布时间表达首次公开时间,更新时间表达修改时间,列表顺序决定源中呈现位置。四者不能混为一谈。修正引用链接可以更新正文,但是否通知读者应由订阅策略决定;把发布时间一律刷新,会让小改动看起来像新文章,读者也很难判断讨论的时间背景。
用 URL 作为标识时,域名迁移可能导致订阅器重新识别条目。稳定内部标识可以降低这种风险,但必须符合所用格式与生成器的输出约定,且不依赖文件排序。迁移前可以在测试源中验证阅读器行为,必要时接受部分客户端重新收录的现实,不能声称某种标识能控制所有订阅器的实现。
排序需要稳定的次级规则。同一天两篇文章具有相同时间戳时,按稳定标识排序能避免每次构建交换顺序。站点仅输出最近若干条时,边界附近的交换会造成条目反复进出源。限制条目数量可以控制下载体积,但归档文章仍应有页面可达,RSS 不承担完整内容数据库的职责。
全文订阅需要自己的渲染边界
Markdown 在网站中经过组件渲染,不代表相同内容可以直接放进 RSS。交互组件依赖脚本,订阅器可能没有执行环境;相对图片路径依赖网站当前位置,订阅器却可能显示错误来源;原始 HTML 若未经清洗,还可能让不可信输入影响阅读器。全文输出应该采用明确允许的静态元素集合,保留文本、链接、图片与代码,拒绝不必要的可执行内容。
链接转换要以文章绝对地址为基准,而不是 RSS 文件地址。例如正文里的 ./diagram.png 若按照 /feed.xml 解析,会指向站点根目录的图片。渲染器应处理图片与普通链接,并拒绝不允许的协议;不能只转换 src 而忘记代码之外的 href。外部资源失效时,替代文本仍然需要解释图片包含的信息。
代码块可保留语言标记和可读空白,但不要依赖网站的高亮脚本。复杂表格在不同订阅器中表现不同,关键结论不能只藏在颜色或交互展开中。全文订阅的验收因此包括至少一个真实阅读器或静态渲染预览,不只是 XML 解析成功。如果站点采用摘要模式,应让摘要有独立价值,而不是截取第一百个字符后在代码中间结束。
删除文章不是删除一次文件就结束
一篇文章从列表移除后,旧详情页可能仍然留在错误配置的部署产物中,订阅器也可能已经缓存正文。发布系统应使用完整构建产物替换,而不是仅上传新增文件。对于敏感信息,应该假设公开过的内容已被复制,不能承诺撤回能消除所有副本。
RSS 自身的缓存时间也影响读者何时看到更新。响应头可以允许合理缓存,但应该让紧急撤稿具备清理路径。不要因为页面即时更新,就认为源、CDN 和客户端缓存同时更新。
缓存验证要考虑读者的拉取行为
网站部署完成后,阅读器可能尚未重新拉取源,这是正常的客户端行为。服务器可以提供验证器,未变化时允许条件请求减少带宽;源变化后必须更新验证器。若仅以模板修改时间生成 ETag,而不包含文章集合变化,阅读器可能收到错误的未修改响应。验证器应反映最终输出字节或同等可靠的内容版本。
测试准备首次请求、无变化条件请求、增加文章后的条件请求和撤稿后的条件请求,检查状态码与正文。再检查 CDN 缓存是否允许及时重新验证。紧急撤稿流程应同时清理详情页与源的缓存,但客户端保存的正文仍然无法保证被删除,告知作者这一限制比暗示撤回可以完全抹去更准确。
哨兵字符串测试还应搜索压缩前的搜索索引、客户端脚本、生成日志和源码映射。草稿可能不在 HTML 中,却被某个全量索引器序列化进浏览器资产。对公开产物做全面负向检查,并让私人目录不参与公共加载器,是两层互补保护。每增加一个公开出口,都要把它加入这套测试,发布边界才不会随着站点功能增长而悄悄扩大。
订阅源更新是一项兼容性变更
从摘要切换到全文、从固定最近十条切换到五十条,都可能改变阅读器下载量和用户预期。变更前应估算输出体积,检查长代码、嵌入图片与特殊字符的呈现,并保持条目标识稳定。不能通过给全部旧条目换标识来强迫阅读器刷新,因为它可能被理解为几十篇新文章,破坏用户的未读状态。
为同一源保持稳定 URL。若需要移动地址,提供明确重定向并长期保留旧入口,测试阅读器是否能跟随。站内发现链接也应更新,让浏览器与订阅工具找到新源。页面上的订阅按钮和 HTML 中的自动发现标签是两个入口,遗漏后者可能导致阅读器仍使用旧地址。使用正确媒体类型可以减少工具误判,但不能补救损坏的 XML。
源的语言信息应与实际内容对应。多语言站点可提供独立语言源,也可明确提供混合源;不要让英文源突然装入全部中文草稿,只因为它们共享一个集合。语言筛选必须发生在已公开集合之后,先隔离发布状态,再选择语言。这样新增语言不会改变私人内容是否被公开的基本判断。
兼容性测试保留一组已发布样本,比较升级前后的标识、时间、顺序和链接。正文格式变化可以是有意行为,身份变化则应有说明。阅读器缓存不完全受服务器控制,所以维护的重点是稳定契约与明确迁移,不是强迫所有客户端在同一时刻刷新。
用事件时间线解释两篇文章的发布一致性
设想九点任务将甲、乙两篇文章转成公开输入,九点零一分构建失败。私人队列不能立刻记录“已经公开”,否则下一次任务会跳过尚未上线的文章;也不能毫无记录地重新选择另两篇,使这一批永远停留在中间状态。需要把选稿、转换、构建、部署与验收区分成可恢复状态,并给这一批稳定标识。状态记录不意味着每一步都自动重试,外部部署结果未知时先查实际版本。
RSS 属于这一批产物中的一个投影。若部署平台返回超时,实际发布可能已经成功,应通过非敏感版本清单和公开地址确认,而不是直接重复发布并重写日期。重试同一批应保持条目标识与首次公开时间的预期值,不能因为运行时间改变就把同一文章变成另一条新闻。稳定批次能追溯结果,但不能替代平台对部署状态的查询能力。
构建成功以后,先在预览产物检查两篇详情与源中的条目,再发布并检查真实入口。缓存可能暂时使源与详情属于不同版本,因此验收需要认识平台切换与缓存传播边界。可以在有限时间内重试读取,超过期限就报告异常,而不是无限等待然后记录成功。若源已经含有新条目而详情缺失,优先修复公开一致性,不再追加下一批文章掩盖问题。
批次日志只保存文章稳定 ID、版本与阶段,私人正文不进入公开日志。恢复工具应该展示将要做什么,并验证输入指纹没有改变。假如作者在失败期间继续修改正文,同一批不能悄悄使用新内容复用旧批准。这条约束在每日任务中尤其重要,因为无人值守不应意味着跳过编辑证据。
把 XML 正确性与 HTML 安全性分开验证
标题包含 A & B 时,XML 序列化器需要正确转义;正文中包含 HTML 时,还要决定订阅格式如何承载内容。XML 转义解决文档结构问题,HTML 清洗解决可执行或危险内容问题,两者不是同一项工作。把字符串简单替换几种字符,既可能双重转义,也无法建立安全元素允许集合。优先使用成熟的 RSS 生成器与适合输出格式的清洗器。
代码示例本身可能包含脚本标签文字。清洗流程应该保留读者正在阅读的代码,而不是把它当成真实可执行节点;反过来,Markdown 的原始 HTML 不能因为出现在作者文件里就自动信任。渲染顺序需要明确:先将 Markdown 按约定转换,再处理允许的 HTML 和链接协议,最后由 XML 工具序列化。若使用 CDATA,也要由工具处理其特殊结束序列,不能以为它能容纳任意原文。
测试样本覆盖尖括号、与号、引号、非 ASCII 字符、相对链接和代码围栏,再解析回输出字段,比较其语义内容。仅断言 XML 不报错,会漏掉被双重转义后显示为实体文本的问题。安全样本使用合成的危险标签,确认输出不保留执行能力;不要在真实阅读器里运行未经清洗的私人文章来“看看会怎样”。
图片还会引入访问与隐私边界。外部图片可能在读者打开条目时向第三方发起请求,是否加载由阅读器策略决定。站点可以减少无必要的外部资源,并提供有意义的替代文本,但不能保证所有客户端禁止追踪。需要登录的图片地址不应进入公开源;签名图片 URL 也可能暴露访问凭据或很快失效,不适合永久订阅内容。
为源规模建立实际的交付预算
全文源从十条增加到五十条,下载体积并不是简单增加五倍,因为文章长度、图片表示和代码密度不同。预算应依据最终序列化字节,包括压缩前后体积,分别观察静态产物大小与实际传输量。阅读器可能频繁拉取,压缩和条件请求可以减少成本,但不能假设所有客户端都正确使用验证器。
可以限制最近条目数或最近时间窗口,并保持边界排序稳定。选择全文还是摘要,依据离线阅读价值与交付成本,而非把全文视为唯一高级选项。若提供两个源,地址与描述应清楚表达区别,条目标识在各自契约内保持稳定。不要在同一个 URL 上反复切换策略,让客户端不知道旧条目是否仍代表相同内容。
源生成也不应该每次请求重新读取私人资料。静态博客可在构建时生成公开 XML,运行时系统则从明确的公开版本读取,并限制生成成本。缓存失效时大量阅读器同时回源,需要考虑源生成延迟与服务器容量。静态 XML 的生成和托管一般更容易预测,但仍需监控加载错误、意外零条目和异常体积。
文章数增加以后,可以保留一个归档页面帮助读者寻找较早内容,而不强迫 RSS 包含全站历史。订阅源不是备份,也不是文章数据库的导出。历史条目退出近期源后,正文链接仍应按站点的长期维护规则可达。这样交付预算与内容保存职责分开,调整近期窗口不会被误解为删除文章。
发生泄漏时先停止新的公开出口
若哨兵检查发现私人摘要进入源,先停止有问题的构建或发布路径,再清理可控制的源与详情缓存。调查时确认泄漏范围:只含标题、含摘要还是含全文,是否进入搜索索引与客户端资产。不要为了定位问题把完整私人集合再复制到公共 CI。使用最小化合成样本重现错误,保留必要版本和时间记录。
修复以后重新执行所有出口的负向测试,而不是只在 RSS 加一个过滤条件。根因可能是上游公开集合的状态解释错误,局部过滤会留下其他泄漏路径。公开过的内容无法承诺完全撤回,但可以停止继续传播、修复隔离并如实说明可控范围。一个安全边界的事故不应该用“页面上没有链接”来减轻严重程度。
用哨兵文章做负向验收
创建三份只在测试中使用的记录:已发布、明确草稿、未来发布时间。给草稿放一个独特哨兵字符串,构建后搜索整个 dist,确认标题、正文、slug 都不存在。再解析 RSS,验证只有已发布记录且地址是生产主域名。
测试应覆盖 XML 可解析、条目标识唯一、日期可解析、链接可访问。对标签页和搜索索引执行相同哨兵检查。最值得保留的自动测试不是“RSS 返回 200”,而是“未公开内容在所有产物里都不存在”。
阅读器没有新条目时,先查看服务器是否提供了新版本,再检查 XML 是否可解析,最后看客户端何时拉取。三者对应不同责任,反复重新发布文章可能制造更多重复条目。日志可以记录源版本、条目数和生成时间,不必记录完整正文。若某次生成的条目数突然为零,应让部署失败或要求确认,避免把加载器错误当成正常清空。
使用公开的测试订阅源时只放合成文章,不放真实私人稿件。测试文件也要带独特标记并验证生产构建不包含它们。这样既能测试特殊字符、长标题和图片地址,又不会为了调试订阅器绕过原本的隔离要求。RSS 是一个持续维护的接口,变更摘要策略、标识规则或时间字段时,应像改 API 一样检查既有消费者会得到什么。
参考:Astro RSS、RSS 2.0 规范。