惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
D
Docker
云风的 BLOG
云风的 BLOG
F
Fortinet All Blogs
F
Full Disclosure
H
Hackread – Cybersecurity News, Data Breaches, AI and More
P
Proofpoint News Feed
Microsoft Azure Blog
Microsoft Azure Blog
WordPress大学
WordPress大学
The GitHub Blog
The GitHub Blog
L
LangChain Blog
H
Help Net Security
B
Blog
T
Tailwind CSS Blog
V
V2EX
博客园_首页
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
Recent Announcements
Recent Announcements
aimingoo的专栏
aimingoo的专栏
美团技术团队
A
About on SuperTechFans
C
Cybersecurity and Infrastructure Security Agency CISA
K
Kaspersky official blog
I
InfoQ
Project Zero
Project Zero
I
Intezer
Google DeepMind News
Google DeepMind News
博客园 - 【当耐特】
Hugging Face - Blog
Hugging Face - Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Threat Research - Cisco Blogs
Last Week in AI
Last Week in AI
C
Cyber Attacks, Cyber Crime and Cyber Security
G
GRAHAM CLULEY
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
AWS News Blog
AWS News Blog
Spread Privacy
Spread Privacy
S
Securelist
Recorded Future
Recorded Future
D
Darknet – Hacking Tools, Hacker News & Cyber Security
博客园 - 叶小钗
S
Security Affairs
Blog — PlanetScale
Blog — PlanetScale
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
The Hacker News
The Hacker News

JustGoIdea

未曾离去 异乡的乡音 风吹幡动 航札(一) 两朵红花 读《陶庵梦忆》(二、锺山) 里斯本的春天 填不满的锚点 读《陶庵梦忆》(一、梦忆序) AI 时代的人类处境:科技、宗教与生命意义的重构 題丙午春節
重做 Stream 页面
L,.G. · 2026-05-31 · via JustGoIdea

去年我就在 Bear Blog 里折腾过一个 Stream 页面。原因无他,只是想减少对社交媒体的依赖。

然而,当时的实现方案大致是这样的:利用 Tana Outliner 的 Make API request 功能,把特定笔记推送到 Telegram Channel;再借助 Channel 的 RSS,让 Cloudflare Worker 读取,然后嵌入到 Bear Blog 的 Stream 页面。

很明显,这恰恰违背了我「减少对社交媒体依赖」的初衷。

我真正想要的是:让 Tana Outliner 里带有 #post 标签的笔记,能直接进入我自己博客的 Stream 页面。中间可以保留必要的技术层,但不应该再有一个社交媒体充当事实来源。

于是,真正的问题变成了:Stream 页面中的内容源头,到底应该在哪里?

让 Tana Outliner 成为源头

对我来说,答案只能是 Tana Outliner。

因为内容最初就诞生在 Outliner 里。某个节点是否值得公开,通常也是在整理笔记时顺手判断出来的。既然如此,最自然的动作就不是「复制到另一个地方再发布」,而是给这个节点打上一个专门的标签,然后直接在 Outliner 中手动触发推送。

这个「手动」很重要。它不是自动同步,也不意味着所有笔记默认公开。它保留了一个微小却关键的判断动作:我仍然需要点一下按钮,确认这段内容可以进入公共页面。

那么,这条链路就应该是:

Tana Outliner 中的特定内容 -> Cloudflare Worker -> D1 / R2 -> Bear Blog Stream page

Bear Blog 无须承担内容管理的职责,它只是页面,是展示层。Outliner 才是输入端和判断端。中间的 Worker 和数据库则负责接收、保存、更新、删除,以及处理网页展示所需的各种细节。

这样一来,系统的边界就清晰多了。

为什么不用 Playwright

一开始,我也想过用 Playwright 模拟登录 Bear Blog 后台,再直接修改某个页面。

技术上这当然可行。Bear Blog 提供了自定义页面和注入脚本的能力,如果非要用浏览器自动化,也不是做不到。

但这个方向很快就暴露出问题:它是在模拟人的操作,而不是重新定义数据流。

如果每次发布都要让脚本去「打开后台、找到页面、改 HTML、保存」,看起来像是自动化,实际上只是给手工操作包了一层外壳。它并没有回答更根本的问题:内容的状态由谁维护?更新时如何识别出是同一条?删除时如何形成闭环?图片和媒体文件又归谁所有?

所以我最终没有把 Bear Blog 当作后台,而是把它当作前台。

Bear Blog 的 Stream 页面里只需要放一个容器,再通过全局脚本加载内容。页面本身很轻,数据由 Cloudflare Worker 提供。这样,Bear Blog 依然保持着它原本的干净,复杂性也不会被硬塞进一个 Markdown 页面里。

需要的不只是追加

最初看起来,Stream 页面似乎只需要「追加」。

比如,Outliner 里有一条打了 #post 标签的内容,点一下按钮,Worker 收到文本,存进 D1,页面刷新后便显示出来。到这里为止,一切都很简单。

但真正用上几分钟就会发现,追加只是最表层的需求,真正需要的是状态管理。

第一,同一条内容可能会被修改。 错别字、日期、措辞等,都可能在推送之后才发现。如果每次重新推送都新增一条,Stream 很快就会变成一堆重复记录。

所以每条内容都必须带上 Outliner 的节点 ID。这个 ID 会作为 source_id 存入数据库。Worker 收到请求后,如果数据库里已存在相同的 source_id,就更新旧记录;如果没有,才创建新记录。

这一步看似微不足道,却决定了这套系统能否长期使用。没有它,发布按钮就会带来清理负担;有了它,重新推送便只是一次修订。

第二,删除也要从源头发起。 Outliner 的 Make API request 只支持 POST、GET、PUT,没有 DELETE。于是我给 Worker 增加了一个专门的 POST 删除入口:照样传入当前节点 ID,由 Worker 在数据库里找到对应记录并软删除。

这比在数据库里手动删除更贴合工作流。既然发布发生在 Outliner 里,撤回也理应能在 Outliner 里完成。

第三,发布时间不应等于推送时间。

一开始,页面显示的是 Worker 接收请求的时间。这在技术上很自然,但在写作语境里却不对。我在 Outliner 的 #post 标签中本就设置了 Published Date 字段。重新推送一条旧内容只是修订,不应把它伪装成今天刚写出来的东西。

所以 payload 里需要把 Published Date 一并传给 Worker。页面展示时优先使用这个日期,只有当它缺失或解析失败时,才退回到接收时间。

当然还有一些其他细节,它们都不复杂,但共同决定了 Stream 页面究竟是「能跑的 demo」,还是「真正可以成为日常工作流一部分」的东西。

文本不是纯文本

另一个真实的问题来自 Outliner 的内容结构。

Outliner 里的正文是 bullet list,在界面里看起来层次分明,但如果只取 ${Content},有时会被压平成一串文本,中间夹杂着半角逗号和空格。对中文内容来说,这种 ,, 。, 的痕迹非常刺眼。

这说明,我不能把 Outliner 的内容简单理解成一段字符串,它更像是一棵带结构的节点树。

后来我把 ${sys:context} 也传给了 Worker,让它有机会从更完整的上下文里恢复段落、列表和图片。这样,Stream 页面才更接近 Bear Blog 原生文章的观感:日期、段落、图片都自然地呈现出来,而不是像一段被 API 挤压过的日志。

这里也藏着一个有意思的边界:我并不想把 Stream 做成一个复杂的富文本系统。

它不需要支持所有排版,不需要评论,不需要点赞,也不需要转发。它只需要把 Outliner 里最常见的表达形式完好地呈现出来:短段落、列表、图片,以及必要的链接。

够用,比完整更重要。

图片要有自己的归宿

Outliner 里的图片有自己的存储地址,但这些地址未必适合公开页面长期引用。即便当前能正常显示,也可能遇到跨域、权限、过期、缓存和可访问性等问题。把一个公开的 Stream 建立在这些临时地址之上,心里总是不踏实。

所以,图片需要在推送时被 Worker 接管。

当正文里出现 Markdown 图片,Worker 会尝试下载原图,并转存到我自己的 R2 路径下。这样,Stream 页面展示的图片就不再依赖 Outliner 的内部资源地址,而是进入了我自己可控的存储体系。

这一步让我重新意识到:所谓「发布」,并不是把文字从 A 复制到 B;发布意味着为内容承担起可访问性的责任。

文字要有稳定的记录,图片也要有稳定的位置。否则,页面今天还能打开,明天就可能只剩下一堆失效链接。

这不是给 Bear Blog 加功能

表面上看,这次是在给 Bear Blog 增加一个 Stream 页面。但更准确地说,是给我的写作系统增加了一个新的出口。

Bear Blog 仍然是 Bear Blog。它没有变成 CMS,也没有变成微博客平台。它只是通过一段脚本读取外部数据,把那些碎片内容展示出来。

Outliner 也仍然是 Outliner。它不需要变成发布后台,只是在 #post 上多了几个操作按钮:发布、更新、删除。

复杂性被收纳进了 Cloudflare Worker、D1 和 R2 这样的技术层,但这种复杂性属于代码、数据库和对象存储;一旦出了问题,我知道该去哪里排查,也清楚哪一层负责什么。

这比把状态分散在几个平台之间,更让我感到可控。

以前,一条内容在 Outliner、Telegram、Bear Blog 之间流转,我很难说清到底哪里才是源头。现在,源头回到了 Outliner:Bear Blog 是窗口,Cloudflare 是桥,D1 和 R2 是仓库。

系统不一定变少了,但关系更清楚了。

这次重做之后,Stream 页面对我来说不再是某个社交平台的影子,而是博客里一条更小、更可控的支流。内容依然从我的笔记系统出发,而公开,只是它经过判断之后的一种去向。


Previous | Next

#Tana #tech #workflow