🦁

メールの HTML を React + TypeScript + TailwindCSS で書く

2021/02/17に公開

React コンポーネントをサーバーサイドレンダリングすることで、メールの HTML を React で書くことができる。
フロントエンドで React を使っている場合、一貫性のある技術スタックになって嬉しい。また TypeScript であれば型安全にメールがかけるので、なお嬉しい。

基本形

基本的には、下記のようにすれば、静的な HTML が吐き出せる。これを Mailgun などに送れば OK。
Props を使って、型安全にメールのテンプレートに変数を渡すことができる。

import React from "react"
import ReactDOMServer from "react-dom/server"

const Email = ({ userName }: { userName: string }) => (
  <html>
    <body>
      <div>
        <p>Hello, {userName}!</p>
      </div>
    </body>
  </html>
)

const html = ReactDOMServer.renderToStaticMarkup(<Email userName="Kazuya" />)

CSS を入れる

CSS は <head /> の中に書いてしまうのが一番簡単。また Juice を使ってインライン化すれば、 Outlook などにも対応できる。

import juice from "juice"
import React from "react"
import ReactDOMServer from "react-dom/server"

const Email = () => (
  <html>
    <head>
      <style>{`
        .button {
	  padding: 4px;
	  background-color: #333;
	  color: #fff;
	}
      `}</style>
    </head>
    <body>
      <div>
        <p>Hello, email!</p>
        <button className="button">LEARN MORE</button>
      </div>
    </body>
  </html>
)

const staticMarkup = ReactDOMServer.renderToStaticMarkup(
  <Email userName="Kazuya" />,
)
const html = juice(staticMarkup)

Context を利用する

このままでも良いのだが、 React らしく Context API を使うと更に便利。使いどころとしては、定型的な文章、例えば送信するユーザー情報や Unsubscribe のリンクを孫コンポーネント等から利用できるようにする。

import juice from "juice"
import React from "react"
import ReactDOMServer from "react-dom/server"

const EmailContext = React.createContext()

const Layout: React.FC = ({ children }) => {
  const { user } = React.useContext(EmailContext)

  return (
    <html>
      <body>
        <div>
          <div>Hey ${user.name},</div>
          <div>{children}</div>
        </div>
        <a href={`https://myapp.com/unsubscribe?token=${user.token}`}>
          Unsubscribe
        </a>
      </body>
    </html>
  )
}

const Email = () => (
  <Layout>
    <div>Hello from Email</div>
  </Layout>
)

const staticMarkup = ReactDOMServer.renderToStaticMarkup(
  <EmailContext.Provider value={{ name: "Kazuya", token: "123456abc" }}>
    <Email />
  </EmailContext.Provider>,
)
const html = juice(staticMarkup)

おまけ: TailwindCSS を入れる

TailwindCSS をフロントエンドで使っている場合、同じ要領で書けると更に嬉しい。単純に <style> 内に TailwindCSS を全て入れても良いのだが、せっかくなので tailwind-rn というライブラリを使ってみた。

もともとは React Native 用のライブラリだが、やっていることは TailwindCSS のクラスを React が扱える style のオブジェクトに変換しているだけなので、いい感じに使える。普通に React Native で TailwindCSS 使いたい時にもお薦め。

https://github.com/vadimdemedes/tailwind-rn

import React from "react"
import ReactDOMServer from "react-dom/server"
import tailwind from "tailwind-rn"

const Email = () => (
  <html>
    <body>
      <div>
        <p>Hello, email!</p>
        <button style={tailwind("p-1 bg-gray-800 text-white")}>
          LEARN MORE
        </button>
      </div>
    </body>
  </html>
)

const html = ReactDOMServer.renderToStaticMarkup(element)

tailwind-rn はインラインで CSS を記述するようになるので、 TailwindCSS で完結させれば Juice が必要なくなったのも嬉しい。

注意点としては text-lg 等のスタイルを利用すると、 line-height がおかしなことになる。結局 Juice でグローバルに * { line-height: 1.6 !important } というスタイルを当てているが、もっと良い解決策がありそう。単位を必ず px にするとか。

Discussion