利用Resend和Workers 实现博客电子邮件订阅

2026-04-01
利用Resend和Workers 实现博客电子邮件订阅 关注 新手上路 关注 新手上路 关注 新手上路 关注 新手上路 03/16 07:32

纵观提供托管 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

  1. 提取 RSS 的第一个 Item
  2. 编写电子邮件内容
  3. 通过 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
)

在使用时,替换 urlapiKeysegmentID 之类的变量就可以了。小心别把机密信息硬编码进去了。 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_KEYRESEND_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
    讨论 我来说一句 发布发表评论 发布互联网 3等 3 人为本文章充电 太阳伞下天然文静素材 关注