使用 Notion API 收集电子邮件订阅

Avatar of Ravgeet Dhillon
Ravgeet Dhillon

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

如今,许多人都在建立自己的通讯。您有像 SubstackMailChimp 这样的知名公司,像 Twitter 这样的公司也加入了 Revue,甚至 Facebook 也加入了通讯业务。有些人正试图通过 自托管的 WordPress 解决方案(通过 MailPoet)来实现更接近家的方式。

让我们谈谈另一种可能性:以传统的方式收集您自己的电子邮件订阅者:一个提交到 API 并将其放入数据存储的<form>

建立通讯的第一个障碍是收集电子邮件的机制。大多数帮助您构建和发送通讯的应用程序都会提供一些可复制粘贴的注册表单示例,但这些选项大多是付费的,而我们正在寻找一些免费且完全由我们自己托管的选项。

没有比建立我们自己的系统更方便的了,我们可以控制它。在本教程中,我们将基于 Jamstack 架构建立自己的系统,以收集我们通讯的电子邮件。我们将实现一个 HTML 表单来收集我们通讯的电子邮件,使用 Netlify 函数 处理这些电子邮件,并使用 Notion 数据库和 Notion API 将其保存到该数据库中。

但在我们开始之前...

我们需要以下内容

所有这些服务都是免费的(有限制)。至于 Mailgun,我们实际上可以使用任何提供 API 的电子邮件服务提供商。

获取本教程中使用的全部代码 在 GitHub 上

首先,Notion

您问 Notion 是什么?让我们让他们 描述它

Notion 是一个单一的空间,您可以在其中思考、写作和计划。捕捉想法,管理项目,甚至运营整个公司——并以您想要的方式进行。

A screenshot of Notion's homepage. It says Notion is an all-in-one workspace and there is a form to enter an email address and create an account. Black and white illustrations of four people are below the form.

换句话说,Notion 有点像数据库,但它有一个漂亮的可视化界面。数据库中的每一行都拥有自己的“页面”,在页面上编写内容就像在 WordPress 块编辑器中编写博客文章一样。

这些数据库可以以多种方式可视化,从简单的电子表格到像 Trello 一样的看板,或者画廊、日历、时间线等等......您明白了吧。

我们使用 Notion 是因为它可以建立表格来存储表单响应,使其基本上成为一个数据存储库。Notion 也恰好最近发布了用于 公开使用完整 API。我们可以利用这个 API 并使用......与它交互。

Netlify 函数

Netlify 函数是 Netlify 托管平台提供的无服务器 API 端点。我们可以使用 JavaScript 或 Go 编写它们。

A screenshot of the Netlify Functions webpage. It has a large black heading in bold type that reads Build scalable, dynamic applications. There is a teal colored button with rounded corners below to get started with Netlify, then a text link beside it that reads talk to an expert.

我们可以在这里使用 Netlify 表单 来收集表单提交。事实上,Matthew Ström 已经分享了它是如何工作的。但对于免费计划,我们每月只能接收 100 个提交。因此,这就是我们使用 Netlify 函数的原因——它们每月可以被调用 125,000 次,这意味着我们可以每月收集 25 万封电子邮件,而无需支付一分钱。

让我们设置 Notion 数据库

我们需要做的第一件事是在 Notion 中创建一个数据库。只需创建一个新页面,然后通过输入/table插入一个全页面表格块。

Screenshot of a Notion page with the block inserter open and the full page table option selected. The page has a dark background and a sidebar on the left with navigation for the user account.
在 Notion 中输入命令(例如/table)会打开一个上下文菜单,以在页面上插入块。

让我们给我们的数据库起一个名字:通讯电子邮件。

我们希望保持简单,因此我们只在有人提交表单时收集电子邮件地址。因此,我们实际上只需要一列名为电子邮件的表格。

A screenshot of the Notion database, which is in a spreadsheet or table view. The white heading is titled Newsletter Emails, then there is one column for an email address.

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

The Notion database showing a second property being added to the database to display the created time of an email entry. A contextual menu is open with a Created time property type selected and an additional contextual menu beside it showing the different types of available properties.
现在,当向表格添加新“电子邮件”条目时,也会显示其添加的日期。这对其他事情很有用,例如计算某人订阅了多长时间。

我们还需要一个 Notion API 令牌

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

A plain white webpage for Notion integrations. It has a heading that says Create a new integration with form fields for name, logo, associated workspace, and a submit button.
创建一个新的 Notion 集成,并将其与 Newsletter Emails 表格所在的 工作区相关联。

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

The Notion integrations conformation screen displaying the secret internal integration token, which is obscured by the app.
创建集成后,Notion 会提供一个秘密 API 令牌,用于验证连接。

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

The Notion database with Share options displayed in a modal with a list of available integrations and a light blue invite button.

接下来是 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 =
    /^(([^&lt;&gt;()[\\]\\\\.,;:\\s@&quot;]+(\\.[^&lt;&gt;()[\\]\\\\.,;:\\s@&quot;]+)*)|(&quot;.+&quot;))@((\\[[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
Showing the email form with a heading that says Get daily tips related to Jamstack in your inbox above a form field for an email and a blue button next to it to submit the form. There is a read error message below the email input that reads Please enter a valid email.
服务器启动并运行后,我们可以访问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 密钥屏幕。

Showing the Mailgun API security screen. The page has a white background with a dark blue left sidebar that contains navigation. The page displays the private API key, the public validation key, and the HTTP web hook signing key.
我们想要“私有 API 密钥”。

让我们复制私有 API 密钥并将其添加到.env文件中,作为

MAILGUN_API_KEY=<your Mailgun api key>

每封电子邮件都必须来自一个发送地址,因此我们还需要的是发送电子邮件的 Mailgun 域名。要获取我们的 Mailgun 域名,我们需要进入发送 → 域名屏幕。

The Mailgun domains screen showing a table with one row that contains a sandbox URL with a series of statistics.
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 数据库,看看电子邮件地址是否被捕获。

太棒了,它就在那里!我们还可以看到,**添加时间**字段也被 Notion 自动填充了,这正是我们想要的。

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

哇!我们收到了订阅时事通讯的欢迎电子邮件。

在您的收件箱中没有看到电子邮件?确保检查您的垃圾邮件文件夹。

下一步是什么?

下一步是设置一个每 30 分钟运行一次并调用欢迎端点的脚本。截至目前,Netlify 还没有提供任何运行 CRON 作业的方法。因此,我们可以设置 GitHub Actions 来处理它。

此外,我们可以使用安全机制(如简单的密码或 JWT 令牌)来保护欢迎端点。这将阻止互联网上的任何人调用welcome端点并向我们的订阅者发送大量垃圾邮件。

就是这样!我们成功地实施了一种 Jamstack 方法来收集时事通讯的电子邮件。这样做最大的好处是我们可以同时了解很多不同的技术。我们将收集电子邮件地址、验证提交、发布到数据库,甚至发送电子邮件的服务和 API 连接在一起。我们可以在相对较少的情况下完成这么多工作,这非常酷。

我希望您喜欢阅读本文并从中学习一些东西,就像我喜欢为您撰写本文一样。请在评论中告诉我您是如何将此方法用于自身的。祝您编码愉快!