全文RSS输出:小站的分发与收录

文章页面很干净,RSS 里却是半截话

我自己的博客用了三年多的全文 RSS,直到一个读者写信问我:"你的 RSS 里每篇文章都只有前两段加一个『阅读全文』,我用 Reeder 订阅了 200 个站,只有你的需要来回点开网页。"这句话让我意识到一件事:RSS 对个人站长来说,不只是"给阅读器用的格式",它是一条完整的、可控的、不依赖搜索引擎的分发渠道。

更实际的收益是收录。很多中文搜索的抓取调度器会通过 RSS 发现新内容,尤其对更新频率不高的小站,RSS 是一个稳定的"新内容信号源"。我的博客从只输出摘要改成输出全文并加上正确的时间戳之后,新文章的发现速度从平均两天缩短到半天以内。

但全文输出不是把 `description` 换掉那么简单。它涉及摘要截断、HTML 合法性、图片地址、相对链接、编码、缓存头、去重标识等一系列细节,任何一处错了都会导致阅读器里排版错乱或者重复推送。下面是我在自己站点上完整实现的一套方案,基于 Typecho,但思路对 WordPress、Hugo、自建程序都适用。

第一步:先看清 RSS 里到底输出的是什么

动手改之前,先用命令行把现有的 feed 抓下来看结构。这一步能省掉很多猜测:

# 抓 RSS 并格式化查看前 60 行
curl -s https://www.example.com/feed/ | xmllint --format - | head -60

# 统计 description 字段的平均长度(判断是否为全文)
curl -s https://www.example.com/feed/ \
  | xmllint --xpath '//item/description/text()' - 2>/dev/null \
  | wc -c

输出一般是 `<description><![CDATA[<p>前两段…</p>]]></description>`。如果长度只有几百字节,就是摘要模式;如果是几 KB 到几十 KB,则已是全文。另外要检查 `<pubDate>` 与 `<guid>`:前者决定阅读器是否认为"有新内容",后者决定去重。

我的经验是四个字段最容易被忽略:guid(应当是永久不变的 URL,而不是时间戳)、pubDate(必须是文章发布时间而非抓取时间)、lastBuildDate(不要每次请求都变)、content:encoded(WordPress 体系用的全文标签,某些阅读器优先读它)。

第二步:全文输出的三种实现路径

不同程序的做法不一样,我把它们分三类,选择成本最低的那条。

路径一:改模板(最干净)

Typecho 的 RSS 模板在主题目录下,通常叫 `feed.php` 或 `rss.php`。关键是找到输出 description 的地方,把它换成正文内容。以 Typecho 默认主题为例:

<?php while ($this->next()): ?>
<item>
  <title><?php $this->title(); ?></title>
  <link><?php $this->permalink(); ?></link>
  <guid isPermaLink="true"><?php $this->permalink(); ?></guid>
  <pubDate><?php $this->date->rfc822(); ?></pubDate>
  <dc:creator><?php $this->author->screenName(); ?></dc:creator>
  <description><![CDATA[<?php echo $this->content(); ?>]]></description>
  <content:encoded><![CDATA[<?php echo $this->content(); ?>]]></content:encoded>
</item>
<?php endwhile; ?>

注意 CDATA 的使用:正文里如果出现 `]]>` 会提前闭合 CDATA,虽然概率极低,但如果文章里贴了 XML 示例就可能发生。稳妥的写法是先转义再输出,或者直接用 `htmlspecialchars` 处理正文并去掉 CDATA 包裹(两种写法都能被阅读器正确解析,前者更兼容老客户端)。

路径二:用插件,不改模板

如果不想动主题(升级会覆盖),可以在 `usr/plugins/` 下放一个极简插件,挂钩到 RSS 输出上:

<?php
class FullFeed_Plugin implements Typecho_Plugin_Interface
{
    public static function activate()
    {
        Typecho_Plugin::factory('Widget_Archive')->feedItem = ['FullFeed_Plugin', 'feedItem'];
    }
    public static function deactivate() {}
    public static function config(Typecho_Widget_Helper_Form $form) {}
    public static function personalConfig(Typecho_Widget_Helper_Form $form) {}

    public static function feedItem($item)
    {
        // 把摘要替换为完整正文,并清掉分页标记
        $content = $item['content'];
        $content = preg_replace('/<!-- ?more ?-->/', '', $content);
        $item['description'] = $content;
        return $item;
    }
}

插件的好处是主题升级不影响,坏处是要跟着程序版本维护。我最终选了这条路,因为我的主题每隔几个月就换一次。

路径三:反代层重写(不推荐,但可用于救急)

用 Nginx 的 `sub_filter` 把摘要替换成全文是不可能的(正文不在 HTML 里)。真正可行的是让 feed 走一个 PHP 中转脚本,由脚本自己查库拼 XML。这条路只有在"程序不给改、也不给装插件"时才考虑,成本远高于前两条。

第三步:全文输出必须处理的六个细节

把摘要换成正文只是第一步,下面这些不做,阅读器里的体验会明显打折。

相对链接改成绝对地址

文章里写 ``,在网页上没问题,但在阅读器里就是死链。服务端输出前必须做一次替换:

$content = preg_replace(
    '#(src|href)=(["\'])/(?!/)#i',
    '$1=$2https://www.example.com/',
    $content
);

注意排除 `//` 开头的协议相对地址,否则会被改坏。更稳的做法是用 DOM 解析而不是正则,避免属性里含引号时误伤。

图片加 width 限制与 lazy 属性清理

很多阅读器不支持 `loading="lazy"`,某些甚至不认 `srcset`,会直接显示空白。输出时可以剥掉这些属性,并给图片加一个最大宽度样式:

$content = preg_replace('/ loading="lazy"/', '', $content);
$content = preg_replace('/ srcset="[^"]*"/', '', $content);
$content = str_replace('<img ', '<img style="max-width:100%;height:auto" ', $content);

清除站内广告与统计代码

我文章末尾有一段 AdSense 的 in-article 广告块。它在网页上是收入,在阅读器里是噪音,还可能因为脚本被拦导致排版错乱。输出到 feed 时按 class 或 script 特征整段剔除:

$content = preg_replace('#<div[^>]*class="adsbygoogle"[^>]*>.*?</div>#s', '', $content);
$content = preg_replace('#<script\b[^>]*>.*?</script>#is', '', $content);

这一条很关键:如果正文里带 `

Last modification:October 29th, 2026 at 01:26 pm

Leave a Comment