使用React Email和Resend在Next.js中实现批量邮件发送

最近,我被指派负责设计和实施一个针对用户的群发邮件通知系统。然而,因为我之前只使用过SendGrid来创建对询问的生硬回复邮件,所以对实施过程中的设计概念一无所知。

因为从那时候的设想到最终的实施过程中,我进行了许多思考和摸索(或许有点夸张),所以我决定借此机会写一篇回顾文章!

目标读者

这主要是针对前端工程师的。

    • 問い合わせ以外にメール配信サービスを活用したことがない方

 

    • メールの本文をHTMLではなく、Reactコンポーネントで実装したい方

 

    これから実務で使おうと検討中の方

目标

    • 本文が装飾された案内メールを一斉配信する。

 

    本文は使い回ししやすいようテンプレート管理する。

任务的列举

在发送导航邮件之前,需要考虑如何装饰正文以及使用哪个邮件分发服务。

1. 文章的修饰

最终需要使用HTML、CSS等来装饰的事情我是知道的,但作为一个过于熟悉和喜爱React的人,我想尽可能地用React来实现。
所以,我开始寻找能否通过React组件来实现,结果发现了一个叫做“React Email”的库。
看起来使用起来也很方便,所以我决定在这里进行实现。

2 電子郵件投遞服務

React Email似乎也支持SendGrid。
看起来它还支持其他一些功能,但其中最方便的是“Resend”服务。
说明页面上写着以下英文:

当与其他服务集成时,您需要在发送之前将React模板转换为HTML。Resend会为您处理这些事情。

据说Resend就是可以直接使用,而无需将React转换为HTML,因为它是由React Email团队构建的,所以使用起来非常方便,因此我们立即采用。
※由于Resend与Vercel的兼容性很好,所以以后可能会成为默认选项吧?

回复邮件

使用React Email模板创建正文。

选择与要实施的引导邮件最接近的模板,并在借用的同时准备自己的原创模板似乎是个不错的选择。

安装React Email

如果你要在现有的仓库中进行设置,请使用这个手册来安装软件包。
基本上按照说明来进行操作,但由于邮件模板中还要使用@react-email/components,因此也要先安装它。

重新发送

接下来是重新发送设置。
大致需要进行以下步骤:
登录(建议使用GitHub认证)
创建API密钥
进行DNS记录设置。

スクリーンショット 2023-07-04 22.41.20.png
スクリーンショット 2023-07-04 22.53.46.png

如果设置正确,Resend仪表板上的状态将显示为“已验证”,如上方的两张图片所示。

重新安装Resend,设置APIKEY。

请使用以下命令进行安装:
npm install resend
yarn add resend

然后,在.env文件中将Resend的APIKEY设置如下:
RESEND_API_KEY=re_XXXXXXXXXX。

代码的实现

一旦到这里,就该开始编写代码了。

将本文的模板进行组件化

由于这次是试行,所以我们将直接使用Slack确认页面的模板。让我们直接将其组件化。

组件/邮件/Slack.tsx

import {
  Body,
  Container,
  Column,
  Head,
  Heading,
  Html,
  Img,
  Link,
  Preview,
  Row,
  Section,
  Text,
} from '@react-email/components';
import * as React from 'react';

interface SlackConfirmEmailProps {
  validationCode?: string;
}

const baseUrl = process.env.VERCEL_URL
  ? `https://${process.env.VERCEL_URL}`
  : '';

export const SlackConfirmEmail = ({
  validationCode = 'DJZ-TLX',
}: SlackConfirmEmailProps) => (
  <Html>
    <Head />
    <Preview>Confirm your email address</Preview>
    <Body style={main}>
      <Container style={container}>
        <Section style={logoContainer}>
          <Img
            src={`${baseUrl}/static/slack-logo.png`}
            width="120"
            height="36"
            alt="Slack"
          />
        </Section>
        <Heading style={h1}>Confirm your email address</Heading>
        <Text style={heroText}>
          Your confirmation code is below - enter it in your open browser window
          and we'll help you get signed in.
        </Text>

        <Section style={codeBox}>
          <Text style={confirmationCodeText}>{validationCode}</Text>
        </Section>

        <Text style={text}>
          If you didn't request this email, there's nothing to worry about - you
          can safely ignore it.
        </Text>

        <Section>
          <Row style={footerLogos}>
            <Column style={{ width: '66%' }}>
              <Img
                src={`${baseUrl}/static/slack-logo.png`}
                width="120"
                height="36"
                alt="Slack"
              />
            </Column>
            <Column>
              <Row>
                <Column>
                  <Link href="/">
                    <Img
                      src={`${baseUrl}/static/slack-twitter.png`}
                      width="32"
                      height="32"
                      alt="Slack"
                      style={socialMediaIcon}
                    />
                  </Link>
                </Column>
                <Column>
                  <Link href="/">
                    <Img
                      src={`${baseUrl}/static/slack-facebook.png`}
                      width="32"
                      height="32"
                      alt="Slack"
                      style={socialMediaIcon}
                    />
                  </Link>
                </Column>
                <Column>
                  <Link href="/">
                    <Img
                      src={`${baseUrl}/static/slack-linkedin.png`}
                      width="32"
                      height="32"
                      alt="Slack"
                      style={socialMediaIcon}
                    />
                  </Link>
                </Column>
              </Row>
            </Column>
          </Row>
        </Section>

        <Section>
          <Link
            style={footerLink}
            href="https://slackhq.com"
            target="_blank"
            rel="noopener noreferrer"
          >
            Our blog
          </Link>
          &nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
          <Link
            style={footerLink}
            href="https://slack.com/legal"
            target="_blank"
            rel="noopener noreferrer"
          >
            Policies
          </Link>
          &nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
          <Link
            style={footerLink}
            href="https://slack.com/help"
            target="_blank"
            rel="noopener noreferrer"
          >
            Help center
          </Link>
          &nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
          <Link
            style={footerLink}
            href="https://slack.com/community"
            target="_blank"
            rel="noopener noreferrer"
            data-auth="NotApplicable"
            data-linkindex="6"
          >
            Slack Community
          </Link>
          <Text style={footerText}>
            ©2022 Slack Technologies, LLC, a Salesforce company. <br />
            500 Howard Street, San Francisco, CA 94105, USA <br />
            <br />
            All rights reserved.
          </Text>
        </Section>
      </Container>
    </Body>
  </Html>
);

export default SlackConfirmEmail;

const footerText = {
  fontSize: '12px',
  color: '#b7b7b7',
  lineHeight: '15px',
  textAlign: 'left' as const,
  marginBottom: '50px',
};

const footerLink = {
  color: '#b7b7b7',
  textDecoration: 'underline',
};

const footerLogos = {
  marginBottom: '32px',
  paddingLeft: '8px',
  paddingRight: '8px',
  width: '100%',
};

const socialMediaIcon = {
  display: 'inline',
  marginLeft: '32px',
};

const main = {
  backgroundColor: '#ffffff',
  margin: '0 auto',
  fontFamily:
    "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
};

const container = {
  maxWidth: '600px',
  margin: '0 auto',
};

const logoContainer = {
  marginTop: '32px',
};

const h1 = {
  color: '#1d1c1d',
  fontSize: '36px',
  fontWeight: '700',
  margin: '30px 0',
  padding: '0',
  lineHeight: '42px',
};

const heroText = {
  fontSize: '20px',
  lineHeight: '28px',
  marginBottom: '30px',
};

const codeBox = {
  background: 'rgb(245, 244, 245)',
  borderRadius: '4px',
  marginRight: '50px',
  marginBottom: '30px',
  padding: '43px 23px',
};

const confirmationCodeText = {
  fontSize: '30px',
  textAlign: 'center' as const,
  verticalAlign: 'middle',
};

const text = {
  color: '#000',
  fontSize: '14px',
  lineHeight: '24px',
};
スクリーンショット 2023-07-04 7.31.29.png

将模板发送到服务器上。

服务器可以选择从API Routes、Route Handlers (Next.js App Router)或自定义后端中进行选择,但是此次选择API Routes(/api/send)。
如上图所示,在模板下方创建一个“发送消息”按钮,并在按钮点击时的onClick事件中向服务器发送模板。
下面是按钮点击时执行的函数代码(handleButtonClick)的示例。

在任意页面内的组件上。

// ボタンの制御用
 const [isButtonClicked, setIsButtonClicked] = useState(false)

  const handleButtonClick = (templateName: string, code?: string) => {
    setIsButtonClicked(true)

    // 宛先のメールアドレスの配列
    const recipients = [
      "hoge@gmail.com",
      "hoge@yahoo.co.jp",
      "hogehoge@gmail.com",
    ]
    // APIルートへのフェッチ用
    fetch("/api/send", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        react: templateName,
        to: recipients, 
      }),
    })
      
      .then((response) => response.json())
      .then((data) => {
        console.log(data)
        setIsButtonClicked(false)
      })
      .catch((error) => {
        console.error("Error:", error)
        setIsButtonClicked(false)
      })
  }

在这些参数中,我设置了一个叫recipients的参数,它是一个包含多个电子邮件地址的数组。
通过传递这个参数,可以实现批量发送邮件。

从服务器上进行批量邮件发送

请将以下内容用中文进行释义:api/send.ts。

/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
/* eslint-disable import/no-anonymous-default-export */
import type { NextApiRequest, NextApiResponse } from "next"
import { Resend } from "resend"
import Slack from "../../components/mail/Slack"

const resend = new Resend(process.env.RESEND_API_KEY)

export default async (req: NextApiRequest, res: NextApiResponse) => {
  try {
    const { react, to: recipients } = req.body // ここでリクエストボディからデータを取得
    // reactの値によってコンポーネントを選択
    let selectedComponent
    if (react === "Slack") selectedComponent = Slack()
    else throw new Error("Invalid react value")

    for (const recipient of recipients) {
      const data = await resend.emails.send({
        from: "demo-mail@hogehoge.com",
        to: recipient,
        subject: "テストメール",
        html: "<strong>TEST</strong>",
        react: selectedComponent,
      })
    }

    res.status(200).json({ message: "Emails sent successfully." })
  } catch (error) {
    if (error instanceof Error) {
      res.status(400).json({ message: error.message })
    } else {
      res.status(400).json({ message: "An unknown error occurred." })
    }
  }
}

另外,上述代码中的批量分发

    for (const recipient of recipients) {
      const data = await resend.emails.send({
        from: "demo-mail@hogehoge.com",
        to: recipient,
        subject: "テストメール",
        html: "<strong>TEST</strong>",
        react: selectedComponent,
      })
    }

我在循环的部分进行了实现。

スクリーンショット 2023-07-05 14.19.48.png

这样一来,暂时算是达到了这次的目标!

如果您想将每个用户的特殊信息嵌入正文中,……?

虽然到目前为止再说这个有些晚了,但是像促销等批量发送的邮件可以使用相同的模板发送给所有用户,但如果是像参考所用的Slack验证码模板一样,需要在正文中嵌入动态值并向个别用户发送。
(如果将模板示例用于Google规则更改通知就好了…)
在这种情况下,需要使用后端拥有的用户信息,因此根据项目的不同,最好使用自己的后端处理发送。比如Python等。
顺便提一下,重新发送似乎也可以在Python或Go中使用。
我打算以后试试这些方面。

结束

这次我们在调查的同时进行了实施,所以花了一些时间,但是一旦熟悉了,我认为就可以轻松地进行实施了。
对于追求效率的工程师来说,使用React组件可以无缝完成从文本创建到邮件发送的整个过程,这非常令人感激。
如果有机会,请尝试一下。

请从根源中获取灵感

请参考 React Email x Resend 体验文章,它更全面地覆盖了下一代邮件服务的内容。虽然我在写作中过于关注具体实务,并且内容有些零散,但还是希望您也能参考这篇文章。

广告
将在 10 秒后关闭
bannerAds