mcpField
E
email-template-builder

email-template-builder

github.com/borghei開発1.0k99 回インストール1 週間前に更新
リポジトリ

email-template-builder について

トランザクションメールのテンプレートと送信基盤を構築するスキルです。エンジニアチームは React Email または MJML で再利用可能な多言語テンプレートを作成し、プレビューや配信性、クライアント互換性の確認を行えます。

主な機能

  1. 01再利用可能なReact EmailまたはMJMLテンプレートの作成
  2. 02複数プロバイダー向け送信処理の抽象化
  3. 03ホットリロードによるテンプレートのプレビュー
  4. 04多言語対応とダークモード
  5. 05スパムリスクとクライアント互換性の確認

活用シーン

  1. 01パスワードリセットや請求書メールの作成
  2. 02開発中のテンプレート変更のプレビュー
  3. 03トランザクションメールテンプレートの多言語化
  4. 04本番送信前の表示確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/borghei/claude-skills --skill email-template-builder

インストール後、Claude Code で/email-template-builderを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

scripts/
render_size_analyzer.py
spam_score_checker.py
template_validator.py

詳細

メールテンプレートビルダー

ティア: パワフル カテゴリー: エンジニアリング/マーケティング タグ: メールテンプレート、React Email、MJML、レスポンシブメール、到達率、トランザクションメール、ダークモード

概要

トランザクションメールシステムを一式構築します。React Email または MJML を使ったコンポーネントベースのテンプレート、複数プロバイダーに対応した送信の抽象化、ホットリロード付きのローカルプレビュー、i18n 対応、ダークモード、迷惑メール対策、UTM トラッキングを実現します。主要なメールプロバイダーで使える、本番環境対応のコードを出力します。

このスキルでは、メールのレンダリングと送信のインフラを構築します。メール本文の作成や配信シーケンスの設計には、email-sequence を使用してください。


最初に確認すること

テンプレートを構築する前に、以下の情報を確認してください。不明または曖昧な項目がある場合は、推測せずに質問してください。

  • フレームワーク — React Email と MJML のどちらを使うか、チームが React/TypeScript を使用しているか(コードベース全体に影響します。以下の比較表を参照してください)
  • 必要なテンプレート — welcome、password reset、invoice、trial-expiring、digest、team-invite など(ひな形を作成するテンプレートファイルが決まります)
  • 送信プロバイダー — Resend、SendGrid、Postmark、SES(構築するプロバイダーアダプターが決まります)
  • ロケールとダークモード — 必要な言語とダークモードへの対応(i18n システムとベースレイアウトのスタイルが決まります)

停止ルール:出力内容に最も影響する2〜3項目だけを質問してください。ユーザーが「とりあえず下書きを」と言った場合は、そのまま進め、成果物の冒頭に前提条件を記載してください。

アーキテクチャの選択:React Email と MJML

要素React EmailMJML
コンポーネントの再利用React のコンポーネントモデルをフル活用一部対応(mj-attributes)
TypeScriptネイティブ対応ビルドステップが必要
プレビューサーバー標準搭載(email dev)別途セットアップが必要
メールクライアントとの互換性良好(テーブルにレンダリング)優秀(実績のある方式)
ダークモードCSS メディアクエリCSS メディアクエリ
学習コスト低い(React の知識があれば)低い(HTML に似た構文)
最適な用途すでに React を使用しているチームメールクライアントとの最大限の互換性

推奨: SaaS を提供する TypeScript チームには React Email をおすすめします。Outlook、Gmail、Apple Mail、旧式のクライアントとの最大限の互換性が必要なマーケティングチームには MJML をおすすめします。


プロジェクト構成

emails/
├── components/
│   ├── layout/
│   │   ├── base-layout.tsx          # Shared wrapper: header, footer, styles
│   │   ├── button.tsx               # CTA button component
│   │   └── divider.tsx              # Styled horizontal rule
│   ├── blocks/
│   │   ├── hero.tsx                 # Hero section with heading + text
│   │   ├── feature-row.tsx          # Icon + text feature highlight
│   │   ├── testimonial.tsx          # Quote + attribution
│   │   └── pricing-table.tsx        # Plan comparison
├── templates/
│   ├── welcome.tsx                  # Welcome / confirm email
│   ├── password-reset.tsx           # Password reset link
│   ├── invoice.tsx                  # Payment receipt / invoice
│   ├── trial-expiring.tsx           # Trial expiration warning
│   ├── weekly-digest.tsx            # Activity summary
│   └── team-invite.tsx              # Team invitation
├── lib/
│   ├── send.ts                      # Unified send function
│   ├── providers/
│   │   ├── resend.ts                # Resend adapter
│   │   ├── sendgrid.ts              # SendGrid adapter
│   │   ├── postmark.ts              # Postmark adapter
│   │   └── ses.ts                   # AWS SES adapter
│   ├── tracking.ts                  # UTM parameter injection
│   └── render.ts                    # Template rendering
├── i18n/
│   ├── en.ts                        # English strings
│   ├── de.ts                        # German strings
│   └── types.ts                     # Typed translation keys
└── package.json

ベースレイアウトコンポーネント

// emails/components/layout/base-layout.tsx
import {
  Body, Container, Head, Html, Img, Preview,
  Section, Text, Hr, Font
} from "@react-email/components";

interface BaseLayoutProps {
  preview: string;
  locale?: string;
  children: React.ReactNode;
}

export function BaseLayout({ preview, locale = "en", children }: BaseLayoutProps) {
  return (
    <Html lang={locale}>
      <Head>
        <Font
          fontFamily="Inter"
          fallbackFontFamily="Arial"
          webFont={{
            url: "https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2",
            format: "woff2",
          }}
          fontWeight={400}
          fontStyle="normal"
        />
        <style>{`
          @media (prefers-color-scheme: dark) {
            .email-body { background-color: #111827 !important; }
            .email-container { background-color: #1f2937 !important; }
            .email-text { color: #e5e7eb !important; }
            .email-heading { color: #f9fafb !important; }
            .email-muted { color: #9ca3af !important; }
          }
          @media only screen and (max-width: 600px) {
            .email-container { width: 100% !important; padding: 16px !important; }
          }
        `}</style>
      </Head>
      <Preview>{preview}</Preview>
      <Body className="email-body" style={body}>
        <Container className="email-container" style={container}>
          <Section style={header}>
            <Img
              src={`${process.env.ASSET_URL}/logo.png`}
              width={120} height={36} alt="[Product]"
            />
          </Section>
          <Section style={content}>{children}</Section>
          <Hr className="email-muted" style={divider} />
          <Section style={footer}>
            <Text className="email-muted" style={footerText}>
              [Company] Inc. - [Address]
            </Text>
            <Text className="email-muted" style={footerText}>
              <a href="{{unsubscribe_url}}" style={link}>Unsubscribe</a>
              {" | "}
              <a href="{{preferences_url}}" style={link}>Email Preferences</a>
              {" | "}
              <a href="{{privacy_url}}" style={link}>Privacy Policy</a>
            </Text>
          </Section>
        </Container>
      </Body>
    </Html>
  );
}

// Styles (inline for email client compatibility)
const body = { backgroundColor: "#f3f4f6", fontFamily: "Inter, Arial, sans-serif", margin: 0, padding: "40px 0" };
const container = { maxWidth: "600px", margin: "0 auto", backgroundColor: "#ffffff", borderRadius: "8px", overflow: "hidden" };
const header = { padding: "24px 32px", borderBottom: "1px solid #e5e7eb" };
const content = { padding: "32px" };
const divider = { borderColor: "#e5e7eb", margin: "0 32px" };
const footer = { padding: "24px 32px" };
const footerText = { fontSize: "12px", color: "#6b7280", textAlign: "center" as const, margin: "4px 0", lineHeight: "1.6" };
const link = { color: "#6b7280", textDecoration: "underline" };

テンプレート例

ウェルカムメール
// emails/templates/welcome.tsx
import { Button, Heading, Text } from "@react-email/components";
import { BaseLayout } from "../components/layout/base-layout";

interface WelcomeProps {
  name: string;
  confirmUrl: string;
  trialDays?: number;
}

export default function Welcome({ name, confirmUrl, trialDays = 14 }: WelcomeProps) {
  return (
    <BaseLayout preview={`Welcome, ${name}! Confirm your email to get started.`}>
      <Heading className="email-heading" style={h1}>
        Welcome to [Product], {name}
      </Heading>
      <Text className="email-text" style={text}>
        You have {trialDays} days to explore everything -- no credit card required.
        Confirm your email to activate your account:
      </Text>
      <Button href={confirmUrl} style={button}>
        Confirm Email Address
      </Button>
      <Text className="email-muted" style={muted}>
        Button not working? Paste this link in your browser:{" "}
        <a href={confirmUrl} style={linkStyle}>{confirmUrl}</a>
      </Text>
    </BaseLayout>
  );
}

const h1 = { fontSize: "24px", fontWeight: "700", color: "#111827", margin: "0 0 16px", lineHeight: "1.3" };
const text = { fontSize: "16px", lineHeight: "1.6", color: "#374151", margin: "0 0 24px" };
const button = { backgroundColor: "#4f46e5", color: "#ffffff", borderRadius: "6px", fontSize: "16px", fontWeight: "600", padding: "12px 24px", textDecoration: "none", display: "inline-block" };
const muted = { fontSize: "13px", color: "#6b7280", marginTop: "24px", lineHeight: "1.5" };
const linkStyle = { color: "#4f46e5", wordBreak: "break-all" as const };
請求書メール
// emails/templates/invoice.tsx
import { Row, Column, Section, Heading, Text, Hr, Button } from "@react-email/components";
import { BaseLayout } from "../components/layout/base-layout";

interface LineItem { description: string; amount: number; }

interface InvoiceProps {
  name: string;
  invoiceNumber: string;
  date: string;
  dueDate: string;
  items: LineItem[];
  total: number;
  currency?: string;
  downloadUrl: string;
}

export default function Invoice({
  name, invoiceNumber, date, dueDate, items,
  total, currency = "USD", downloadUrl,
}: InvoiceProps) {
  const fmt = new Intl.NumberFormat("en-US", { style: "currency", currency });

  return (
    <BaseLayout preview={`Invoice ${invoiceNumber} -- ${fmt.format(total / 100)}`}>
      <Heading className="email-heading" style={h1}>
        Invoice #{invoiceNumber}
      </Heading>
      <Text className="email-text" style={text}>Hi {name},</Text>
      <Text className="email-text" style={text}>
        Here is your invoice. Thank you for your business.
      </Text>

      {/* Meta row */}
      <Section style={metaBox}>
        <Row>
          <Column>
            <Text style={metaLabel}>Invoice Date</Text>
            <Text style={metaValue}>{date}</Text>
          </Column>
          <Column>
            <Text style={metaLabel}>Due Date</Text>
            <Text style={metaValue}>{dueDate}</Text>
          </Column>
          <Column>
            <Text style={metaLabel}>Amount Due</Text>
            <Text style={metaValueBold}>{fmt.format(total / 100)}</Text>
          </Column>
        </Row>
      </Section>

      {/* Line items */}
      {items.map((item, i) => (
        <Row key={i} style={i % 2 === 0 ? rowEven : rowOdd}>
          <Column><Text style={cell}>{item.description}</Text></Column>
          <Column><Text style={cellRight}>{fmt.format(item.amount / 100)}</Text></Column>
        </Row>
      ))}
      <Hr style={divider} />
      <Row>
        <Column><Text style={totalLabel}>Total</Text></Column>
        <Column><Text style={totalValue}>{fmt.format(total / 100)}</Text></Column>
      </Row>

      <Button href={downloadUrl} style={button}>
        Download PDF
      </Button>
    </BaseLayout>
  );
}

const h1 = { fontSize: "24px", fontWeight: "700", color: "#111827", margin: "0 0 16px" };
const text = { fontSize: "15px", lineHeight: "1.6", color: "#374151", margin: "0 0 12px" };
const metaBox = { backgroundColor: "#f9fafb", borderRadius: "8px", padding: "16px", margin: "16px 0" };
const metaLabel = { fontSize: "11px", color: "#6b7280", fontWeight: "600", textTransform: "uppercase" as const, margin: "0 0 4px", letterSpacing: "0.05em" };
const metaValue = { fontSize: "14px", color: "#111827", margin: "0" };
const metaValueBold = { fontSize: "18px", fontWeight: "700", color: "#4f46e5", margin: "0" };
const rowEven = { backgroundColor: "#ffffff" };
const rowOdd = { backgroundColor: "#f9fafb" };
const cell = { fontSize: "14px", color: "#374151", padding: "10px 12px" };
const cellRight = { ...cell, textAlign: "right" as const };
const divider = { borderColor: "#e5e7eb", margin: "8px 0" };
const totalLabel = { fontSize: "16px", fontWeight: "700", color: "#111827", padding: "8px 12px" };
const totalValue = { ...totalLabel, textAlign: "right" as const };
const button = { backgroundColor: "#4f46e5", color: "#ffffff", borderRadius: "6px", padding: "12px 24px", fontSize: "15px", fontWeight: "600", textDecoration: "none", display: "inline-block", marginTop: "16px" };

複数プロバイダー対応の送信抽象化

// emails/lib/send.ts
import { render } from "@react-email/render";

interface EmailPayload {
  to: string;
  subject: string;
  template: React.ReactElement;
  tags?: Record<string, string>;
}

interface EmailProvider {
  send(payload: { to: string; subject: string; html: string; text: string; tags?: Record<string, string> }): Promise<{ id: string }>;
}

// Provider factory
function getProvider(): EmailProvider {
  const provider = process.env.EMAIL_PROVIDER || "resend";
  switch (provider) {
    case "resend": return require("./providers/resend").default;
    case "sendgrid": return require("./providers/sendgrid").default;
    case "postmark": return require("./providers/postmark").default;
    case "ses": return require("./providers/ses").default;
    default: throw new Error(`Unknown email provider: ${provider}`);
  }
}

export async function sendEmail(payload: EmailPayload) {
  const html = addTracking(render(payload.template), { campaign: payload.tags?.type || "transactional" });
  const text = render(payload.template, { plainText: true });

  return getProvider().send({
    to: payload.to,
    subject: payload.subject,
    html,
    text,
    tags: payload.tags,
  });
}

UTM トラッキングの挿入

// emails/lib/tracking.ts
interface TrackingConfig {
  campaign: string;
  source?: string;
  medium?: string;
}

export function addTracking(html: string, config: TrackingConfig): string {
  const params = new URLSearchParams({
    utm_source: config.source || "email",
    utm_medium: config.medium || "transactional",
    utm_campaign: config.campaign,
  }).toString();

  // Add UTM to all internal links (skip unsubscribe and external)
  return html.replace(
    /href="(https?:\/\/(?:www\.)?yourdomain\.com[^"]*?)"/g,
    (match, url) => {
      const sep = url.includes("?") ? "&" : "?";
      return `href="${url}${sep}${params}"`;
    }
  );
}

i18n システム

// emails/i18n/types.ts
export interface EmailStrings {
  welcome: {
    preview: (name: string) => string;
    heading: (name: string) => string;
    body: (days: number) => string;
    cta: string;
    fallbackLink: string;
  };
  invoice: {
    preview: (number: string, amount: string) => string;
    heading: (number: string) => string;
    greeting: (name: string) => string;
    downloadCta: string;
  };
  common: {
    unsubscribe: string;
    preferences: string;
    privacy: string;
  };
}

// emails/i18n/en.ts
import type { EmailStrings } from "./types";
export const en: EmailStrings = {
  welcome: {
    preview: (name) => `Welcome, ${name}! Confirm your email to get started.`,
    heading: (name) => `Welcome to [Product], ${name}`,
    body: (days) => `You have ${days} days to explore everything -- no credit card required.`,
    cta: "Confirm Email Address",
    fallbackLink: "Button not working? Paste this link in your browser:",
  },
  // ... other templates
};

// emails/i18n/de.ts
import type { EmailStrings } from "./types";
export const de: EmailStrings = {
  welcome: {
    preview: (name) => `Willkommen, ${name}! Bestaetigen Sie Ihre E-Mail.`,
    heading: (name) => `Willkommen bei [Product], ${name}`,
    body: (days) => `Sie haben ${days} Tage Zeit, alles zu erkunden -- keine Kreditkarte noetig.`,
    cta: "E-Mail-Adresse bestaetigen",
    fallbackLink: "Button funktioniert nicht? Fuegen Sie diesen Link in Ihren Browser ein:",
  },
  // ... other templates
};

到達率チェックリスト

DNS レコード(必須)
  • SPF:送信ドメインに v=spf1 include:_spf.provider.com ~all を設定
  • DKIM:プロバイダー固有の CNAME レコードを設定
  • DMARC:v=DMARC1; p=quarantine; rua=mailto:[email protected]
  • Return-Path:送信ドメインと一致していること(プロバイダーのデフォルトではないこと)
コンテンツのルール
  • 送信者は独自ドメインを使用する(@gmail.com は使用しない)
  • 件名は50文字未満にし、すべて大文字にせず、迷惑メール判定につながる表現を避ける
  • テキストと画像の比率:テキストを最低60%にする
  • HTML と合わせてプレーンテキスト版も含める
  • すべてのメールに配信停止リンクを含める(CAN-SPAM、GDPR、ワンクリック対応)
  • フッターに物理的な郵送先住所を記載する(CAN-SPAM の要件)
  • URL 短縮サービスを使わない(ブランド名入りの完全なリンクを使用する)
  • 1通につき主要な CTA は1つにする
  • すべての画像に alt テキストを設定する
  • HTML が検証済みであること(タグの破損や閉じ忘れがないこと)
インフラ
  • トランザクションメールとマーケティングメールで送信ドメインを分ける
  • 新しい送信ドメインは徐々にウォームアップする(1日50通から始め、毎週2倍に増やす)
  • バウンス率を監視する(ハードバウンスは2%未満)
  • バウンスと苦情を自動的に処理する
  • 本番送信前に Mail-Tester.com でテストする(目標:9/10以上)

メールクライアントとの互換性

既知の癖
クライアント癖回避策
Outlook (Windows)CSS grid/flexbox に対応しておらず、画像の margin を無視します<table> レイアウトを使用します(React Email が処理します)
Gmail<head> のスタイルを削除し、CSS を制限しますすべてのスタイルをインライン化します(React Email が処理します)
Apple Mail最も対応状況が良く、ダークモードも適切に表示します標準的な方法で問題ありません
Yahoo MailCSS の対応が限定的です高度なセレクターを避けます
Outlook.com背景画像を削除しますフォールバックとして background-color を使用します
テストマトリックス

本番環境に移行する前に、すべてのテンプレートを以下のクライアントでテストしてください。

優先度クライアント方法
CriticalGmail (web)テストメールを送信
CriticalApple Mail (iOS)テストメールを送信
CriticalOutlook (Windows, latest)Litmus または Email on Acid
HighOutlook.com (web)テストメールを送信
HighGmail (Android)テストメールを送信
MediumYahoo MailLitmus
MediumOutlook (Mac)テストメールを送信

開発ワークフロー

# Start preview server with hot reload
npx email dev --dir emails/templates --port 3001

# Export to static HTML (for testing with Litmus/Email on Acid)
npx email export --dir emails/templates --outDir emails/dist

# Send test email
npx tsx emails/lib/send-test.ts --template welcome --to [email protected]

# Validate HTML
npx email lint --dir emails/templates

よくある落とし穴

落とし穴結果防止策
CSS grid/flexbox の使用Outlook でレイアウトが崩れますReact Email の Row/Column を使用します(テーブルに変換して表示されます)
コンテナの幅が 600px を超えるGmail モバイルで崩れますコンテナの max-width を 600px にします
プレーンテキスト版がない配信到達率スコアが低下します常に render(template, { plainText: true }) でプレーンテキストを生成します
トランザクションメールとマーケティングメールで同じドメインを使用マーケティングメールへの苦情により、トランザクションメールの配信到達率が大幅に低下します送信ドメイン/サブドメインを分けます
メールのウォームアップを省略メールが迷惑メールとして扱われます少量から始め、2-4 週間かけて徐々に増やします
ダークモードへの対応を無視30% 以上のユーザーにとってメールが読めなくなります!important を付けた prefers-color-scheme: dark メディアクエリを追加します

関連スキル

スキル使用する場面
email-sequenceメールの文面を作成し、自動化フローを設計するとき
analytics-trackingメールのエンゲージメント追跡とアトリビューションを設定するとき
launch-strategy製品のローンチに向けてメールテンプレートを調整するとき

トラブルシューティング

症状考えられる原因修正方法
Gmail でメールが途中で切れるHTML が 102KB を超えているrender_size_analyzer.py を実行します。コメントを削除し、圧縮し、base64 画像を置き換えます。
Outlook でレイアウトが崩れるCSS flexbox/grid が使用されているテーブルベースのレイアウトを使用します。互換性を確認するには template_validator.py を実行します。
Gmail でスタイルが削除されるスタイルが <head> のみにあるすべての CSS をインライン化します。React Email が自動的に処理します。
ダークモードで読めないダークモード用 CSS がない!important を付けた prefers-color-scheme: dark メディアクエリを追加します。
配信到達率スコアが低い配信停止機能がない、画像が多すぎるspam_score_checker.py を実行します。RFC 8058 のワンクリック配信停止ヘッダーを追加します。
画像が読み込まれないメールクライアントのデフォルト設定でブロックされている説明的な alt テキストを追加します。テキストと画像の比率を 60% 以上に保ちます。
クライアントによってテンプレートの表示が異なる対応していない CSS プロパティが使われている本番環境で送信する前に、Gmail、Apple Mail、Outlook (Windows) でテストします。

成功基準

  • 本番環境で送信する前に mail-tester.com でスパムスコア 9+/10 を達成している
  • テンプレートが Gmail、Apple Mail、Outlook (Windows) で正しく表示される
  • HTML が 80KB 未満(Gmail の 102KB の切り捨て基準を十分に下回る)
  • テキストと画像の比率が 60% を超えている
  • ダークモードをテスト済みで、30% 以上のユーザーが読める
  • すべての画像に alt テキストと明示的な width/height の指定がある
  • すべてのテンプレートでワンクリック配信停止(RFC 8058)が実装されている
  • トランザクションメールとマーケティングメールで送信ドメインを分けている

対象範囲と制限事項

対象範囲: メールの HTML/CSS テンプレートエンジニアリング、React Email と MJML のコンポーネント、複数プロバイダーに対応する送信抽象化、i18n、ダークモード、配信到達率の基盤、スパムスコアの最適化。

対象範囲外: メールの文面やシーケンスの作成(email-sequence を使用)、マーケティング自動化ワークフロー、メールリスト管理、A/B テストの統計分析。


Python 自動化ツール

1. スパムスコアチェッカー(scripts/spam_score_checker.py)

メールの HTML を分析し、スパムのリスクを評価します。テキストと画像の比率、リンク密度、スパムワード、配信停止リンクの有無、HTML 構造をチェックします。

python scripts/spam_score_checker.py template.html
python scripts/spam_score_checker.py template.html --json
2. テンプレートバリデーター(scripts/template_validator.py)

メールテンプレートについて、クライアントとの互換性(Outlook、Gmail)、アクセシビリティ、レスポンシブデザイン、インラインスタイルを検証します。

python scripts/template_validator.py template.html
python scripts/template_validator.py template.html --json
3. レンダリングサイズアナライザー(scripts/render_size_analyzer.py)

テンプレートのファイルサイズを分析し、レンダリング時のサイズを推定して、詳細な内訳とともに Gmail の 102KB 切り捨てしきい値を超えていないか確認します。

python scripts/render_size_analyzer.py template.html
python scripts/render_size_analyzer.py --dir templates/ --json

コメント