背景
项目 chuhai168.top
是一个部署在 Vercel 的静态网站,需要实现一个邮件订阅功能:
- 用户提交邮箱订阅 → 发邮件确认 → 点击确认链接完成订阅。
- 后端服务托管在 Vercel,使用 Neon Serverless Postgres 存储订阅记录,Resend 作为邮件发送服务。
- 前端和后端分离,前端纯静态 HTML/JS,后端作为单独 Vercel 项目
api.chuhai168.top
提供 API。
在搭建过程中,遇到了多种典型问题:TypeScript 版本冲突、CORS 配置错误、DNS 解析失效、TLS 握手失败等。这篇文章完整总结整个过程。
功能架构
[前端静态站 chuhai168.top]
|
v
[API Gateway: api.chuhai168.top]
|
v
[Neon Serverless Postgres] <-- 存储用户邮箱、订阅状态
[Resend 邮件服务] <-- 发送确认/欢迎邮件
- 前端使用
fetch('/api/subscribe')
调用后端 API。 - 后端 Edge Function 提供
/api/subscribe
和/api/confirm
两个接口。 - Neon 数据库存储用户订阅状态:pending → confirmed。
- Resend 邮件服务负责发邮件,域名配置了 DKIM/SPF 保障投递。
主要开发步骤
1初始化 API 项目
- 使用 Vercel 创建新项目
chuhai168-api
。 - 项目结构:
api/
subscribe.ts # 接收邮箱订阅请求,写数据库,发送确认邮件
confirm.ts # 用户点击邮件确认链接,修改状态为 confirmed
package.json
tsconfig.json
安装依赖:
pnpm add @neondatabase/serverless
pnpm add -D typescript
2数据库与表结构
使用 Neon Serverless Postgres:
CREATE TABLE IF NOT EXISTS subscribers (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
status TEXT CHECK (status IN ('pending','confirmed')),
token TEXT UNIQUE,
created_at TIMESTAMP DEFAULT now()
);
3邮件服务配置
- 注册 Resend,获取 API Key。
- 添加自定义域
chuhai168.top
,配置:- DKIM CNAME
- SPF TXT
- DMARC TXT
- 验证成功后
FROM_EMAIL
使用no-reply@chuhai168.top
。
4后端接口逻辑
简化版逻辑:
// subscribe.ts
const rows = (await sql`
SELECT status FROM subscribers WHERE email = ${email} LIMIT 1
`) as any[];
if (!rows.length) {
// 插入记录
const token = crypto.randomUUID();
await sql`
INSERT INTO subscribers (email, status, token)
VALUES (${email}, 'pending', ${token})
`;
// 发送确认邮件
await resend.emails.send({
from: FROM_EMAIL,
to: email,
subject: "请确认订阅",
html: `<a href="${CONFIRM_URL}?token=${token}">点击确认</a>`
});
}
踩坑与解决方案
🔹 1. TypeScript 编译报错
错误:
TS2344: Type '{ status: string; }[]' does not satisfy the constraint 'boolean'.
原因:TS 4.9 不兼容 Neon 模板标签的泛型。
解决方案:
- 升级 TS 至 5.4;
- 或移除泛型,改用
as any[]
明确类型断言。
🔹 2. Vercel 未使用最新 package.json
问题:Vercel 构建日志仍显示 Using built-in TypeScript 4.9.5
,没读到 devDependencies。
原因:构建目录或缓存问题。
解决方案:
- 确认 Root Directory 设置正确;
Redeploy
时选择Clear build cache
;- 或直接用
npx vercel --prod
本地上传部署。
🔹 3. CORS 报错
错误:
No 'Access-Control-Allow-Origin' header is present
解决方案:
- 在 API 项目设置
ALLOWED_ORIGINS=https://chuhai168.top,https://www.chuhai168.top
; - 后端在响应头中动态注入:
res.headers.set('Access-Control-Allow-Origin', origin);
🔹 4. 域名解析错误
问题:
dig api.chuhai168.top
显示198.18.0.119
,不是 Vercel;- TLS 握手失败:
LibreSSL SSL_connect: SSL_ERROR_SYSCALL
。
原因:api
子域是 A 记录,指向错误 IP。
解决方案:
- 删除
api
的 A 记录; - 添加
api
的 CNAME:
api -> cname.vercel-dns.com
- 等待生效并重新签发证书。
🔹 5. 最终联调
修正 DNS 后:
curl -i -X POST https://api.chuhai168.top/api/subscribe \
-H "Origin: https://www.chuhai168.top" \
-H "Content-Type: application/json" \
--data '{"email":"test@example.com","hp":""}'
返回:
{"ok":true,"message":"订阅请求已发送,请前往邮箱点击确认链接完成订阅。"}
浏览器端提交表单成功,邮件送达,点击确认链接跳转首页提示"订阅成功"。
总结
在 Vercel 上开发和部署完整订阅功能的流程:
- 分离前后端项目:前端纯静态部署,后端用 Edge Function 提供 API。
- 邮件服务:推荐 Resend,域名配置 DKIM/SPF 确保投递。
- 数据库:Neon Serverless Postgres,轻量且无服务器化。
- CORS 配置:根据前端实际域名动态允许跨域。
- DNS 正确配置:子域必须 CNAME 到
cname.vercel-dns.com
,否则证书无法签发。 - 调试工具:
curl
和dig
是排查 API 和 DNS 的必备工具;openssl s_client
查看 TLS 细节;- Vercel CLI 可快速从本地强制部署。
- 最佳实践:
- 所有敏感变量放在 Vercel 环境变量;
- API 响应必须显式设置
Access-Control-Allow-*
; - 发布前先验证默认
*.vercel.app
域是否正常,再配置自定义域。