DragonRster`s Void
Banner Image
目录
脚本详解:generate-rss.ps1

让老派阅读器也能订阅你的博客


generate-rss.ps1 负责将博客文章列表转换为标准的 RSS 2.0 XML 格式。RSS(Really Simple Syndication)是一个诞生于 1999 年的内容订阅协议,至今仍被大量阅读器和聚合工具使用[1]。在 90s 风格的网站上提供 RSS,既是功能上的实用选择,也是精神上的契合——RSS 本身就是那个时代的产物。

1. RSS 2.0 格式概述

RSS 2.0 使用 XML 描述一个"频道"(channel)和其中的"条目"(item)。基本结构如下:


<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel>
    <title>DragonRS 90s Blog</title>
    <link>index.html</link>
    <description>DragonRS 90s Blog RSS feed</description>
    <language>zh-cn</language>
    <lastBuildDate>Mon, 28 Apr 2026 23:00:00 GMT</lastBuildDate>
    <item>
      <title>文章标题</title>
      <link>blog-slug.html</link>
      <description>前 200 字的摘要...</description>
      <guid isPermaLink="false">blog-slug.html</guid>
      <pubDate>Mon, 28 Apr 2026 12:00:00 GMT</pubDate>
    </item>
    <!-- 更多 item... -->
  </channel>
</rss>

每个 <item> 有 5 个关键字段:标题(title)、链接(link)、摘要(description)、唯一标识(guid)、发布日期(pubDate)[2]

2. XML 转义函数

生成 XML 的第一步是确保内容不会破坏 XML 结构。脚本定义了一个专门的 XML 转义函数:


function Escape-Xml {
    param([string]$value)
    if ($null -eq $value) { return "" }
    return $value.Replace('&', '&')
                 .Replace('<', '<')
                 .Replace('>', '>')
                 .Replace('"', '"')
                 .Replace("'", ''')
}

五个字符需要转义:& < > " '。这覆盖了 XML 规范要求的所有特殊字符[3]。注意 & 必须最先替换,否则会破坏后续已经转义的实体。

PowerShell 的 .Replace() 方法返回新字符串(字符串不可变),链式调用等同于依次执行 5 次替换。

3. 标题提取

文章标题在 HTML 中以 <font size="6" color="#ff66cc"> 包裹。脚本用正则提取第一个匹配的 <font> 标签内的文字:


function Get-RssTitle {
    param([string]$html)
    $match = [regex]::Match($html,
        ']*>(.*?)', 'Singleline')
    if ($match.Success) {
        return $match.Groups[1].Value.Trim()
    }
    return "Blog post"
}

'Singleline' 选项让 . 匹配换行符,确保跨行的标题内容能被完整捕获。如果匹配失败(例如内容文件格式异常),回退为默认值 "Blog post"

4. 发布日期推导

脚本使用了一个巧妙的启发式方法从标题中提取日期,而不是依赖文件的 LastWriteTime


function Get-RssPubDate {
    param([string]$title, [datetime]$fallback)
    $date = $null
    # 尝试匹配 "2026年4月28日" 格式
    if ($title -match '(\d{4})\D+(\d{1,2})\D+(\d{1,2})') {
        $year = [int]$matches[1]
        $month = [int]$matches[2]
        $day = [int]$matches[3]
        $date = Get-Date -Year $year -Month $month -Day $day
    }
    # 回退:匹配 "2026年4月" 格式(取当月1日)
    elseif ($title -match '(\d{4})\D+(\d{1,2})') {
        ...
        $date = Get-Date -Year $year -Month $month -Day 1
    }
    if ($null -ne $date) { return $date }
    return $fallback    # 最终回退:文件修改时间
}

这个函数有三层回退机制:

  • 第一层:从标题中提取年-月-日(如"2026年4月28日")
  • 第二层:从标题中提取年-月(如"2026年4月"),默认为当月 1 日
  • 第三层:使用文件的 LastWriteTime 作为最终兜底
  • RSS 的 pubDate 使用 RFC 2822 格式(也叫 RFC 822 格式),例如 Mon, 28 Apr 2026 12:00:00 GMT。PowerShell 的 DateTime.ToString('R') 正好输出这种格式[4]

    5. 正文摘要生成

    RSS 的 <description> 字段是纯文本,不能包含 HTML。脚本需要将 HTML 正文转换为纯文本摘要:

    
    function Get-PlainText {
        param([string]$html)
        $text = [regex]::Replace($html, '<[^>]+>', ' ')     # 去标签
        $text = [regex]::Replace($text, ' | ', ' ')# 去  
        $text = [regex]::Replace($text, '\s+', ' ')         # 压缩空白
        return $text.Trim()
    }
    
    # 取前 200 字符作为摘要
    $description = $plain.Substring(0, [Math]::Min(200, $plain.Length))
    $description = Escape-Xml $description
    

    三步清洗:去标签 → 去实体 → 压缩空白。最终取前 200 个字符,这是一个经验值——既能在阅读器中展示足够的预览内容,又不至于让 RSS 文件过于庞大[5]

    6. 文件扫描与排序

    博客文件按修改时间降序排列:

    
    $blogFiles = Get-ChildItem (Join-Path $blogDir 'content-*.html')
        | Sort-Object LastWriteTime -Descending
    

    这里有一个重要的设计选择:使用 LastWriteTime(文件修改时间)而非元数据中的 date。原因是在 RSS 中,pubDate 由标题推导(见上一节),但文章在 RSS 文件中的排序由文件系统时间决定。这确保即使标题中没有日期信息(理论上不应该发生),文章也能有一个合理的排序[6]

    7. GUID 设计

    RSS 的 <guid> 字段用于唯一标识一篇文章。这里使用文章链接作为 GUID:

    
    $guid = Escape-Xml $link    # 即 "blog-slug.html"
    # ...
    <guid isPermaLink="false">$guid</guid>
    

    isPermaLink="false" 表示 GUID 不是 URL(虽然它看起来像一个路径)。这是一种常见的做法——用页面路径作为唯一标识。如果将来文章 slug 变更,RSS 阅读器会将其视为一篇新文章,这通常是期望的行为[7]

    8. 已知限制

    当前版本有几个可以改进的地方:

  • 只扫描中文博客目录(src/content/pages/blog/),不包含英文文章。英文文章的 RSS 需要单独生成
  • 摘要的 200 字符截断可能切断多字节 UTF-8 序列——虽然 .NET 的 Substring 按字符(而非字节)计算,所以实际上不会出现这个问题
  • lastBuildDate 使用的是脚本运行的时间,而非最新文章的发布时间。这意味着即使没有新文章,RSS 的 lastBuildDate 也会在每次构建时更新——有些 RSS 阅读器会因此认为有新内容
  • 小结

    generate-rss.ps1 是整个站点中最简单的脚本(约 108 行),但它实现了一个完整且符合标准的 RSS 2.0 生成器。从 XML 转义到标题提取到纯文本化,每一步都很直接,没有多余的抽象。

    RSS 在今天可能被视为"过时技术",但它代表了 Web 2.0 时代对开放标准的信仰——内容属于作者,不属于平台。一个没有任何 JavaScript 的网站,搭配一个纯 XML 的订阅源,正是这种理念的最佳实践[8]


    [1]RSS 有两个主要版本:RSS 0.91/2.0(Dave Winer, 1999-2002)和 RSS 1.0(RDF Site Summary, 2000)。此外还有 Atom(2005)作为替代。本网站选择 RSS 2.0,因为它最简单,且在播客和传统博客中最为普及。

    [2]guid 的全称是 Globally Unique Identifier。在 RSS 中,它用于让阅读器判断一篇文章是否已经被读过。如果两篇文章的 guid 相同,阅读器会认为它们是同一篇。

    [3]XML 1.0 规范规定只有 <& 是"必须"转义的(在元素内容中),但转义所有五个字符是更安全的做法,尤其在属性值中。

    [4]RFC 2822 日期格式(如 Mon, 28 Apr 2026 12:00:00 GMT)诞生于 2001 年的电子邮件标准,被 RSS 2.0 规范采纳。DateTime.ToString('R') 中的 'R' 代表 RFC1123,它输出的格式与 RFC 2822 兼容。

    [5]RSS 的 <description> 字段可以包含 HTML(如果包裹在 <![CDATA[]]> 中)。但这里选择纯文本摘要,因为并非所有 RSS 阅读器都能正确渲染 HTML——尤其是老式阅读器和命令行 RSS 工具。

    [6]实际上,更好的方案是从元数据的 date 字段读取排序依据,因为文件修改时间可能在 git pull 等操作后被重置。这是未来的优化方向。

    [7]如果使用 URL 作为 GUID(isPermaLink="true"),RSS 阅读器会在每次 URL 变化时认为有新内容。使用 isPermaLink="false" 的路径作为标识符,给了我们更多的控制权。

    [8]在 2026 年的今天,大多数内容被锁在封闭平台(微信公众号、Medium、Substack)中,RSS 更显得珍贵。它不依赖任何公司的服务器,不需要注册账号,不需要算法推荐——你订阅,你收到,仅此而已。


    « 脚本详解:rebuild-all.ps1 —— 全站构建的指挥家 返回主页 脚本详解:generate-sitemap.ps1 —— 为搜索引擎铺路 »
    English

    搜索

    最新文章

    » 脚本详解:build.ps1
    » 脚本详解:gener...
    » 脚本详解:gener...
    » 脚本详解:rebui...
    » 脚本详解:gener...

    » 文章归档


    本文标签

    教程 网站建设 脚本


    功能

    » RSS 订阅
    » GitHub 源码
    » 返回顶部
    » 文章归档


    DRAGONRSTER
    CC BY-NC-SA
    © 2004-2026 DragonRster • Made with HTML • 本站支持IE5.5+
    最佳浏览分辨率:1024x768 • 最后更新于 2026年04月29日 02:05:28