静态博客实现邮件订阅
博客文章通过邮箱推送,对于有后端的动态博客来说,算是很平常的事情。像是 Newsletter、WordPress、Ghost,或者其他的什么动态博客搭建系统。要么就是本身内置,就算不是内置的,市场里也有通用的插件。但对于静态博客来说,就是绝对的稀罕物了。为此,有各种各样的邮箱实现的推送方式,比如本地生成好完整后发送邮件,又或是通过 GitHub 仓库工单系统或者社区系统订阅推送。
我的朋友阿普修使用的就是 GitHub Issues 进行邮件推送,原理就是推送到博客仓库后,通过 GitHub Actions 进行构建。构建完成后会向该 Issues 发送新消息,如果你订阅了这个 Issues,那么它就会从邮件推送过来给你。个人觉得虽然很简单,但是也充满了奇技淫巧,就像是 Giscus 那种心态。如果我的目标读者没有 GitHub 账号,那它就无法通过邮件系统订阅我的博客更新。
况且,他的推送方式就没考虑过重复性。这意味着,如果他持续更新网站、而不更新文章的话。你是他的订阅者,你会被反复推送一个文章。个人认为即时性推送更适合说说,而不是文章推送。因为从心理学上讲,一个人如果他在我的博客网站上点击了订阅,说明他本来就没时间来看我的博客,希望偶尔接收到推送而已。那既然是这样的话,多篇文章多次推送,确实显得行不通——它太暴力了。
在我看来,既然使用了 GitHub Actions,那意味着有完整的环境。这完全可以运行一个 Python 脚本、安装一些环境依赖、进行一些基本的算法。因此对我来说,首要目的解决的就是这两项——真邮箱推送、不重复推送。GitHub Actions 存在一个 Cache 系统,这意味着我可以存储一个文件,专门记录该文章链接是否被推送过。该推送逻辑使得文章在中午 12 点左右被推送(可能会排队),就是你吃完饭之后。
想清楚了流程,那接下来要考虑的就是邮箱列表的存储方式了。为此,我头痛了一星期:如果是放在仓库,那就是明文的,泄露隐私;如果是放在网站上,那黑客迟早会根据 Actions 日志或者代码实现方式,扫到访客的邮箱作为轰炸对象。好在 GitHub 提供了一种更加隐私的方式——GitHub Secrets,我完全可以把那些变量信息、敏感信息,全部存在这里,同时还能被 Actions 读到:
1 | name: 文章更新邮件通知 |
代码方面,因为我需要用网络库获取 atom.xml 这个文件。这是一个典型 ATOM 格式的 RSS 文件,存在几个重要标签:<title>(文章标题)、<summary>(文章简介)、<link>(文章链接)。因此,我只需要解析 XML,提取这几个部分的值,放入 HTML 中排序即可。获取了值后和 link.txt 进行比对:如果已经存在,则跳过这次运行;如果不存在,则发送邮件。例如:
1 | #!/usr/bin/env python3 |
得益于 Resend 提供的域名邮箱的免费服务,我可以使用自己的域名邮箱——[email protected] 发送这些文章,同时不需要自己搭建任何的邮件服务。如果你没有 Resend 域名邮箱,你可以去注册一个,或者 QQ 邮箱、163 邮箱,因为脚本使用的是 SMTP 协议。并且上次讲过的这些东西都不需要你自己搭建,你只需要 Fork 我的仓库,并创建几个 Secrets:
SMTP_SERVER:指定一个 SMTP 服务商,例如smtp.resend.com。SMTP_PORT:指定服务商的端口,例如465。SMTP_USER:指定 SMTP 用户名,例如resend。SMTP_PASS:指定 SMTP 密码,例如re_xxxxx。SMTP_FROM_NAME:指定发件人名称,这里一般是写网站,比如我的他说。SMTP_FROM_ADDR:发件邮箱地址,例如[email protected]。SMTP_SUBJECT:指定邮件主题,例如他说,你收到了新的订阅。RSS_URL:网站的 RSS 订阅地址,例如https://example.com/atom.xml。EMAIL_LIST:指定访客密送列表,用空格间隔,例如[email protected] [email protected]。TAG_TITLE:文章标题标签,例如h1、h2。TAG_SUMMARY:文章摘要标签,例如:p。TAG_LINK:详情链接标签,例如:a。LINK_TEXT:链接显示文字,例如阅读详情、阅读更多。
这些 Secrets 全部都是要手动创建的,而且要注意的一点是,Serects 是每次打开都会空白的。因此你需要把这个变量保存在本地某个文件,每次添加新成员的时候,在那个文件里面加,还好后复制全部到后台那里。Serects 的字数是有上限的,但是我猜测大部分个人博客到不了上限。如果你想使用邮箱订阅我的后续文章,现在就可以点击网站上的订阅按钮,向我发送订阅申请。





