利用Resend和Workers 实现博客电子邮件订阅
纵观提供托管 newsletter 的平台,比如 MailerLite,它们都提供看起来非常慷慨的免费额度。一个月几万封邮件,看起来确实很有吸引力。但注册之后才会发现,最核心的 RSS to Email 服务只是订阅计划可用。 jogodotigrinhodemo a5game.app 足球比分 a5game.app demo a5game.app
Resend 为开发者提供了批量发送邮件的能力,它支持多种语言的 API, 而且可以自动管理订阅和退订逻辑。它的免费额度也不小,是一个理想选择。 小宝影院xiaobaotv.video
Resend 功能虽然强大,但它只有发件和管理订阅者的功能,并不是一种我们想要的全功能托管平台。想以 Resend 为核心构建系统,还缺少以下组件: 寻秦记爱壹帆yfsp.app slotpix a5game.app 华人影视xiaobaotv.video
- 一个表单,用于收集订阅用户的邮箱
- 转换 RSS 到电子邮件的实现
- 调用 Resend API 发送邮件的服务
- 负责添加订阅用户的服务
因为我的博客本身是完全静态的,我不想在博客内部引入任何无服务器函数。为了系统的可扩展性和可维护性考虑,每个服务之间和每个服务与博客之间应该没有任何关联。最终,我的方案如下: 爱壹帆国际版 yfsp.app fortuneoxdemográtis a5game.app jogosdemopg a5game.app
- Tally 作为 Web 表单,通过 Webhook 发送信息
- 写一个 Python 脚本处理 RSS 转换和邮件发送
- Cloudflare Workers 充当 Webhook endpoint,调用 API 向 Resend 添加订阅者
- 事件驱动的 GitHub Action 运行 Python 脚本
实现细节
resend
这里利用了 Resend 的 Broadcasts 功能。 ifuntvyfsp.app slots a5game.app 爱壹帆免费版yfsp.app ifvodyfsp.app
在注册 Resend 并添加域名后,记得添加一个 API Key,这个需要保留好,后面会用到。 fortunedragon demo a5game.app
同时管理订阅者也是利用了 Resend Audience 功能里的 Segment 功能实现的,下文会提到。 tigrinhodemo a5game.app 爱壹帆国际版 yfsp.app
通过 Broadcasts API 的 Create Broadcasts 进行邮件的创建和批量发送。它的 Python API 大概长这样: 电影小宝影院xiaobaotv.video 小寶影院xiaobaotv.video 免费在线影院xiaobaotv.video
import resend
resend.api_key = "re_xxxxxxxxx"
// Create a draft broadcast
params: resend.Broadcasts.CreateParams = {
"segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
"from": "Acme <[email protected]>",
"subject": "Hello, world!",
"html": "Hi {{{FIRST_NAME|there}}}, you can unsubscribe here: {{{RESEND_UNSUBSCRIBE_URL}}}",
}
resend.Broadcasts.create(params)
// Create and send immediately
params: resend.Broadcasts.CreateParams = {
"segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
"from": "Acme <[email protected]>",
"subject": "Hello, world!",
"html": "Hi {{{FIRST_NAME|there}}}, you can unsubscribe here: {{{RESEND_UNSUBSCRIBE_URL}}}",
"send": true,
}
resend.Broadcasts.create(params)
// Create and schedule
params: resend.Broadcasts.CreateParams = {
"segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
"from": "Acme <[email protected]>",
"subject": "Hello, world!",
"html": "Hi {{{FIRST_NAME|there}}}, you can unsubscribe here: {{{RESEND_UNSUBSCRIBE_URL}}}",
"send": true,
"scheduled_at": "in 1 hour",
}
resend.Broadcasts.create(params)这样就可以方便地使用 Python 去调用这些 API 发送邮件了。 fortunetigerbônusgrátissemdepósito a5game.app 免费在线影院xiaobaotv.video
Python 脚本
Python 脚本需要做的事情大概有三件: slotdemo a5game.app 小宝影院电影xiaobaotv.video
- 提取 RSS 的第一个 Item
- 编写电子邮件内容
- 通过 Resend Broadcasts API 发送
这些代码是我学习了半个小时 Python 之后手搓的,看起来可能比较简陋,但还是可以用的。 爱壹帆电影yfsp.app
import html
import requests
import xml.etree.ElementTree as ET
import resend
import os
def getRss(url):
headers = {
"User-Agent": "Nalanyinyun RSS and email service/1.0, +https://nalanyinyun.work"
}
response = requests.get(url, headers=headers, timeout=15)
response.raise_for_status()
return response.text
def generateEmailContent(rss):
root = ET.fromstring(rss)
items = root.findall("./channel/item")
if not items:
return "No posts found in RSS feed."
first = items[0]
title = first.findtext("title", default="Untitled")
description = first.findtext("description", default="No description.")
pubDate = first.findtext("pubDate", default="Unknown date")
formatted_str = (
f"<pre style='white-space: pre-wrap; font-family: sans-serif; font-size: 14px;'>"
f"Nalanyinyun's Library 已更新,以下是摘要:\n\n"
f"Title: {title}\n"
f"Date: {pubDate}\n"
f"{'-'*20}\n"
f"Description: {description}\n\n"
f"退订见:<a href=\"{{{{{{ resend_unsubscribe_url }}}}}}\">点击此处退订</a>"
f"</pre>"
)
return formatted_str
def publishLatest(apiKey, segmentID, fromID, subject, content):
resend.api_key = apiKey
resend.Broadcasts.create({
"segment_id": segmentID,
"from": fromID,
"subject": subject,
"html": content,
"headers": {
"List-Unsubscribe": "<{{{{ resend_unsubscribe_url }}}}>",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
},
"send": True
})
url = "https://nalanyinyun.work/rss.xml"
content = generateEmailContent(getRss(url))
publishLatest(
apiKey = os.getenv("RESEND_API_KEY"),
segmentID="76654bf7-fd97-45e9-81a6-e38cda6391fc",
fromID="Nalanyinyun <[email protected]>",
subject="Nalanyinyun's Library Content Delivered",
content=content
)在使用时,替换 url 和 apiKey、segmentID 之类的变量就可以了。小心别把机密信息硬编码进去了。 pgdemo a5game.app 爱亦凡yfsp.app demotigrinho a5game.app
值得注意的是,Resend 已经替我们处理好了所有的退订逻辑,但请在的正文和 Headers 里标识出来,不然很可能第一次发邮件就被拒信了。 爱壹帆影视yfsp.app 爱一番yfsp.app
Cloudflare Workers
需要Resend Audience。这个只需要创建好 Segment 并记住 ID 就可以了。
Cloudflare Workers 主要负责解析 Webhook 传入的数据,因为我实在是不懂 JavaScript,所以找 Gemini 生成一个勉强能用的后端。 sugarrush1000demo a5game.app pragmatic a5game.app 海外华人视频网xiaobaotv.video slot a5game.app
export default {
async fetch(request, env, ctx) {
if (request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
try {
const body = await request.json();
const emailField = body.data.fields.find(f => f.type === "INPUT_EMAIL");
const userEmail = emailField ? emailField.value : null;
if (!userEmail) {
return new Response("No email found in webhook data", { status: 400 });
}
const segmentId = env.RESEND_SEGMENT_ID;
const url = `https://api.resend.com/segments/${segmentId}/contacts`;
const resendResponse = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${env.RESEND_API_KEY}`,
},
body: JSON.stringify({
email: userEmail,
}),
});
const result = await resendResponse.json();
if (resendResponse.ok) {
console.log(`Success: Added ${userEmail} to segment`);
return new Response("Subscribed to segment successfully!", { status: 200 });
} else {
console.error("Resend API Error:", result);
return new Response(JSON.stringify(result), { status: resendResponse.status });
}
} catch (err) {
return new Response("Internal Server Error: " + err.message, { status: 500 });
}
},
};使用时需要添加机密 RESEND_API_KEY 和RESEND_SEGMENT_ID。 一帆yfsp.app slotsdemo a5game.app pragmaticplay a5game.app Caça-níqueis a5game.app fortunedragon demo a5game.app
如果你的 Tally Webhook 传入和我不一样,可能需要稍微改一改解析的逻辑,调用 API 那部分应该是没有问题的。 爱壹帆电影 yfsp.app
Github Action
这部分就很简单了 plataformademo a5game.app tigrinhodemo a5game.app
name: RSS to Email on File Change
on:
push:
branches:
- main
paths:
- src/content/posts/**
workflow_dispatch:
jobs:
email_notification:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Dependencies
run: pip install requests resend
- name: Run Script
env:
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
run: python blogutils/emailutils.py根据需要,你可能要 sleep 一会儿。当然不是因为你困了,是因为我们要等站点构建完成之后才能获取到新的 RSS 内容。 爱壹帆寻秦记yfsp.app 小宝影院在线视频xiaobaotv.video 一帆视频yfsp.app
Tally
Tally 的 Web 表单是开箱即用的,我的表单只有这么一个输入框。在编辑之后点击 Integrities,添加一个 Webhook endpoint 就可以了。 pgslotgacor a5game.app demo a5game.app
Webhook endpoint 的实现下文会提到。 a5game a5game.app fortunetigerdemográtis a5game.app 电影爱壹帆yfsp.app Cassinos a5game.app nba比分 a5game.app xiaobao xiaobaotv.video
Tally 的自定义域名需要付费计划,不过我觉得这个是不是自己的域名应该无关紧要。记得在设置里开启禁止重复填写。 ifun yfsp.app plataformademográtis a5game.app
Source
文章也发表在独立博客 Nalanyinyun's Library 上。 pg a5game.app
文中涉及到的源代码以及博客源代码在 Github 中可用: pglucky88 a5game.app iyf yfsp.app aiyifan yfsp.app 小寶影院电影xiaobaotv.video 爱壹帆在线yfsp.app iyifanyfsp.app
如果想订阅 newsletter, 见: 爱壹帆yfsp.app tigrinho gratis a5game.app demo a5game.app
本站不定期更新文学类、技术类文章,感谢支持。 pgslot a5game.app sweetbonanza1000demo a5game.app JogodoTigrinho a5game.app iyftvyfsp.app 爱一帆 yfsp.app
33目录 0