内容管线:从 Feed 原文到可读、可搜、可增强
一、Entry 不是一个字符串
一个 Entry 可能同时包含:标题、摘要、正文、作者、作者头像、媒体、附件、分类、来源、语言、阅读状态、Readability 正文、翻译、摘要和收藏视图。内容展示必须先判断“有哪些事实”,再决定“用哪种模板”。
二、Readability 的位置
packages/readability 独立成包,依赖 Mozilla Readability、JSDOM/Linkedom、DOMPurify 和字符检测。桌面 Main 也依赖它,说明正文抽取可能在不同运行时按需要执行。
Readability 的输出写进 Entry 的派生字段,而不是把 Feed 原文替换掉。原因很现实:
- 原站 HTML 可能已经被截断;
- 抽取规则可能随库升级改变;
- 用户可能需要回看原始描述;
- 安全清洗必须与内容展示责任分离。
三、内容模板为什么拆开
桌面和移动端都能看到 Article、Social、Video、Picture、Normal 等内容形态。拆成模板有三个收益:
- 图片/视频的加载和占位策略不污染文章阅读;
- 社交内容的作者与媒体结构可以独立演进;
- 移动端列表性能优化可以针对最常见模板做虚拟化和 memo。
模板选择依赖 Entry 数据和用户 view 设置,因此“视图类型”不仅是 CSS 样式,它是领域配置与渲染策略之间的协议。
四、AI Summary 与 Translation 的缓存语义
摘要和翻译表都按 entryId + language 建联合唯一索引。这给 UI 一个明确的缓存模型:请求前检查本地是否有同语言结果,有就直接显示;没有则发起任务,结果落表并更新 Store;请求失败不影响原始 Entry。
语言不是全局单值:用户可能在中文界面阅读英文源,也可能为同一 Entry 生成多种语言结果。把语言放进唯一键,是比“Entry 上只有 summary 字段”更可持续的设计。
五、HTML 安全边界
内容来自外部源,渲染链必须假设 HTML 不可信。Folo 的依赖和工具层可见 dompurify、rehype-sanitize、xss 等清洗手段。它们应当分布在不同边界:
- 服务端 Meta/HTML 注入:对动态文本做 XSS 保护;
- 阅读器 HTML:清洗标签、属性和 URL;
- 外部图片/视频:通过代理、协议和 CSP 限制风险;
- Markdown/富文本编辑:使用统一解析与 schema,而不是信任用户输入。
安全不是“入口过滤一次”就结束,因为同一内容会在 API、SQLite、SSR、WebView 和原生文本组件之间流动。
六、搜索与内容索引
桌面 Renderer 有独立 search store/types,搜索结果区分 feeds、entries、subscriptions,并保留与 Entry/Feed 的关联信息。搜索不是在每个页面里写一套过滤,而是把“跨领域检索”提升到应用级能力。
这带来的设计问题是索引来源:
- 本地搜索适合已 hydrate 的 Feed/Entry;
- 服务端搜索适合发现、推荐和未在本地的内容;
- 分享页搜索/Meta 更关心 URL 对应的公开资源。
读代码时要把“查询数据”和“内容展示”分开,避免因为一个搜索框就让所有领域模块互相依赖。
七、内容管线的可迁移经验
- 永远保留原始事实,派生结果单独存储;
- 让派生结果带有语言、版本或时间信息;
- 让每种内容形态拥有自己的展示模板;
- 把清洗、安全和代理放在内容边界;
- 让 UI 能在增强结果缺失时优雅降级。