起点

最初是要解决一个小问题:站内 notes 链接在 Telegram 中生成的卡片,宽度和内容密度不一致。后来,这些问题修复后,我想干脆再增加在Discord上的同步功能吧。

有些卡片能显示完整的标题和简介,有些只剩下一个日期加一行空泛的说明文字。表现上看起来像是 bug,但根源在于不同类型的 note 走的是同一套 metadata 生成逻辑,而这套逻辑并没有为"没有手写简介"的情况做好兜底。

从这个具体的卡片问题出发,排查逐步扩展成对整条通知链路的重新梳理——metadata 怎么拆、推送文案怎么收敛、Discord 怎么接入、以及相邻仓库怎样复用同一套设计。

网页标题和卡片标题是两件事

排查过程中发现的第一个结构性问题是:网页详情页的标题和 Telegram 卡片的标题共用了同一套生成逻辑。对于有手写标题的文章,这没有影响;但对于 memo 类型的 note,一旦为了让卡片看起来有内容而把正文前几句话抬成标题,网页详情页的标题也会跟着变,最终导致页面上出现一段很长的"标题",既不好看也不符合阅读预期。

解决办法是把这两个场景拆开。在数据模型层面,为 note 统一计算一个 previewText 字段,作为正文前若干字的标准截取结果。在此基础上,分出两个独立的函数:一个服务网页详情页本身的标题和说明,另一个只服务 Telegram 和社交卡片的 metadata。网页详情页保持日期标题或手写描述,Telegram 卡片则使用日期标题加上固定长度的正文摘要。

这一步同时也修正了 Open Graph 和 Twitter Card 的 metadata 输出。对于没有手写描述的 memo,og:description 自动取正文前若干字,不再输出空字符串;og:image 也统一指定了一张固定的站点图片,减少卡片布局因来源图片不同而产生的尺寸波动。

推送文案的收敛

站点 metadata 调整完之后,推送脚本本身也需要跟进。之前的 Telegram 通知脚本虽然会提取 description,但消息正文里并没有真正输出这段信息,用户在频道里看到的只有标题和链接。同时,标签行有时会变得很长,把消息气泡和卡片的视觉宽度拉得不稳定。

调整分两步。第一步,把摘要真正写入消息正文,让频道消息本身也具备稳定的简介层。第二步,对标签输出做限制:限制 hashtag 的数量和总长度,保留必要的分类信息,但避免标签成为卡片宽度的主要驱动因素。

这一层的目标不是让所有卡片绝对等宽——Telegram 的客户端渲染行为本身就不完全可控——而是削弱长标题、空摘要和长标签叠加带来的波动,让大多数卡片看起来处于一个可预期的范围内。

Discord 接入与共享通知模块

在 Telegram 通知逻辑调整之后,接下来是把通知链路向 Discord 扩展。

为了避免两个平台各自维护一套几乎相同的文件变更解析逻辑,先抽出了一个共享模块。这个模块做的事情不复杂:基于 git diff 判断哪些内容是新增的,从中识别出文章和 note,解析它们的 frontmatter,然后生成标题、摘要和标签。Telegram 通知脚本被改造成复用这个模块,只保留 Telegram API 调用和 IV 链接构造的部分。Discord 通知脚本则是新写的,通过 Incoming Webhook 发送 embed 风格的频道消息。

这样处理的好处是"检测新增内容"和"提取摘要"只有一份实现。以后调整摘要规则或标签策略时不需要同时改两个脚本。Discord 的接入方式选择了 webhook 而不是 bot,因为当前场景只是部署后的单向推送,webhook 足够用,不需要引入更复杂的机器人权限管理。

跨仓库的复用可能

除了当前仓库的代码改动,这轮还核查了相邻的另一个 Hugo 仓库。那个仓库已经有 Telegram 推送脚本和部署后通知的 workflow 步骤,但还没有 Discord 通知,也没有把内容变更解析抽成共享模块。

基于当前仓库已经验证过的做法,整理了一份面向那个仓库的执行建议:抽出共享解析模块,保留 Hugo 仓库自身的路由和 URL 生成逻辑,让 Telegram 和 Discord 共用同一套摘要生成策略。这部分只完成了方案整理,没有直接改动那个仓库的代码。

已验证的和尚待确认的

本轮的验证覆盖了本地构建和静态文件检查:构建通过,导出的 HTML 中 og:title 已恢复为日期标题,og:description 对 memo 正确使用了正文前若干字。三个通知脚本的语法检查也都通过了。

但有几件事在本轮中没有完成端到端验证:Discord 频道是否真的能收到消息,需要一次真实部署来确认;Telegram 旧消息上的卡片受客户端缓存影响,不能仅凭旧消息的表现来判断当前 metadata 是否已生效;相邻仓库的改造建议还停留在文档阶段。

拆开之后的结构边界

这轮工作的成果不只是多接入了一个 Discord webhook。更重要的是把几件容易混在一起的事情拆清楚了:网页标题和卡片标题各自由谁决定,简介文本从哪里来,通知脚本的内容解析和平台调用怎样分层,以及不同仓库之间怎样复用同一套通知链路设计。

后续如果需要调整卡片的展示策略,或者为新的平台接入推送,改动的范围和影响已经比之前更容易预判了。