用模板发欢迎邮件(2026):改文案不用重新部署
用 full_access 一次创建 Rovela 欢迎模板(POST /templates),注册流程用 sending_access 通过 template {id, variables} 发送,Idempotency-Key 用 welcome-{userId},并对变量做 HTML 转义——对齐线上文档的 curl 与 Node。
为什么欢迎邮件应该放进模板
欢迎邮件通常写一次、之后反复改——产品、市场、负责引导文案的人都会动。如果 HTML 写死在注册处理函数里,改个标题都要重新部署。把正文放进 Rovela 模板:创建一次,之后发送只带模板 id 和几个变量即可。
本文对齐线上示例 用模板发欢迎邮件,以及 邮件模板指南 与 Templates API。示例用 curl 创建模板、用 Node fetch 在注册路径发送——与文档同款形态。
这不是欢迎邮件最佳实践(主题、CTA、引导体验),也不是HTML 邮件模板开发(表格布局、内联 CSS、客户端兼容)。相关事务邮件文:带 PDF 附件的发票邮件、Next.js 魔法链接、Express OTP。
密钥:full_access 与 sending_access
模板管理(POST /templates、列表、修改、复制、删除、发布)需要 full_access API 密钥,或控制台 Templates 页。sending_access 打这些接口会得到 403(见 Authentication、Templates API)。
用 template: { id, variables } 走 POST /emails 发信,只需 sending_access。这才是生产注册服务该用的权限:能发欢迎信,不能改文案。
两种密钥都不要放进浏览器包,只在服务端调用。
前置条件
- 在 Rovela 控制台有一个已验证的发信域名(Quick Start)。
- 一次性创建步骤用
full_access(或控制台会话)。 - 应用环境变量
ROVELA_API_KEY使用sending_access。 - 保存返回的模板 id,例如环境变量
WELCOME_TEMPLATE_ID。
创建一次模板(curl + full_access)
在本机或初始化脚本里跑,不要放进热路径上的注册接口。
curl -X POST https://api.rovela.dev/templates \
-H "Authorization: Bearer re_your_full_access_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome",
"subject": "Welcome to Acme, {{first_name}}",
"html": "<h1>Hi {{first_name}},</h1><p>Your workspace <strong>{{workspace}}</strong> is ready.</p><p><a href=\"{{login_url}}\">Open Acme</a></p>",
"text": "Hi {{first_name}},\n\nYour workspace {{workspace}} is ready.\nOpen Acme: {{login_url}}"
}'
保存返回的 id(UUID)。生产发送只认这个 id:
{"object": "template", "id": "3b0e7c52-...", "name": "Welcome", ...}
建议同时提供 html 与 text。多数客户端看 HTML;纯文本利于纯文本客户端、部分无障碍配置和反垃圾信号(邮件模板指南)。
占位符精确匹配:{{first_name}} 不会被名为 firstName 的键填充。缺键会原样留下 {{first_name}}——这是有意设计,方便发现漏传,而不是静默空白(Templates API)。
没有条件与循环语法。正文需要逻辑时,在应用里渲染 HTML,直接传 html,不要用 template。
在注册流程里发送(Node + sending_access)
请求里不要带 subject 或 html:二者都来自模板,用 variables 渲染。template 与 html / text 互斥;发送时再写 subject 会被忽略(Templates API)。
变量按原样插入,不会自动做 HTML 转义。 用户输入进 variables 前先转义,否则类似 <img src=x> 的名字会变成邮件里的标记。
const WELCOME_TEMPLATE_ID = process.env.WELCOME_TEMPLATE_ID;
// 文档明确:不会自动转义。用户输入的值先走这里。
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
export async function sendWelcome(user) {
const res = await fetch("https://api.rovela.dev/emails", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.ROVELA_API_KEY,
"Content-Type": "application/json",
// 每个用户只发一封欢迎信,注册重试也不会重复。
"Idempotency-Key": "welcome-" + user.id,
},
body: JSON.stringify({
from: "Acme <hello@mail.acme.com>",
to: user.email,
template: {
id: WELCOME_TEMPLATE_ID,
variables: {
first_name: escapeHtml(user.firstName),
workspace: escapeHtml(user.workspaceName),
login_url: "https://app.acme.com/login",
},
},
}),
});
if (!res.ok) throw new Error("welcome email failed: " + res.status);
return (await res.json()).id;
}
幂等
注册接口会重试。使用 Idempotency-Key: welcome-{userId}(最长 256 字符;24 小时内重试返回原响应),用户只会收到一封欢迎信。思路与发票附件发送用发票号做键相同。
转义与主题 / 纯文本
同一组 variables 也会填进主题和 text。HTML 转义后的值可能在纯文本里显示成实体(Tom & Jerry 而不是 Tom & Jerry)。名字里可能有 & 或 < 时,给 HTML 与纯文本准备不同变量(welcome-template 示例)。
你自己拼的可信 URL(如上例 login_url)通常不必做实体转义——但不要把未校验的用户输入直接放进 href。
常见问题
- 收件箱里出现字面量
{{first_name}}——variables缺键,或拼写不一致。键名精确匹配。 - 有人改了线上模板 —— 改动会影响下一次发送,含尚未触发的定时信。更稳妥:
POST /templates/:id/duplicate,改副本,先给自己发一封测过,再切换WELCOME_TEMPLATE_ID(邮件模板指南)。 - 批量接口 ——
POST /emails/batch直接拒绝template(不会默默丢掉正文)。欢迎信本来就该每用户一次POST /emails(Templates API)。 - 创建模板
403—— 用了sending_access。管理操作用full_access或控制台。
发布模板(POST /templates/:id/publish)会记录首次上线时间,且幂等——再次发布不会改动该时间戳。
清单
- 用
full_access(或控制台)创建,模板 id 写入环境变量 - 生产发信只用
sending_access - 注册路径
POST /emails带template: { id, variables },不要内联 HTML - 用户输入的变量先 HTML 转义;文档不会自动转义
- 设置
Idempotency-Key: welcome-{userId} - 改线上文案:复制 → 修改 → 自测 → 切换 id
- 不要对模板走
/emails/batch