
在算法推荐与短视频信息洪流无孔不入的时代,个人的注意力常常被动地在信息的信息茧房里被消耗殆尽。正因如此,那些依然坚守个人独立博客、坚持使用 RSS 阅读器订阅精神食粮的朋友们,显得格外珍贵而笃定。
通过 RSS(Really Simple Syndication)或者 Atom 协议,读者与作者之间建立起了一种纯粹、去中心化、毫无商业广告干扰的数字契约:作者安安静静地写字落盘,读者在自己挑选的阅读器里从容翻阅,既没有算法的粗暴洗脑,也没有点赞转发的焦虑绑架。
然而,在很多静态博客系统中,生成一份合格的订阅流往往成为一件笨重的事:许多现成的插件动辄引入庞大复杂的 XML 序列化类库,在生成订阅文件时频繁破坏字符转义;更让人遗憾的是,很多传统 RSS 生成器完全忽视了现代独立网络社区(IndieWeb)所推崇的开放标准——微格式(Microformats 2)。
微格式是一种极其优雅的思想:它不需要单独维护冗余的外部 XML 文件,而是直接利用语义化的 HTML 属性(如 h-entry、p-name、dt-published),将订阅元数据自然融入文章本身的页面结构之中。
为了让手账博客既能拥抱经典的 XML 订阅生态,又能站在现代语义 Web 的前沿,我用纯原生 Node.js 构建了一套兼顾 Atom 1.0 规范与 Microformats 2 微格式的极简全站订阅流流水线。
双轨订阅模型:Atom 规范与 Microformats 2
一套真正完善的现代独立手账站点,应当具备“双轨互补”的订阅分发能力:
零依赖纯原生订阅流生成脚本
我们可以编写一个完全基于 Node.js 原生 API 的生成器脚本 scripts/generate-feeds.mjs,在静态站编译的最后阶段以毫秒级极速生成所有提要:
import fs from 'node:fs/promises';
import path from 'node:path';
const SITE_URL = 'https://bailingyu.me';
const SITE_TITLE = '白泠钰的秋日手账';
const SITE_DESC = '治愈系前端生活家,记录水彩质感 UI、本地轻量技术与生活随笔';
const AUTHOR_NAME = '白泠钰 (泠不丁)';
const POSTS_DIR = 'src/posts';
const DIST_DIR = 'dist';
// 简易 XML 特殊字符安全转义
function escapeXml(unsafe) {
return unsafe.replace(/[<>&'"]/g, (c) => {
switch (c) {
case '<': return '<';
case '>': return '>';
case '&': return '&';
case '\\'': return ''';
case '"': return '"';
}
});
}
// 抽取 Markdown 中的元数据
async function parsePost(filePath) {
const content = await fs.readFile(filePath, 'utf-8');
const lines = content.split('\\n');
// 提取一级标题
const titleLine = lines.find(l => l.startsWith('# ')) || '# 秋日无题';
const title = titleLine.replace('# ', '').trim();
// 剥离代码块并提取前 160 字作为纯文本摘要
const noCode = content.replace(/```[\\s\\S]*?```/g, '');
const plainText = noCode.replace(/[#*`>\\[\\]\\(\\)]/g, '').replace(/\\s+/g, ' ').trim();
const summary = plainText.slice(0, 160) + '…';
// 获取文件修改时间作为发布时间戳
const stat = await fs.stat(filePath);
const isoDate = stat.mtime.toISOString();
const slug = path.basename(filePath, '.md');
const postUrl = `${SITE_URL}/${slug}.html`;
return { title, summary, isoDate, postUrl, slug };
}
async function generateAtomFeed(posts) {
const latestUpdated = posts[0]?.isoDate || new Date().toISOString();
const entriesXml = posts.map(post => `
<entry>
<title>${escapeXml(post.title)}</title>
<link href="${post.postUrl}" rel="alternate" type="text/html"/>
<id>${post.postUrl}</id>
<updated>${post.isoDate}</updated>
<summary type="text">${escapeXml(post.summary)}</summary>
<author>
<name>${escapeXml(AUTHOR_NAME)}</name>
<uri>${SITE_URL}</uri>
</author>
</entry>`).join('\\n');
return `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>${escapeXml(SITE_TITLE)}</title>
<subtitle>${escapeXml(SITE_DESC)}</subtitle>
<link href="${SITE_URL}/atom.xml" rel="self" type="application/atom+xml"/>
<link href="${SITE_URL}/"/>
<id>${SITE_URL}/</id>
<updated>${latestUpdated}</updated>
<author>
<name>${escapeXml(AUTHOR_NAME)}</name>
</author>
${entriesXml}
</feed>`;
}
async function main() {
const files = (await fs.readdir(POSTS_DIR))
.filter(f => f.endsWith('.md'))
.sort()
.reverse();
const posts = [];
for (const f of files.slice(0, 20)) { // 仅取最新 20 篇随笔
posts.push(await parsePost(path.join(POSTS_DIR, f)));
}
// 生成 Atom 1.0 XML 提要
const atomXml = await generateAtomFeed(posts);
await fs.writeFile(path.join(DIST_DIR, 'atom.xml'), atomXml, 'utf-8');
console.log(`✨ 成功生成包含 ${posts.length} 篇随笔的 Atom 1.0 聚合提要 -> dist/atom.xml`);
}
main().catch(console.error);
在 HTML 模板中注入微格式(Microformats 2)
除了外部的 XML 提要,我们在文章的通用 HTML 模版中,只需按照微格式的 Class 命名规则进行简单的结构标记:
<!– 文章详情页:内嵌标准的 Microformats 2 语义结构 –>
<article class="h-entry post-article">
<header>
<!– 标明文章标题 –>
<h1 class="p-name post-title">{{TITLE}}</h1>
<!– 标明发布者与时间元数据 –>
<div class="post-meta">
<span class="p-author h-card" style="display: none;">
<a class="u-url p-name" href="https://bailingyu.me">白泠钰</a>
</span>
<time class="dt-published" datetime="{{ISO_DATE}}">{{FRIENDLY_DATE}}</time>
</div>
</header>
<!– 标明核心文章正文载荷 –>
<div class="e-content post-body">
{{CONTENT_HTML}}
</div>
<footer class="post-footer">
<p>🍃 感谢你的阅读。你可以通过 <a href="/atom.xml" rel="alternate" type="application/atom+xml">RSS/Atom 提要</a> 订阅本站的所有秋日心绪。</p>
</footer>
</article>
串联构建与无感上线
将该脚本挂接在 Makefile 构建目标末尾:
# 完整的全站发布管线
build: dist-dirs compile-posts localize generate-feed optimize-images
@echo "✨ 博客全站编译与全格式订阅流生成圆满完成。"
generate-feed:
@node scripts/generate-feeds.mjs
每次在本地保存新写完的秋日随笔,在终端敲下 make,仅需十几毫秒,dist/atom.xml 便被同步刷新,而全站 HTML 内部的微格式语义也紧密对齐。
微格式为独立个人博客带来的深远影响,远远超出了单纯的订阅通知本身。在传统的中心化社交媒体架构中,你的每一篇动态、每一张配图都被囚禁在各自封闭的数据库围墙之内,外部程序若想抓取,往往面临严格的 API 频率限制甚至登录封锁。而借助 Microformats 2,你的整个静态博客本身就变成了一座天然面向人类阅读、同时也完全向开放语义机器人敞开大门的结构化数据矿藏。
更进一步,通过在页面上铺设 h-entry 与 u-url,你甚至可以直接接入现代去中心化互动协议(如 Webmention)。当另一位博主在他的博客中撰文引用或回应了你的某篇秋日手记时,他的服务器会自动向你的端点发送一条无害的轻量通知;双方无需注册同一个商业平台账号,就能在各自独立的数字花园之间,形成一种如同古人鸿雁传书般温润典雅的跨站对话。
无论远方的朋友是通过经典的 NetNewsWire 客户端拉取 XML,还是通过 IndieWeb 的去中心化漫游网关直接读取网页,他们都能在同一时间,毫无阻碍地接收到你在安静的秋夜里写下的文字。
去中心化的互联网之美,正是在于这种跨越协议、跨越商业平台的质朴互联。没有算法的操纵,没有数据被锁死的焦虑,只有两颗热爱文字的心灵,在开放标准的桥梁上默默相逢。用最纯净的技术协议守护好属于自己的文字阵地,我们便能在喧嚣浮躁的时代浪潮中,守住一份内心的笃定与安宁。
网硕互联帮助中心



评论前必须登录!
注册