如今,许多人都在建立自己的通讯。您有像 Substack 和 MailChimp 这样的知名公司,像 Twitter 这样的公司也加入了 Revue,甚至 Facebook 也加入了通讯业务。有些人正试图通过 自托管的 WordPress 解决方案(通过 MailPoet)来实现更接近家的方式。
让我们谈谈另一种可能性:以传统的方式收集您自己的电子邮件订阅者:一个提交到 API 并将其放入数据存储的<form>
。
建立通讯的第一个障碍是收集电子邮件的机制。大多数帮助您构建和发送通讯的应用程序都会提供一些可复制粘贴的注册表单示例,但这些选项大多是付费的,而我们正在寻找一些免费且完全由我们自己托管的选项。
没有比建立我们自己的系统更方便的了,我们可以控制它。在本教程中,我们将基于 Jamstack 架构建立自己的系统,以收集我们通讯的电子邮件。我们将实现一个 HTML 表单来收集我们通讯的电子邮件,使用 Netlify 函数 处理这些电子邮件,并使用 Notion 数据库和 Notion API 将其保存到该数据库中。
但在我们开始之前...
我们需要以下内容
所有这些服务都是免费的(有限制)。至于 Mailgun,我们实际上可以使用任何提供 API 的电子邮件服务提供商。
获取本教程中使用的全部代码 在 GitHub 上。
首先,Notion
您问 Notion 是什么?让我们让他们 描述它
Notion 是一个单一的空间,您可以在其中思考、写作和计划。捕捉想法,管理项目,甚至运营整个公司——并以您想要的方式进行。

换句话说,Notion 有点像数据库,但它有一个漂亮的可视化界面。数据库中的每一行都拥有自己的“页面”,在页面上编写内容就像在 WordPress 块编辑器中编写博客文章一样。
这些数据库可以以多种方式可视化,从简单的电子表格到像 Trello 一样的看板,或者画廊、日历、时间线等等......您明白了吧。
我们使用 Notion 是因为它可以建立表格来存储表单响应,使其基本上成为一个数据存储库。Notion 也恰好最近发布了用于 公开使用的 完整 API。我们可以利用这个 API 并使用......与它交互。
Netlify 函数
Netlify 函数是 Netlify 托管平台提供的无服务器 API 端点。我们可以使用 JavaScript 或 Go 编写它们。

我们可以在这里使用 Netlify 表单 来收集表单提交。事实上,Matthew Ström 已经分享了它是如何工作的。但对于免费计划,我们每月只能接收 100 个提交。因此,这就是我们使用 Netlify 函数的原因——它们每月可以被调用 125,000 次,这意味着我们可以每月收集 25 万封电子邮件,而无需支付一分钱。
让我们设置 Notion 数据库
我们需要做的第一件事是在 Notion 中创建一个数据库。只需创建一个新页面,然后通过输入/table
插入一个全页面表格块。

/table
)会打开一个上下文菜单,以在页面上插入块。让我们给我们的数据库起一个名字:通讯电子邮件。
我们希望保持简单,因此我们只在有人提交表单时收集电子邮件地址。因此,我们实际上只需要一列名为电子邮件的表格。

但 Notion 比这更智能,它包含提供更多信息的属性。其中一个是“创建时间”属性,顾名思义,它会为新提交生成时间戳。

我们还需要一个 Notion API 令牌
为了与我们的 Notion 数据库交互,我们需要创建一个 Notion 集成并获取 API 令牌。新的集成不是在您的 Notion 帐户中创建的,而是在您登录帐户时在 Notion 网站上创建的。我们将给这个集成一个与 Notion 表格一样有创意的名字:通讯订阅。

创建集成后,我们可以获取内部集成令牌(API 令牌)。请保留它,因为我们将在稍后使用它。

默认情况下,Notion 集成实际上无法访问 Notion 页面或数据库。我们需要明确地将特定页面或数据库与我们的集成共享,以便集成能够正确读取和使用表格中的信息。点击 Notion 数据库右上角的共享链接,使用选择器找到按名称命名的集成,然后点击邀请。

接下来是 Netlify
我们的数据库已准备好开始接收来自 Notion API 的表单提交。现在是创建表单和用于处理表单提交的无服务器函数的时候了。
由于我们在 Netlify 上设置了网站,因此让我们在本地机器上安装 Netlify CLI。打开终端并使用此命令
npm install netlify-cli -g
根据您的喜好,我们可以通过不同的方式进行 Netlify 身份验证。您可以按照 Netlify 的精彩指南作为起点。完成 Netlify 身份验证后,我们需要做的下一件事是创建一个 Netlify 项目。以下是如何创建项目的命令
$ mkdir newsletter-subscription-notion-netlify-function
$ cd newsletter-subscription-notion-netlify-function
$ npm init
请参考 GitHub 项目中的此提交。
让我们编写一个 Netlify 函数
在我们开始编写 Netlify 函数之前,我们需要做两件事。
首先,我们需要安装[@notionhq/client](https://github.com/makenotion/notion-sdk-js)
npm 包,这是一个官方的 Notion JavaScript SDK,用于使用 Notion API。
$ npm install @notionhq/client --save
其次,我们需要在项目根目录创建.env
文件并添加以下两个环境变量
NOTION_API_TOKEN=<your Notion API token>
NOTION_DATABASE_ID=<your Notion database>
数据库 ID 可以从其 URL 中找到。典型的 Notion 页面有一个类似于https://www.notion.so/{workspace_name}/{database_id}?v={view_id}
的 URL,其中{database_id}
对应于 ID。
我们将 Netlify 函数写入functions
目录。因此,我们需要在项目根目录创建netlify.toml
文件,以告诉 Netlify 我们的函数需要从此目录构建。
[build]
functions = "functions"
现在,让我们在项目根目录创建一个functions
目录。在functions
目录中,我们需要创建一个index.js
文件并添加以下代码。此函数作为 API 端点在/.netlify/functions/index
处可用。
const { Client, LogLevel } = require('@notionhq/client');
const { NOTION_API_TOKEN, NOTION_DATABASE_ID } = process.env;
async function addEmail(email) {
// Initialize Notion client
const notion = new Client({
auth: NOTION_API_TOKEN,
logLevel: LogLevel.DEBUG,
});
await notion.pages.create({
parent: {
database_id: NOTION_DATABASE_ID,
},
properties: {
Email: {
title: [
{
text: {
content: email,
},
},
],
},
},
});
}
function validateEmail(email) {
const re =
/^(([^<>()[\\]\\\\.,;:\\s@"]+(\\.[^<>()[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
module.exports.handler = async function (event, context) {
// Check the request method
if (event.httpMethod != 'POST') {
return { statusCode: 405, body: 'Method not allowed' };
}
// Get the body
try {
var body = JSON.parse(event.body);
} catch (err) {
return {
statusCode: 400,
body: err.toString(),
};
return;
}
// Validate the email
const { email } = body;
if (!validateEmail(email)) {
return { statusCode: 400, body: 'Email is not valid' };
}
// Store email in Notion
await addEmail(email);
return { statusCode: 200, body: 'ok' };
};
让我们逐块分解它。
addEmail
函数用于将电子邮件添加到我们的 Notion 数据库。它接受一个email
参数,该参数是一个字符串。
我们通过提供我们的NOTION_API_TOKEN
为与 Notion API 进行身份验证创建一个新客户端。我们还将日志级别参数设置为获取与命令执行相关的信息,这些信息是在我们开发项目时可以获取的,一旦我们准备好将项目部署到生产环境,就可以将其删除。
在 Notion 数据库中,每个条目都被称为页面。因此,我们使用notion.pages.create
方法在我们的 Notion 数据库中创建一个新条目。module.exports.handler
函数由处理对其进行的请求的 Netlify 函数使用。
我们首先检查请求方法。如果请求方法不是POST
,我们返回一个405 - 方法不允许
响应。
然后我们解析前端发送的有效负载(event.body
)对象。如果我们无法解析有效负载对象,我们将返回一个400 - 错误请求
响应。
解析请求有效负载后,我们会检查是否存在电子邮件地址。我们使用validateEmail
函数来检查电子邮件的有效性。如果电子邮件无效,我们会返回一个400 - 错误请求
响应,并带有自定义消息,说明电子邮件无效
。
完成所有检查后,我们将调用addEmail
函数将电子邮件存储到我们的 Notion 数据库中,并返回一个200 - 成功
响应,表示一切都已成功。
在 GitHub 项目中使用commit ed607db,与代码保持一致。
现在是一个基本的 HTML 表单
我们的无服务器后端已经准备就绪,最后一步是创建一个表单来收集我们时事通讯的电子邮件提交。
让我们在项目根目录创建一个index.html
文件,并将以下 HTML 代码添加到其中。请注意,标记和类是从 Bootstrap 5 中提取的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notion + Netlify Functions Newsletter</title>
<link href="<https://cdn.jsdelivr.net.cn/npm/[email protected]/dist/css/bootstrap.min.css>" rel="stylesheet" crossorigin="anonymous">
</head>
<body>
<div class="container py-5">
<p>
Get daily tips related to Jamstack in your inbox.
</p>
<div id="form" class="row">
<div class="col-sm-8">
<input name="email" type="email" class="form-control" placeholder="Your email" autocomplete="off">
<div id="feedback-box" class="invalid-feedback">
Please enter a valid email
</div>
</div>
<div class="col-sm-4">
<button class="btn btn-primary w-100" type="button" onclick="registerUser()">Submit form</button>
</div>
</div>
<div id="success-box" class="alert alert-success d-none">
Thanks for subscribing to our newsletter.
</div>
<div id="error-box" class="alert alert-danger d-none">
There was some problem adding your email.
</div>
</div>
</body>
<script>
</script>
</html>
我们有一个用于收集用户电子邮件的输入字段。我们有一个按钮,当点击时会调用registerUser
函数。我们有两个警报,一个用于成功提交,一个用于不成功提交。
由于我们已安装 Netlify CLI,因此可以使用netlify dev
命令在 localhost 服务器上运行我们的网站
$ netlify dev

localhost:8888
并查看带有表单的网页。接下来,我们需要编写一些 JavaScript 代码来验证电子邮件并向 Netlify 函数发送 HTTP 请求。让我们在index.html
文件中的<script>
标签中添加以下代码
function validateEmail(email) {
const re =
/^(([^<>()[\\]\\\\.,;:\\s@"]+(\\.[^<>()[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
async function registerUser() {
const form = document.getElementById('form');
const successBox = document.getElementById('success-box');
const errorBox = document.getElementById('error-box');
const feedbackBox = document.getElementById('feedback-box');
const email = document.getElementsByName('email')[0].value;
if (!validateEmail(email)) {
feedbackBox.classList.add('d-block');
return;
}
const headers = new Headers();
headers.append('Content-Type', 'application/json');
const options = {
method: 'POST',
headers,
body: JSON.stringify({email}),
};
const response = await fetch('/.netlify/functions/index', options);
if (response.ok) {
form.classList.add('d-none');
successBox.classList.remove('d-none');
}
else {
form.classList.add('d-none');
errorBox.classList.remove('d-none');
}
}
在registerUser
函数中,我们使用 RegEx 函数(validateEmail
)来检查电子邮件输入的有效性。如果电子邮件无效,我们会提示用户重试。验证电子邮件后,我们将调用fetch
方法以向我们的 Netlify 函数发送一个POST
请求,该函数在/.netlify/functions/index
处可用,并带有包含用户电子邮件的有效负载对象。
根据响应,我们会向用户显示一个成功警报,否则我们会显示一个错误警报。
在 GitHub 项目中使用commit cd9791b,与代码保持一致。
获取 Mailgun 凭据
在我们的数据库中获取电子邮件很好,但还不够,因为我们不知道如何使用它们。因此,最后一步是向订阅者发送确认电子邮件,以确认他们已注册。
我们不会在用户注册后立即发送消息。相反,我们将设置一种方法,在过去 30 分钟内获取电子邮件注册,然后发送电子邮件。我认为,在自动生成的电子邮件中添加延迟会使欢迎消息感觉更个性化。
我们使用Mailgun发送电子邮件,但同样,我们可以使用任何带有 API 的电子邮件服务提供商。让我们访问 Mailgun 仪表板,从侧边栏进入设置 → API 密钥屏幕。

让我们复制私有 API 密钥并将其添加到.env
文件中,作为
MAILGUN_API_KEY=<your Mailgun api key>
每封电子邮件都必须来自一个发送地址,因此我们还需要的是发送电子邮件的 Mailgun 域名。要获取我们的 Mailgun 域名,我们需要进入发送 → 域名屏幕。

让我们复制我们的 Mailgun 沙盒域名并将其添加到.env
文件中,作为
MAILGUN_DOMAIN=<your Mailgun domain>
发送确认电子邮件
在开始编写用于发送电子邮件确认的 Netlify 函数之前,我们需要安装[mailgun-js](https://github.com/mailgun/mailgun-js)
npm 包,这是一个用于使用 Mailgun API 的官方 Mailgun JavaScript SDK
$ npm install mailgun-js --save
我们使用/.netlify/functions/index
端点发送欢迎电子邮件。因此,在functions
目录中,让我们创建一个新的welcome.js
文件并添加以下代码
const { Client, LogLevel } = require('@notionhq/client');
const mailgun = require('mailgun-js');
const {
NOTION_API_TOKEN,
NOTION_DATABASE_ID,
MAILGUN_API_KEY,
MAILGUN_DOMAIN,
} = process.env;
async function fetchNewSignups() {
// Initialize notion client
const notion = new Client({
auth: NOTION_API_TOKEN,
logLevel: LogLevel.DEBUG,
});
// Create a datetime that is 30 mins earlier than the current time
let fetchAfterDate = new Date();
fetchAfterDate.setMinutes(fetchAfterDate.getMinutes() - 30);
// Query the database
// and fetch only entries created in the last 30 mins
const response = await notion.databases.query({
database_id: NOTION_DATABASE_ID,
filter: {
or: [
{
property: 'Added On',
date: {
after: fetchAfterDate,
},
},
],
},
});
const emails = response.results.map((entry) => entry.properties.Email.title[0].plain_text);
return emails;
}
async function sendWelcomeEmail(to) {
const mg = mailgun({ apiKey: MAILGUN_API_KEY, domain: MAILGUN_DOMAIN });
const data = {
from: `Ravgeet Dhillon <postmaster@${MAILGUN_DOMAIN}>`,
to: to,
subject: 'Thank you for subscribing',
text: "Thank you for subscribing to my newsletter. I'll be sending daily tips related to JavScript.",
};
await mg.messages().send(data);
}
module.exports.handler = async function (event, context) {
// Check the request method
if (event.httpMethod != 'POST') {
return { statusCode: 405, body: 'Method not allowed' };
}
const emails = await fetchNewSignups();
emails.forEach(async (email) => {
await sendWelcomeEmail(email);
});
return { statusCode: 200, body: 'ok' };
};
上面的代码有两个主要函数,我们需要理解它们。首先,我们有一个fetchNewSignups
函数。我们使用 Notion SDK 客户端来获取我们 Notion 数据库中的条目。我们使用了一个过滤器来仅获取在过去 30 分钟内添加的电子邮件。从 Notion API 获取响应后,我们将循环遍历响应并将电子邮件映射到emails
数组中。
其次,我们有一个sendWelcomeEmail
函数。此函数用于通过 Mailgun API 向新订阅者发送电子邮件。
为了将所有内容整合在一起,我们在module.exports.handler
函数中循环遍历新电子邮件,并向每个新电子邮件发送欢迎电子邮件。
此函数在localhost:8888/.netlify/functions/welcome
处可用。
在 GitHub 项目中使用commit b802f93,与代码保持一致。
让我们测试一下!
现在我们的项目已经准备就绪,我们需要做的第一件事是测试表单,并查看电子邮件是否存储在我们的 Notion 数据库中。让我们转到localhost:8888
并输入一个有效的电子邮件地址。

完美!我们收到了成功消息。现在让我们转到我们的 Notion 数据库,看看电子邮件地址是否被捕获。

接下来,让我们从Postman向localhost:8888/.netlify/functions/welcome
发送一个POST
请求,并检查我们的收件箱以查看订阅确认消息。

在您的收件箱中没有看到电子邮件?确保检查您的垃圾邮件文件夹。
下一步是什么?
下一步是设置一个每 30 分钟运行一次并调用欢迎端点的脚本。截至目前,Netlify 还没有提供任何运行 CRON 作业的方法。因此,我们可以设置 GitHub Actions 来处理它。
此外,我们可以使用安全机制(如简单的密码或 JWT 令牌)来保护欢迎端点。这将阻止互联网上的任何人调用welcome
端点并向我们的订阅者发送大量垃圾邮件。
就是这样!我们成功地实施了一种 Jamstack 方法来收集时事通讯的电子邮件。这样做最大的好处是我们可以同时了解很多不同的技术。我们将收集电子邮件地址、验证提交、发布到数据库,甚至发送电子邮件的服务和 API 连接在一起。我们可以在相对较少的情况下完成这么多工作,这非常酷。
我希望您喜欢阅读本文并从中学习一些东西,就像我喜欢为您撰写本文一样。请在评论中告诉我您是如何将此方法用于自身的。祝您编码愉快!
为什么不直接从 JS 中调用 Notion API 呢?为什么还需要 Netlify 函数呢?
我正在按照这个教程来收集电子邮件(不进行 Mailgun 流程)。所有内容在本地机器上都可以正常运行。如何部署它?
嗨,Alex:
如果您在 Github 仓库中有代码,可以将您的仓库连接到 Netlify。它将自动部署到 Netlify。
感谢您的回复!我刚刚将所有代码推送到 github 并使用 netlify 部署了它,但 netlify 版本显示了一些错误:502 无法加载。请查看此处:https://prnt.sc/1u66rkn
嗨,Sebastien:
我们不想在前端公开 Notion API 密钥和 Mailgun API 密钥之类的私钥。因此,为了避免此安全问题,我们使用了 Netlify Functions。
尝试在 netlify 上部署它,但出现错误 POST 502 on index file
https://——–.netlify.app/.netlify/functions/index
我在创建 Netlify 函数后放弃了。但这篇文章非常有趣,我将把它收藏起来以备后用!
这是一个很棒的指南,我很享受遵循它。但我遇到了本地无法正常工作的问题。有一个错误一直出现在我面前,与 Notion API 令牌有关。我不确定它为什么无效,因为我从 Notion 复制到根目录中的 process.env 文件中。我还确保我创建的集成已链接到我的 Notion 工作区。
您知道我在这里做错了什么吗?
来自 ::1: POST /.netlify/functions/index 的请求
@notionhq/client 信息:请求开始 { method: 'post', path: 'pages' }
@notionhq/client 警告:请求失败 { code: 'unauthorized', message: 'API token is invalid.' }
@notionhq/client 调试:失败的响应正文 {
body: '{"object":"error","status":401,"code":"unauthorized","message":"API token is invalid."}'
}
谢谢。
嗨,John:
确保你已将你试图访问的 Notion 数据库与你的 Notion 集成共享。
如果我想添加另一个字段,比如姓名,该怎么办?
首先,在 Notion 中添加一个文本字段,并将其命名为“姓名”。
其次,向
notion.pages.create
函数添加一个新属性。参考此文档 https://developers.notion.com/reference/page#property-value-object.第三,通过添加姓名字段来更新前端的表单和 javascript 代码。
很棒的指南,我刚刚将所有代码推送到 github 并使用 netlify 部署了它,但 netlify 版本显示了一些错误,以下是来自 chrome 开发者控制台的一些日志:502 无法加载。请查看此处:https://prnt.sc/1u66rkn
是不是我做错了什么?
它在本地主机上运行良好。我对这一切都很陌生,如果可以的话,请帮忙。