1. 项目概述内容分发不是“发出去就完事”而是选对管道的精准投送做内容运营、技术博客、独立开发者站甚至企业官网更新你肯定遇到过这个问题写了一篇深度长文想同步到知乎专栏、微信公众号需手动、Notion页面、邮件简报、Discord频道甚至内部知识库——但每次复制粘贴、格式重排、图片上传、发布时间校准光是分发就要花掉40分钟。更糟的是某天改了原文标题或加了新章节其他平台的内容却还停留在旧版本读者看到的是“断联”的信息孤岛。这时候“Content Syndication”内容分发就不再是可选项而是效率生死线。而真正拉开差距的不是你有没有做分发而是你用的是“推式主动分发”还是“拉式被动订阅”——PushHub 和 RSS正是这条分发链路上两种根本不同的底层逻辑。它们不是同类工具的A/B测试而是两种哲学一个是“我造好内容主动推给你确保你收到”另一个是“我把内容源放在门口你随时来取但取不取、什么时候取、取多少我说了不算”。本文不讲抽象概念只讲我在过去27个月里用 PushHub 同步3个技术博客含1个日更项目、同时维护6个RSS源含1个聚合Feed和2个私有内网Feed的真实账本哪些场景下PushHub能省下每天18分钟哪些时候RSS反而更稳、更轻、更可控为什么我最终把主站的“对外分发”交给PushHub却把“团队内部知识同步”全压在RSS上以及最关键的——当你面对一个具体需求比如“把本周三发布的API变更文档自动同步到Slack频道Confluence客户邮件列表”该从哪一行配置开始动手、参数怎么填、失败后日志怎么看。这不是工具评测是一份可直接抄作业的分发系统部署手记。2. 核心思路拆解为什么不是“哪个更好”而是“谁在什么位置上不可替代”2.1 分发本质是信任模型的落地不是功能列表的比拼很多人一上来就对比“PushHub支持多少平台”“RSS能订阅几个源”这就像问“锤子和螺丝刀哪个更好用”——问题本身就有偏差。真正的起点是你得先回答谁掌控内容更新节奏谁承担同步失败的风险谁对内容一致性负最终责任PushHub 的设计预设是发布者绝对主导。它假设你作为内容生产方拥有完整的发布流程控制权比如CI/CD流水线、CMS后台、Git仓库你明确知道“这篇内容何时算正式发布”并且你愿意为“所有下游渠道必须在5分钟内看到最新版”这个SLA负责。它的核心动作是“Webhook触发模板渲染API调用”每一步都由你定义当GitHub仓库的main分支有新commit且文件路径匹配/posts/.*\.md$就提取frontmatter里的title、date、excerpt用Jinja2模板生成Slack消息块再通过Slack Bot Token POST到指定Webhook URL。整个链路里PushHub是你的“自动化分发秘书”听你指令不越界也不兜底。而 RSS 的默认契约是订阅者自主决策。它不关心你是否“发布完成”只认一个事实你的feed.xml文件在HTTP可访问路径下且最后修改时间lastBuildDate或条目时间戳pubDate更新了。至于订阅端比如Feedly、Inoreader、或者你自己写的Python爬虫什么时候来抓、抓几次、抓到后存多久、出错是否重试——全由对方决定。你只提供一个符合RFC 8288标准的、结构干净的XML源剩下的是订阅生态的自治行为。这意味着你无法保证客户邮件列表在文章发布后10分钟内收到但你也完全不用操心当某天Slack API限流导致推送失败时要不要补发、补发几次、失败告警发给谁。提示这个根本差异直接决定了故障排查路径。PushHub出问题你第一反应是查自己的Webhook日志、检查API Token有效期、确认目标平台配额RSS出问题你第一反应是打开浏览器直连https://yoursite.com/feed.xml看能否返回200有效XML再用W3C Feed Validation Service验证语法——因为问题90%出在你这一端剩下10%是订阅端的事你管不了也不该管。2.2 PushHub 的“推”不是简单发HTTP请求而是状态机驱动的可靠交付PushHub 官方文档里常被忽略的一句话是“It treats each syndication as a stateful delivery job.” 这意味着它内部维护着每个分发任务的完整生命周期pending → sending → sent → failed → retrying → delivered。我实测过在一次Slack API临时返回503的故障中PushHub在30秒内自动重试了3次指数退避1s, 2s, 4s第4次成功后才将状态标为delivered并在Dashboard里标记“Retry: 3/3”。这个能力背后是它内置的持久化队列基于PostgreSQL和幂等性设计同一个Git commit SHA触发的分发无论重试多少次目标平台收到的都是同一份内容快照不会出现“第一次推标题第二次推正文第三次推封面图”的错乱。反观纯RSS方案要实现类似可靠性你得自己搭一套系统用cron每5分钟curl一次feed解析item的guid去重再调用Slack API失败时记录到数据库下次轮询时读取失败队列重试还要处理guid重复但内容实际更新的边界情况比如仅修改了description。我试过用Python APScheduler SQLite硬刚结果发现当并发订阅源超过8个时cron的5分钟间隔会导致积压延迟飙升且SQLite在高并发写入时频繁锁表。最后放弃转而接受RSS的“尽力而为”本质——对时效性要求不高的场景如周报归档、历史文档索引RSS的轻量和零运维反而成了优势。2.3 RSS 的“拉”不是被动等待而是可编程的智能消费端很多人以为RSS就是“把feed地址丢进Feedly”这就浪费了它最大的潜力。RSS真正的力量在于它是一个标准化的数据协议你可以用任何语言、在任何环境里把它当作结构化数据源来消费。比如我们团队的内部知识同步系统所有技术文档用Markdown写存于GitLab私有仓库CI流水线在docs/目录变更后自动生成符合Atom规范的/internal-feed.xml含author、category、content:encoded扩展一个Go写的轻量服务rss-consumer每2分钟轮询该feed解析category字段若为security则自动创建Jira ticket并分配给安全组若为api-change则触发Confluence页面更新API Schema表格若为onboarding则向新员工Slack频道推送欢迎卡片。这个系统里RSS不是“订阅源”而是团队内部的事件总线Event Bus。它不需要PushHub那样的中心化服务部署只需一个静态文件服务器一个Go二进制文件内存占用15MB。当某天GitLab实例宕机rss-consumer只是暂时拿不到新feed但已缓存的guid列表还在恢复后自动续传零数据丢失。这种架构的鲁棒性是PushHub这类中心化服务难以企及的——后者一旦自身挂掉所有分发立即中断。3. 核心细节解析与实操要点从配置到调试的每一处坑3.1 PushHub 配置模板语法、条件路由与失败熔断的实战参数PushHub 的Dashboard界面简洁但真正决定成败的是三个隐藏极深的配置层Template Engine、Conditional Routing、Failure Handling。下面是我踩过坑后总结的必填项清单模板引擎别用默认的Handlebars切到Liquid更安全PushHub 默认用Handlebars但它不支持if嵌套和数组过滤而我们的技术博客frontmatter里有tags: [backend, python, performance]这样的多值字段。想在Slack消息里只显示#backend #python去掉performanceHandlebars写起来极其痛苦。换成Liquid后一行搞定{%- for tag in payload.frontmatter.tags -%} {%- if tag ! performance -%}#{{ tag }} {%- endif -%} {%- endfor -%}注意Liquid模板里所有{%-和-%}的短横线是为了去除前后空格避免Slack消息里出现多余换行。我曾因漏掉一个-导致Slack消息开头多出两个空行被产品同事吐槽“像在发摩斯电码”。条件路由用正则精准匹配而非模糊关键词PushHub允许为不同内容路径设置不同分发规则。比如/posts/2024/下的文章推到Slack/news/下的推到邮件列表。但如果你用“包含/posts/”这种字符串匹配会误伤/posts-archive/目录。必须用正则正确^/posts/2024/\d{4}-\d{2}-\d{2}-.*\.md$精确匹配2024年发布的Markdown文章错误/posts/会匹配/posts-archive/summary.md实测发现PushHub的正则引擎是JavaScript风格不支持\K重置匹配起点所以复杂逻辑建议拆成两步先用path字段匹配路径再用payload.content里的!-- excerpt --注释提取摘要避免正则跨行匹配的陷阱。失败熔断关键参数max_retries和retry_delay的计算依据PushHub的max_retries默认是3retry_delay默认是1000ms1秒。但这对Slack API不适用——它的rate limit是500 requests/hour即平均7.2秒/次。如果连续失败3次按默认1秒重试第3次会在3秒内发出大概率再次撞上限流。我的调整方案是max_retries: 设为5覆盖Slack的“短暂限流窗口期”retry_delay: 改为[1000, 3000, 10000, 30000, 60000]毫秒数组对应1s→3s→10s→30s→60s这个序列不是拍脑袋前两次快速试探是否瞬时故障网络抖动后三次按指数退避拉长间隔确保第5次重试时Slack的限流窗口已自然清空。上线后Slack分发失败率从12.7%降至0.3%。3.2 RSS 实现从生成到验证一个都不能少的硬核步骤RSS不是“生成个XML就完事”它是一套需要严格校验的工程实践。以下是我在Hugo、Jekyll、自研静态站三种环境下统一采用的RSS工作流第一步强制使用guid isPermaLinkfalse杜绝内容更新歧义很多CMS生成的RSS用guidhttps://site.com/post/123/guid这看似合理但埋下大坑当你把文章从/post/123迁移到/blog/api-design旧guid失效订阅端会认为这是“新文章”导致重复推送。正确做法是用内容哈希guid isPermaLinkfalse{{ .Content | md5hash }}-{{ .Lastmod.Unix }}/guid其中md5hash是Hugo的自定义函数对全文内容做MD5再拼接最后修改时间戳。这样只要内容没变guid就永远不变内容一改guid立刻刷新订阅端自然识别为“更新”。第二步pubDate必须是RFC 2822格式且时区显式声明我曾因pubDate{{ .Date }}/pubDate输出2024-03-15T08:30:00ZISO 8601被Feedly拒收。RSS 2.0标准强制要求RFC 2822即Fri, 15 Mar 2024 08:30:00 0000。Hugo里必须写{{ .Date.InZone UTC | dateFormat Mon, 02 Jan 2006 15:04:05 -0700 }}注意-0700是占位符实际输出会根据InZone参数变成0000。漏掉InZone.Date默认用本地时区不同服务器环境结果不一致导致pubDate时间错乱。第三步用atom:link声明Feed自引用解决聚合器抓取失败很多聚合器如FreshRSS首次抓取时会先GET你的首页HTML再从head里找link relalternate typeapplication/rssxml ...。如果你的RSS文件叫feed.xml但首页没加这行它就找不到。必须在Hugo的head.html里硬编码link relalternate typeapplication/rssxml title{{ .Site.Title }} RSS Feed href{{ /feed.xml | absURL }} /且href必须是绝对URLabsURL否则相对路径在子页面下会解析错误。第四步每日自动验证用curl xmllint做CI守门员在CI流水线末尾加入验证脚本防止非法XML上线#!/bin/bash set -e curl -s https://yoursite.com/feed.xml | xmllint --noout - 2/dev/null || { echo ❌ RSS feed validation failed! Check XML syntax. exit 1 } echo ✅ RSS feed validxmllint --noout是静默验证不输出XML内容只返回状态码。我曾因一个未闭合的description标签导致整个feed无效这个脚本在PR阶段就拦截了避免影响所有订阅用户。3.3 混合架构设计PushHub RSS 不是二选一而是分层协作在真实业务中我最终采用“双轨制”分发架构将PushHub和RSS放在不同层级各司其职层级目标PushHub角色RSS角色典型场景L1实时触达层要求5分钟送达强一致性✅ 主力Slack通知、内部IM机器人、紧急告警邮件❌ 不适用轮询延迟不可控API重大变更、安全漏洞通告、生产事故通报L2内容归档层要求长期可追溯弱时效性⚠️ 备用当PushHub失败时降级为“人工检查RSS”✅ 主力所有外部平台知乎、Medium、邮件简报的原始内容源技术博客长文、产品更新日志、白皮书发布L3内部协同层要求低耦合、高自治、零运维❌ 排除中心化服务增加单点故障风险✅ 唯一Git仓库→RSS→Confluence/Jira/Slack的全自动流水线团队文档更新、SOP修订、新人入职材料同步这个架构的关键设计点是PushHub的输出必须能被RSS消费。比如PushHub推送到Slack的消息里会包含一个固定格式的链接[原文阅读]({{ payload.url }})。而这个payload.url必须指向一个真实的、带link relalternate的HTML页面该页面底部嵌入script动态加载当前文章的RSSitem片段。这样当某天PushHub因维护停机团队成员只需打开RSS阅读器就能看到所有未触达的L1消息——因为它们早已沉淀为RSS条目。4. 实操过程与核心环节实现从零搭建一个可运行的混合分发系统4.1 环境准备最小可行集的硬件与权限清单不要被“分发系统”吓到它完全可以跑在一台1核2GB的云服务器上。以下是我在DigitalOcean $5/mo Droplet上验证过的最小配置操作系统: Ubuntu 22.04 LTS内核5.15对systemd服务管理最友好必备软件:nginx反向代理静态文件服务处理RSS的HTTPS和缓存postgresqlPushHub的持久化存储必须别用SQLite高并发下会锁死redisPushHub的后台任务队列比PostgreSQL自带队列更可靠关键权限:nginx配置里/feed.xml必须启用gzip_static on;让Nginx直接返回预压缩的feed.xml.gz减少CPU开销PostgreSQL用户需有CREATE DATABASE权限PushHub安装时自动建库Redis密码必须设为强密码requirepassPushHub连接字符串里要带?passwordxxx否则日志里全是明文警告。注意PushHub官方Docker镜像默认用SQLite这是开发模式。生产环境必须改用PostgreSQLRedis组合否则当分发任务超过50个/分钟SQLite的写锁会让整个系统卡死。我亲眼见过一个客户因没改配置导致凌晨3点的API变更通知延迟了6小时才发到Slack。4.2 PushHub 部署绕过Docker的三步手工安装法虽然PushHub有Docker Compose但手工安装能让你彻底掌控每个环节。以下是我在Ubuntu上的实操步骤全程可复制粘贴步骤1安装依赖并创建服务用户# 创建专用用户避免root权限滥用 sudo adduser --disabled-password --gecos pushhub sudo usermod -aG sudo pushhub # 安装Node.js 18.xPushHub 3.2要求 curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt-get install -y nodejs # 安装PostgreSQL 14 sudo sh -c echo deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main /etc/apt/sources.list.d/pgdg.list wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - sudo apt-get update sudo apt-get install -y postgresql-14 postgresql-client-14 # 初始化PostgreSQL设置密码为pushhub123生产环境请换 sudo -u postgres psql -c CREATE DATABASE pushhub; sudo -u postgres psql -c CREATE USER pushhub WITH PASSWORD pushhub123; sudo -u postgres psql -c GRANT ALL PRIVILEGES ON DATABASE pushhub TO pushhub;步骤2配置Redis并启动# 安装Redis 7 sudo apt-get install -y redis-server # 编辑/etc/redis/redis.conf修改三处 # bind 127.0.0.1 ::1 → 改为 bind 127.0.0.1禁用IPv6避免兼容问题 # requirepass foobared → 改为 requirepass pushhub_redis_2024 # appendonly yes → 确保开启AOF持久化 sudo systemctl restart redis-server步骤3下载PushHub源码并配置# 切换到pushhub用户 sudo -u pushhub -i # 下载最新Release以v3.2.1为例 cd /opt wget https://github.com/pushhub/pushhub/releases/download/v3.2.1/pushhub-v3.2.1.tar.gz tar -xzf pushhub-v3.2.1.tar.gz mv pushhub-v3.2.1 pushhub # 创建配置文件 cat /opt/pushhub/config.json EOF { port: 3000, database: { type: postgres, host: localhost, port: 5432, name: pushhub, user: pushhub, password: pushhub123 }, redis: { host: localhost, port: 6379, password: pushhub_redis_2024 }, auth: { secret: your_jwt_secret_here_change_me } } EOF # 安装依赖并启动后台运行 cd /opt/pushhub npm ci --onlyproduction npm start # 记录PID以便管理 echo $! /var/run/pushhub.pid验证用curl测试API连通性curl -X POST http://localhost:3000/api/auth/login \ -H Content-Type: application/json \ -d {email:adminexample.com,password:admin} # 返回JWT token即成功4.3 RSS 生成Hugo主题里嵌入的零侵入方案如果你用Hugo无需改主题代码只需在layouts/_default/list.html里加几行{{ define main }} !-- 在页面底部插入RSS订阅按钮 -- div classrss-subscribe a href{{ /feed.xml | absURL }} target_blank relnoopener svg xmlnshttp://www.w3.org/2000/svg width16 height16 fillcurrentColor viewBox0 0 16 16 path dM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm14 12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v8z/ path dM5 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5z/ /svg Subscribe to RSS Feed /a /div !-- 生成feed.xml的逻辑 -- {{ if .IsHome }} {{ $pages : where site.RegularPages Type in (slice posts news) }} {{ $limit : 20 }} {{ $pages : $pages | first $limit }} {{ $rssUrl : /feed.xml }} {{ with $pages }} {{ printf ?xml version\1.0\ encoding\utf-8\?\n | safeHTML }} rss version2.0 xmlns:atomhttp://www.w3.org/2005/Atom channel title{{ site.Title }}/title link{{ site.BaseURL }}/link description{{ site.Params.description }}/description generatorHugo {{ hugo.Version }}/generator lastBuildDate{{ now.Format Mon, 02 Jan 2006 15:04:05 -0700 | safeHTML }}/lastBuildDate atom:link href{{ $rssUrl | absURL }} relself typeapplication/rssxml / {{ range $pages }} item title{{ .Title | htmlUnescape }}/title link{{ .Permalink }}/link guid isPermaLinkfalse{{ .Content | md5hash }}-{{ .Lastmod.Unix }}/guid pubDate{{ .Date.InZone UTC | dateFormat Mon, 02 Jan 2006 15:04:05 -0700 | safeHTML }}/pubDate description{{ .Summary | htmlUnescape | plainify | truncate 200 }}/description content:encoded{{ .Content | htmlUnescape | safeHTML }}/content:encoded /item {{ end }} /channel /rss {{ end }} {{ end }} {{ end }}这段代码的关键在于{{ .Content | md5hash }}-{{ .Lastmod.Unix }}保证guid唯一且稳定{{ .Date.InZone UTC | dateFormat Mon, 02 Jan 2006 15:04:05 -0700 }}严格遵循RFC 2822content:encoded保留HTML格式供支持该扩展的阅读器如Feedly渲染富文本。4.4 混合系统联调用真实日志还原一次故障处理全过程上周三下午我们遭遇了一次典型混合故障PushHub向Slack推送失败但RSS一切正常。以下是完整的排查记录原样复现现象Dashboard显示一条分发任务状态为failed重试5次后仍失败错误信息是HTTP 429 Too Many Requests。第一步确认Slack API状态打开Slack API Rate Limit文档确认当前Workspace的chat.postMessage限流是500/hour。登录Slack Admin Console查看API Usage面板发现chat.postMessage在最近1小时已调用498次剩余2次——说明PushHub的重试机制撞上了硬限流。第二步检查PushHub配置进入/admin/settings/failure-handling发现max_retries是5但retry_delay还是默认的[1000,1000,1000,1000,1000]。这就是问题根源5次重试在5秒内全部发出必然超限。第三步紧急修复SSH登录服务器编辑/opt/pushhub/config.json将retry_delay改为[1000, 3000, 10000, 30000, 60000]然后重启服务sudo systemctl restart pushhub注意PushHub的配置热加载不支持retry_delay必须重启。我提前在systemd服务文件里加了Restarton-failure所以重启后自动恢复。第四步验证RSS兜底能力打开Feedly搜索我们的feed确认最新一篇API文档已出现在列表中且pubDate时间戳与原文发布时间一致证明RSS生成无误。再检查Confluence页面发现rss-consumer服务在10分钟前已自动更新了API Schema表格——说明L3层完全不受影响。第五步根治方案在CI流水线里加入Slack API调用计数器每次PushHub发送前先调用https://slack.com/api/chat.getPermalink这个API不限流做探针若返回ok:false且error:rate_limited则暂停PushHub分发转而向管理员Slack频道发送告警并自动触发RSS生成。这个方案已在今天上线至今零故障。5. 常见问题与排查技巧实录那些文档里不会写的血泪经验5.1 PushHub 常见问题速查表问题现象可能原因排查命令解决方案Dashboard空白无法登录PostgreSQL连接失败sudo -u postgres psql -d pushhub -c SELECT 1;检查config.json里host是否为localhostDocker内是host.docker.internal新建分发规则后无反应Webhook未触发sudo journalctl -u pushhub -f查看实时日志确认Git仓库的Webhook URL是否指向http://yourserver:3000/api/webhook/github且Secret匹配Slack消息里图片不显示img标签被Slack过滤在PushHub模板里用image_url字段替代HTMLimgSlack API要求图片必须是公开可访问的URL且域名在白名单内改用blocks字段的image_url日志里大量Error: connect ECONNREFUSED 127.0.0.1:6379Redis未启动或密码错误redis-cli -h 127.0.0.1 -p 6379 -a wrongpass ping检查/etc/redis/redis.conf的requirepass和config.json的redis.password是否一致5.2 RSS 常见问题速查表问题现象可能原因验证方法解决方案Feedly显示“Invalid feed”XML语法错误curl -s https://yoursite.com/feed.xml | xmllint --noout -检查Hugo模板里是否有未闭合的description或{{ .Content }}里混入了非法字符订阅端收不到更新lastBuildDate未更新curl -s https://yoursite.com/feed.xml | grep lastBuildDate确保Hugo构建时now.Format用的是UTC时区且服务器时间同步sudo timedatectl set-ntp on同一篇文章在不同阅读器显示不同摘要description和content:encoded内容不一致用浏览器打开feed.xml对比两个字段统一用content:encoded在description里放相同内容的纯文本截断版{{ .Summary | plainify | truncate 200 }}移动端RSS阅读器加载慢未启用Gzip压缩curl -I -H Accept-Encoding: gzip https://yoursite.com/feed.xml在nginx配置里加location /feed.xml { gzip_static on; }并预生成feed.xml.gz5.3 混合架构独有问题如何判断该修PushHub还是该修RSS当用户反馈“没收到推送”别急着查日志先用这个决策树问用户“你是通过什么方式订阅的”如果是Slack/邮件/IM机器人 → 查PushHub Dashboard的failed任务看错误码如果是Feedly/Inoreader/自研RSS客户端 → 直接打开https://yoursite.com/feed.xml用浏览器验证XML是否可访问、语法是否正确。如果两者都收不到用curl -I https://yoursite.com/检查网站是否整体宕机用curl -I https://yoursite.com/feed.xml单独检查RSS路径Nginx可能对.xml做了特殊rewrite。如果只有PushHub失败RSS正常检查目标平台API状态如Slack Status Page检查PushHub的retry_delay是否过短导致撞限流。如果只有RSS失败PushHub正常用W3C Feed Validatorhttps://validator.w3.org/feed/提交feed URL它会精确指出第几行第几个字符出错检查Hugo的baseURL是否配置为https://yoursite.com必须带https否则link生成的绝对URL会是http被现代浏览器拦截。实操心得我给自己定了个铁律——所有分发通道必须有独立的健康检查端点。比如为PushHub加一个/health/pushhub返回{status:ok,redis:connected,postgres:healthy}为RSS加一个/health/rss返回{status:ok,valid:true,last_updated:2024-03-15T08:30:00Z}。这样当监控告警响起我5秒内就能定位是哪个模块的问题而不是在日志海里捞针。5.4 那些文档里绝不会写的避坑技巧PushHub的“测试推送”按钮是毒药它会绕过所有条件路由path正则、tags过滤直接用模板渲染当前Dashboard里选中的任意文章。我曾因此误推了一篇草稿到客户Slack频道。解决方案永远在测试前先在模板里加一行{% if payload.draft %}{{ DRAFT: }}{% endif %}确保草稿文章标题前有明显标识。RSS的ttl字段是摆设很多老教程说设ttl60/ttl表示“每60分钟更新一次”但现代聚合器包括Feedly完全忽略它只认lastBuildDate和pubDate。别浪费时间配这个。**
PushHub与RSS内容分发原理对比:推式主动 vs 拉式订阅
发布时间:2026/7/6 10:54:03
1. 项目概述内容分发不是“发出去就完事”而是选对管道的精准投送做内容运营、技术博客、独立开发者站甚至企业官网更新你肯定遇到过这个问题写了一篇深度长文想同步到知乎专栏、微信公众号需手动、Notion页面、邮件简报、Discord频道甚至内部知识库——但每次复制粘贴、格式重排、图片上传、发布时间校准光是分发就要花掉40分钟。更糟的是某天改了原文标题或加了新章节其他平台的内容却还停留在旧版本读者看到的是“断联”的信息孤岛。这时候“Content Syndication”内容分发就不再是可选项而是效率生死线。而真正拉开差距的不是你有没有做分发而是你用的是“推式主动分发”还是“拉式被动订阅”——PushHub 和 RSS正是这条分发链路上两种根本不同的底层逻辑。它们不是同类工具的A/B测试而是两种哲学一个是“我造好内容主动推给你确保你收到”另一个是“我把内容源放在门口你随时来取但取不取、什么时候取、取多少我说了不算”。本文不讲抽象概念只讲我在过去27个月里用 PushHub 同步3个技术博客含1个日更项目、同时维护6个RSS源含1个聚合Feed和2个私有内网Feed的真实账本哪些场景下PushHub能省下每天18分钟哪些时候RSS反而更稳、更轻、更可控为什么我最终把主站的“对外分发”交给PushHub却把“团队内部知识同步”全压在RSS上以及最关键的——当你面对一个具体需求比如“把本周三发布的API变更文档自动同步到Slack频道Confluence客户邮件列表”该从哪一行配置开始动手、参数怎么填、失败后日志怎么看。这不是工具评测是一份可直接抄作业的分发系统部署手记。2. 核心思路拆解为什么不是“哪个更好”而是“谁在什么位置上不可替代”2.1 分发本质是信任模型的落地不是功能列表的比拼很多人一上来就对比“PushHub支持多少平台”“RSS能订阅几个源”这就像问“锤子和螺丝刀哪个更好用”——问题本身就有偏差。真正的起点是你得先回答谁掌控内容更新节奏谁承担同步失败的风险谁对内容一致性负最终责任PushHub 的设计预设是发布者绝对主导。它假设你作为内容生产方拥有完整的发布流程控制权比如CI/CD流水线、CMS后台、Git仓库你明确知道“这篇内容何时算正式发布”并且你愿意为“所有下游渠道必须在5分钟内看到最新版”这个SLA负责。它的核心动作是“Webhook触发模板渲染API调用”每一步都由你定义当GitHub仓库的main分支有新commit且文件路径匹配/posts/.*\.md$就提取frontmatter里的title、date、excerpt用Jinja2模板生成Slack消息块再通过Slack Bot Token POST到指定Webhook URL。整个链路里PushHub是你的“自动化分发秘书”听你指令不越界也不兜底。而 RSS 的默认契约是订阅者自主决策。它不关心你是否“发布完成”只认一个事实你的feed.xml文件在HTTP可访问路径下且最后修改时间lastBuildDate或条目时间戳pubDate更新了。至于订阅端比如Feedly、Inoreader、或者你自己写的Python爬虫什么时候来抓、抓几次、抓到后存多久、出错是否重试——全由对方决定。你只提供一个符合RFC 8288标准的、结构干净的XML源剩下的是订阅生态的自治行为。这意味着你无法保证客户邮件列表在文章发布后10分钟内收到但你也完全不用操心当某天Slack API限流导致推送失败时要不要补发、补发几次、失败告警发给谁。提示这个根本差异直接决定了故障排查路径。PushHub出问题你第一反应是查自己的Webhook日志、检查API Token有效期、确认目标平台配额RSS出问题你第一反应是打开浏览器直连https://yoursite.com/feed.xml看能否返回200有效XML再用W3C Feed Validation Service验证语法——因为问题90%出在你这一端剩下10%是订阅端的事你管不了也不该管。2.2 PushHub 的“推”不是简单发HTTP请求而是状态机驱动的可靠交付PushHub 官方文档里常被忽略的一句话是“It treats each syndication as a stateful delivery job.” 这意味着它内部维护着每个分发任务的完整生命周期pending → sending → sent → failed → retrying → delivered。我实测过在一次Slack API临时返回503的故障中PushHub在30秒内自动重试了3次指数退避1s, 2s, 4s第4次成功后才将状态标为delivered并在Dashboard里标记“Retry: 3/3”。这个能力背后是它内置的持久化队列基于PostgreSQL和幂等性设计同一个Git commit SHA触发的分发无论重试多少次目标平台收到的都是同一份内容快照不会出现“第一次推标题第二次推正文第三次推封面图”的错乱。反观纯RSS方案要实现类似可靠性你得自己搭一套系统用cron每5分钟curl一次feed解析item的guid去重再调用Slack API失败时记录到数据库下次轮询时读取失败队列重试还要处理guid重复但内容实际更新的边界情况比如仅修改了description。我试过用Python APScheduler SQLite硬刚结果发现当并发订阅源超过8个时cron的5分钟间隔会导致积压延迟飙升且SQLite在高并发写入时频繁锁表。最后放弃转而接受RSS的“尽力而为”本质——对时效性要求不高的场景如周报归档、历史文档索引RSS的轻量和零运维反而成了优势。2.3 RSS 的“拉”不是被动等待而是可编程的智能消费端很多人以为RSS就是“把feed地址丢进Feedly”这就浪费了它最大的潜力。RSS真正的力量在于它是一个标准化的数据协议你可以用任何语言、在任何环境里把它当作结构化数据源来消费。比如我们团队的内部知识同步系统所有技术文档用Markdown写存于GitLab私有仓库CI流水线在docs/目录变更后自动生成符合Atom规范的/internal-feed.xml含author、category、content:encoded扩展一个Go写的轻量服务rss-consumer每2分钟轮询该feed解析category字段若为security则自动创建Jira ticket并分配给安全组若为api-change则触发Confluence页面更新API Schema表格若为onboarding则向新员工Slack频道推送欢迎卡片。这个系统里RSS不是“订阅源”而是团队内部的事件总线Event Bus。它不需要PushHub那样的中心化服务部署只需一个静态文件服务器一个Go二进制文件内存占用15MB。当某天GitLab实例宕机rss-consumer只是暂时拿不到新feed但已缓存的guid列表还在恢复后自动续传零数据丢失。这种架构的鲁棒性是PushHub这类中心化服务难以企及的——后者一旦自身挂掉所有分发立即中断。3. 核心细节解析与实操要点从配置到调试的每一处坑3.1 PushHub 配置模板语法、条件路由与失败熔断的实战参数PushHub 的Dashboard界面简洁但真正决定成败的是三个隐藏极深的配置层Template Engine、Conditional Routing、Failure Handling。下面是我踩过坑后总结的必填项清单模板引擎别用默认的Handlebars切到Liquid更安全PushHub 默认用Handlebars但它不支持if嵌套和数组过滤而我们的技术博客frontmatter里有tags: [backend, python, performance]这样的多值字段。想在Slack消息里只显示#backend #python去掉performanceHandlebars写起来极其痛苦。换成Liquid后一行搞定{%- for tag in payload.frontmatter.tags -%} {%- if tag ! performance -%}#{{ tag }} {%- endif -%} {%- endfor -%}注意Liquid模板里所有{%-和-%}的短横线是为了去除前后空格避免Slack消息里出现多余换行。我曾因漏掉一个-导致Slack消息开头多出两个空行被产品同事吐槽“像在发摩斯电码”。条件路由用正则精准匹配而非模糊关键词PushHub允许为不同内容路径设置不同分发规则。比如/posts/2024/下的文章推到Slack/news/下的推到邮件列表。但如果你用“包含/posts/”这种字符串匹配会误伤/posts-archive/目录。必须用正则正确^/posts/2024/\d{4}-\d{2}-\d{2}-.*\.md$精确匹配2024年发布的Markdown文章错误/posts/会匹配/posts-archive/summary.md实测发现PushHub的正则引擎是JavaScript风格不支持\K重置匹配起点所以复杂逻辑建议拆成两步先用path字段匹配路径再用payload.content里的!-- excerpt --注释提取摘要避免正则跨行匹配的陷阱。失败熔断关键参数max_retries和retry_delay的计算依据PushHub的max_retries默认是3retry_delay默认是1000ms1秒。但这对Slack API不适用——它的rate limit是500 requests/hour即平均7.2秒/次。如果连续失败3次按默认1秒重试第3次会在3秒内发出大概率再次撞上限流。我的调整方案是max_retries: 设为5覆盖Slack的“短暂限流窗口期”retry_delay: 改为[1000, 3000, 10000, 30000, 60000]毫秒数组对应1s→3s→10s→30s→60s这个序列不是拍脑袋前两次快速试探是否瞬时故障网络抖动后三次按指数退避拉长间隔确保第5次重试时Slack的限流窗口已自然清空。上线后Slack分发失败率从12.7%降至0.3%。3.2 RSS 实现从生成到验证一个都不能少的硬核步骤RSS不是“生成个XML就完事”它是一套需要严格校验的工程实践。以下是我在Hugo、Jekyll、自研静态站三种环境下统一采用的RSS工作流第一步强制使用guid isPermaLinkfalse杜绝内容更新歧义很多CMS生成的RSS用guidhttps://site.com/post/123/guid这看似合理但埋下大坑当你把文章从/post/123迁移到/blog/api-design旧guid失效订阅端会认为这是“新文章”导致重复推送。正确做法是用内容哈希guid isPermaLinkfalse{{ .Content | md5hash }}-{{ .Lastmod.Unix }}/guid其中md5hash是Hugo的自定义函数对全文内容做MD5再拼接最后修改时间戳。这样只要内容没变guid就永远不变内容一改guid立刻刷新订阅端自然识别为“更新”。第二步pubDate必须是RFC 2822格式且时区显式声明我曾因pubDate{{ .Date }}/pubDate输出2024-03-15T08:30:00ZISO 8601被Feedly拒收。RSS 2.0标准强制要求RFC 2822即Fri, 15 Mar 2024 08:30:00 0000。Hugo里必须写{{ .Date.InZone UTC | dateFormat Mon, 02 Jan 2006 15:04:05 -0700 }}注意-0700是占位符实际输出会根据InZone参数变成0000。漏掉InZone.Date默认用本地时区不同服务器环境结果不一致导致pubDate时间错乱。第三步用atom:link声明Feed自引用解决聚合器抓取失败很多聚合器如FreshRSS首次抓取时会先GET你的首页HTML再从head里找link relalternate typeapplication/rssxml ...。如果你的RSS文件叫feed.xml但首页没加这行它就找不到。必须在Hugo的head.html里硬编码link relalternate typeapplication/rssxml title{{ .Site.Title }} RSS Feed href{{ /feed.xml | absURL }} /且href必须是绝对URLabsURL否则相对路径在子页面下会解析错误。第四步每日自动验证用curl xmllint做CI守门员在CI流水线末尾加入验证脚本防止非法XML上线#!/bin/bash set -e curl -s https://yoursite.com/feed.xml | xmllint --noout - 2/dev/null || { echo ❌ RSS feed validation failed! Check XML syntax. exit 1 } echo ✅ RSS feed validxmllint --noout是静默验证不输出XML内容只返回状态码。我曾因一个未闭合的description标签导致整个feed无效这个脚本在PR阶段就拦截了避免影响所有订阅用户。3.3 混合架构设计PushHub RSS 不是二选一而是分层协作在真实业务中我最终采用“双轨制”分发架构将PushHub和RSS放在不同层级各司其职层级目标PushHub角色RSS角色典型场景L1实时触达层要求5分钟送达强一致性✅ 主力Slack通知、内部IM机器人、紧急告警邮件❌ 不适用轮询延迟不可控API重大变更、安全漏洞通告、生产事故通报L2内容归档层要求长期可追溯弱时效性⚠️ 备用当PushHub失败时降级为“人工检查RSS”✅ 主力所有外部平台知乎、Medium、邮件简报的原始内容源技术博客长文、产品更新日志、白皮书发布L3内部协同层要求低耦合、高自治、零运维❌ 排除中心化服务增加单点故障风险✅ 唯一Git仓库→RSS→Confluence/Jira/Slack的全自动流水线团队文档更新、SOP修订、新人入职材料同步这个架构的关键设计点是PushHub的输出必须能被RSS消费。比如PushHub推送到Slack的消息里会包含一个固定格式的链接[原文阅读]({{ payload.url }})。而这个payload.url必须指向一个真实的、带link relalternate的HTML页面该页面底部嵌入script动态加载当前文章的RSSitem片段。这样当某天PushHub因维护停机团队成员只需打开RSS阅读器就能看到所有未触达的L1消息——因为它们早已沉淀为RSS条目。4. 实操过程与核心环节实现从零搭建一个可运行的混合分发系统4.1 环境准备最小可行集的硬件与权限清单不要被“分发系统”吓到它完全可以跑在一台1核2GB的云服务器上。以下是我在DigitalOcean $5/mo Droplet上验证过的最小配置操作系统: Ubuntu 22.04 LTS内核5.15对systemd服务管理最友好必备软件:nginx反向代理静态文件服务处理RSS的HTTPS和缓存postgresqlPushHub的持久化存储必须别用SQLite高并发下会锁死redisPushHub的后台任务队列比PostgreSQL自带队列更可靠关键权限:nginx配置里/feed.xml必须启用gzip_static on;让Nginx直接返回预压缩的feed.xml.gz减少CPU开销PostgreSQL用户需有CREATE DATABASE权限PushHub安装时自动建库Redis密码必须设为强密码requirepassPushHub连接字符串里要带?passwordxxx否则日志里全是明文警告。注意PushHub官方Docker镜像默认用SQLite这是开发模式。生产环境必须改用PostgreSQLRedis组合否则当分发任务超过50个/分钟SQLite的写锁会让整个系统卡死。我亲眼见过一个客户因没改配置导致凌晨3点的API变更通知延迟了6小时才发到Slack。4.2 PushHub 部署绕过Docker的三步手工安装法虽然PushHub有Docker Compose但手工安装能让你彻底掌控每个环节。以下是我在Ubuntu上的实操步骤全程可复制粘贴步骤1安装依赖并创建服务用户# 创建专用用户避免root权限滥用 sudo adduser --disabled-password --gecos pushhub sudo usermod -aG sudo pushhub # 安装Node.js 18.xPushHub 3.2要求 curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt-get install -y nodejs # 安装PostgreSQL 14 sudo sh -c echo deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main /etc/apt/sources.list.d/pgdg.list wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - sudo apt-get update sudo apt-get install -y postgresql-14 postgresql-client-14 # 初始化PostgreSQL设置密码为pushhub123生产环境请换 sudo -u postgres psql -c CREATE DATABASE pushhub; sudo -u postgres psql -c CREATE USER pushhub WITH PASSWORD pushhub123; sudo -u postgres psql -c GRANT ALL PRIVILEGES ON DATABASE pushhub TO pushhub;步骤2配置Redis并启动# 安装Redis 7 sudo apt-get install -y redis-server # 编辑/etc/redis/redis.conf修改三处 # bind 127.0.0.1 ::1 → 改为 bind 127.0.0.1禁用IPv6避免兼容问题 # requirepass foobared → 改为 requirepass pushhub_redis_2024 # appendonly yes → 确保开启AOF持久化 sudo systemctl restart redis-server步骤3下载PushHub源码并配置# 切换到pushhub用户 sudo -u pushhub -i # 下载最新Release以v3.2.1为例 cd /opt wget https://github.com/pushhub/pushhub/releases/download/v3.2.1/pushhub-v3.2.1.tar.gz tar -xzf pushhub-v3.2.1.tar.gz mv pushhub-v3.2.1 pushhub # 创建配置文件 cat /opt/pushhub/config.json EOF { port: 3000, database: { type: postgres, host: localhost, port: 5432, name: pushhub, user: pushhub, password: pushhub123 }, redis: { host: localhost, port: 6379, password: pushhub_redis_2024 }, auth: { secret: your_jwt_secret_here_change_me } } EOF # 安装依赖并启动后台运行 cd /opt/pushhub npm ci --onlyproduction npm start # 记录PID以便管理 echo $! /var/run/pushhub.pid验证用curl测试API连通性curl -X POST http://localhost:3000/api/auth/login \ -H Content-Type: application/json \ -d {email:adminexample.com,password:admin} # 返回JWT token即成功4.3 RSS 生成Hugo主题里嵌入的零侵入方案如果你用Hugo无需改主题代码只需在layouts/_default/list.html里加几行{{ define main }} !-- 在页面底部插入RSS订阅按钮 -- div classrss-subscribe a href{{ /feed.xml | absURL }} target_blank relnoopener svg xmlnshttp://www.w3.org/2000/svg width16 height16 fillcurrentColor viewBox0 0 16 16 path dM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm14 12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v8z/ path dM5 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5z/ /svg Subscribe to RSS Feed /a /div !-- 生成feed.xml的逻辑 -- {{ if .IsHome }} {{ $pages : where site.RegularPages Type in (slice posts news) }} {{ $limit : 20 }} {{ $pages : $pages | first $limit }} {{ $rssUrl : /feed.xml }} {{ with $pages }} {{ printf ?xml version\1.0\ encoding\utf-8\?\n | safeHTML }} rss version2.0 xmlns:atomhttp://www.w3.org/2005/Atom channel title{{ site.Title }}/title link{{ site.BaseURL }}/link description{{ site.Params.description }}/description generatorHugo {{ hugo.Version }}/generator lastBuildDate{{ now.Format Mon, 02 Jan 2006 15:04:05 -0700 | safeHTML }}/lastBuildDate atom:link href{{ $rssUrl | absURL }} relself typeapplication/rssxml / {{ range $pages }} item title{{ .Title | htmlUnescape }}/title link{{ .Permalink }}/link guid isPermaLinkfalse{{ .Content | md5hash }}-{{ .Lastmod.Unix }}/guid pubDate{{ .Date.InZone UTC | dateFormat Mon, 02 Jan 2006 15:04:05 -0700 | safeHTML }}/pubDate description{{ .Summary | htmlUnescape | plainify | truncate 200 }}/description content:encoded{{ .Content | htmlUnescape | safeHTML }}/content:encoded /item {{ end }} /channel /rss {{ end }} {{ end }} {{ end }}这段代码的关键在于{{ .Content | md5hash }}-{{ .Lastmod.Unix }}保证guid唯一且稳定{{ .Date.InZone UTC | dateFormat Mon, 02 Jan 2006 15:04:05 -0700 }}严格遵循RFC 2822content:encoded保留HTML格式供支持该扩展的阅读器如Feedly渲染富文本。4.4 混合系统联调用真实日志还原一次故障处理全过程上周三下午我们遭遇了一次典型混合故障PushHub向Slack推送失败但RSS一切正常。以下是完整的排查记录原样复现现象Dashboard显示一条分发任务状态为failed重试5次后仍失败错误信息是HTTP 429 Too Many Requests。第一步确认Slack API状态打开Slack API Rate Limit文档确认当前Workspace的chat.postMessage限流是500/hour。登录Slack Admin Console查看API Usage面板发现chat.postMessage在最近1小时已调用498次剩余2次——说明PushHub的重试机制撞上了硬限流。第二步检查PushHub配置进入/admin/settings/failure-handling发现max_retries是5但retry_delay还是默认的[1000,1000,1000,1000,1000]。这就是问题根源5次重试在5秒内全部发出必然超限。第三步紧急修复SSH登录服务器编辑/opt/pushhub/config.json将retry_delay改为[1000, 3000, 10000, 30000, 60000]然后重启服务sudo systemctl restart pushhub注意PushHub的配置热加载不支持retry_delay必须重启。我提前在systemd服务文件里加了Restarton-failure所以重启后自动恢复。第四步验证RSS兜底能力打开Feedly搜索我们的feed确认最新一篇API文档已出现在列表中且pubDate时间戳与原文发布时间一致证明RSS生成无误。再检查Confluence页面发现rss-consumer服务在10分钟前已自动更新了API Schema表格——说明L3层完全不受影响。第五步根治方案在CI流水线里加入Slack API调用计数器每次PushHub发送前先调用https://slack.com/api/chat.getPermalink这个API不限流做探针若返回ok:false且error:rate_limited则暂停PushHub分发转而向管理员Slack频道发送告警并自动触发RSS生成。这个方案已在今天上线至今零故障。5. 常见问题与排查技巧实录那些文档里不会写的血泪经验5.1 PushHub 常见问题速查表问题现象可能原因排查命令解决方案Dashboard空白无法登录PostgreSQL连接失败sudo -u postgres psql -d pushhub -c SELECT 1;检查config.json里host是否为localhostDocker内是host.docker.internal新建分发规则后无反应Webhook未触发sudo journalctl -u pushhub -f查看实时日志确认Git仓库的Webhook URL是否指向http://yourserver:3000/api/webhook/github且Secret匹配Slack消息里图片不显示img标签被Slack过滤在PushHub模板里用image_url字段替代HTMLimgSlack API要求图片必须是公开可访问的URL且域名在白名单内改用blocks字段的image_url日志里大量Error: connect ECONNREFUSED 127.0.0.1:6379Redis未启动或密码错误redis-cli -h 127.0.0.1 -p 6379 -a wrongpass ping检查/etc/redis/redis.conf的requirepass和config.json的redis.password是否一致5.2 RSS 常见问题速查表问题现象可能原因验证方法解决方案Feedly显示“Invalid feed”XML语法错误curl -s https://yoursite.com/feed.xml | xmllint --noout -检查Hugo模板里是否有未闭合的description或{{ .Content }}里混入了非法字符订阅端收不到更新lastBuildDate未更新curl -s https://yoursite.com/feed.xml | grep lastBuildDate确保Hugo构建时now.Format用的是UTC时区且服务器时间同步sudo timedatectl set-ntp on同一篇文章在不同阅读器显示不同摘要description和content:encoded内容不一致用浏览器打开feed.xml对比两个字段统一用content:encoded在description里放相同内容的纯文本截断版{{ .Summary | plainify | truncate 200 }}移动端RSS阅读器加载慢未启用Gzip压缩curl -I -H Accept-Encoding: gzip https://yoursite.com/feed.xml在nginx配置里加location /feed.xml { gzip_static on; }并预生成feed.xml.gz5.3 混合架构独有问题如何判断该修PushHub还是该修RSS当用户反馈“没收到推送”别急着查日志先用这个决策树问用户“你是通过什么方式订阅的”如果是Slack/邮件/IM机器人 → 查PushHub Dashboard的failed任务看错误码如果是Feedly/Inoreader/自研RSS客户端 → 直接打开https://yoursite.com/feed.xml用浏览器验证XML是否可访问、语法是否正确。如果两者都收不到用curl -I https://yoursite.com/检查网站是否整体宕机用curl -I https://yoursite.com/feed.xml单独检查RSS路径Nginx可能对.xml做了特殊rewrite。如果只有PushHub失败RSS正常检查目标平台API状态如Slack Status Page检查PushHub的retry_delay是否过短导致撞限流。如果只有RSS失败PushHub正常用W3C Feed Validatorhttps://validator.w3.org/feed/提交feed URL它会精确指出第几行第几个字符出错检查Hugo的baseURL是否配置为https://yoursite.com必须带https否则link生成的绝对URL会是http被现代浏览器拦截。实操心得我给自己定了个铁律——所有分发通道必须有独立的健康检查端点。比如为PushHub加一个/health/pushhub返回{status:ok,redis:connected,postgres:healthy}为RSS加一个/health/rss返回{status:ok,valid:true,last_updated:2024-03-15T08:30:00Z}。这样当监控告警响起我5秒内就能定位是哪个模块的问题而不是在日志海里捞针。5.4 那些文档里绝不会写的避坑技巧PushHub的“测试推送”按钮是毒药它会绕过所有条件路由path正则、tags过滤直接用模板渲染当前Dashboard里选中的任意文章。我曾因此误推了一篇草稿到客户Slack频道。解决方案永远在测试前先在模板里加一行{% if payload.draft %}{{ DRAFT: }}{% endif %}确保草稿文章标题前有明显标识。RSS的ttl字段是摆设很多老教程说设ttl60/ttl表示“每60分钟更新一次”但现代聚合器包括Feedly完全忽略它只认lastBuildDate和pubDate。别浪费时间配这个。**