Email template builder

Build complete transactional email systems: React Email templates, provider integration (Resend, Postmark, SendGrid, AWS SES), preview server, i18n support, dark mode, spam optimization, analytics tracking.

How to use it

Claude Code
  1. Run the line below. It pulls the whole folder into ~/.claude/skills/email-template-builder, including the files SKILL.md points to.
  2. Describe your job in plain words. Claude Code follows the skill from there.
Claude Code — installs the whole folder, not just SKILL.md
npx degit alirezarezvani/claude-skills/engineering-team/skills/email-template-builder#main ~/.claude/skills/email-template-builder

For one project only, change the path to .claude/skills/email-template-builder. This skill also uses send.ts, resend.ts, postmark.ts, ses.ts, tracking.ts, en.ts — copying SKILL.md alone won't be enough. See the folder on GitHub.

Claude (web or desktop app)
  1. On this page open ⋯ → Download .md.
  2. Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
  3. Pick the file and Save. Claude shows the name and description and runs a security scan.
  4. Check the skill is switched on.
  5. Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
  1. ChatGPT: make a Project and paste it into Instructions.
  2. Neither? Paste it at the top of a new chat — it works for that chat.
Not working?
  • Check which app you pasted it into — the steps above name the right one.
  • Some skills need the paid tier of Claude or ChatGPT.
Step-by-step guide with screenshots · Ask in the forum

Paste into Claude, ChatGPT or Cursor.

Source of Email template builder

Show the full text440 lines
namedescription
email-template-builderBuild complete transactional email systems: React Email templates, provider integration (Resend, Postmark, SendGrid, AWS SES), preview server, i18n support, dark mode, spam optimization, analytics tracking. Use when adding transactional email to a new product, migrating between email providers, refactoring legacy email templates for accessibility, or adding internationalization to existing templates.

Email Template Builder

Tier: POWERFUL
Category: Engineering Team
Domain: Transactional Email / Communications Infrastructure


Overview

Build complete transactional email systems: React Email templates, provider integration, preview server, i18n support, dark mode, spam optimization, and analytics tracking. Output production-ready code for Resend, Postmark, SendGrid, or AWS SES.


Core Capabilities

  • React Email templates (welcome, verification, password reset, invoice, notification, digest)
  • MJML templates for maximum email client compatibility
  • Multi-provider support with unified sending interface
  • Local preview server with hot reload
  • i18n/localization with typed translation keys
  • Dark mode support using media queries
  • Spam score optimization checklist
  • Open/click tracking with UTM parameters

When to Use

  • Setting up transactional email for a new product
  • Migrating from a legacy email system
  • Adding new email types (invoice, digest, notification)
  • Debugging email deliverability issues
  • Implementing i18n for email templates

Project Structure

emails/
├── components/
│   ├── layout/
│   │   ├── email-layout.tsx       # Base layout with brand header/footer
│   │   └── email-button.tsx       # CTA button component
│   ├── partials/
│   │   ├── header.tsx
│   │   └── footer.tsx
├── templates/
│   ├── welcome.tsx
│   ├── verify-email.tsx
│   ├── password-reset.tsx
│   ├── invoice.tsx
│   ├── notification.tsx
│   └── weekly-digest.tsx
├── lib/
│   ├── send.ts                    # Unified send function
│   ├── providers/
│   │   ├── resend.ts
│   │   ├── postmark.ts
│   │   └── ses.ts
│   └── tracking.ts                # UTM + analytics
├── i18n/
│   ├── en.ts
│   └── de.ts
└── preview/                       # Dev preview server
    └── server.ts

Base Email Layout

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

interface EmailLayoutProps {
  preview: string
  children: React.ReactNode
}

export function EmailLayout({ preview, children }: EmailLayoutProps) {
  return (
    <Html lang="en">
      <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"
        />
        {/* Dark mode styles */}
        <style>{`
          @media (prefers-color-scheme: dark) {
            .email-body { background-color: #0f0f0f !important; }
            .email-container { background-color: #1a1a1a !important; }
            .email-text { color: #e5e5e5 !important; }
            .email-heading { color: #ffffff !important; }
            .email-divider { border-color: #333333 !important; }
          }
        `}</style>
      </Head>
      <Preview>{preview}</Preview>
      <Body className="email-body" style={styles.body}>
        <Container className="email-container" style={styles.container}>
          {/* Header */}
          <Section style={styles.header}>
            <Img src="https://yourapp.com/logo.png" width={120} height={40} alt="MyApp" />
          </Section>
          
          {/* Content */}
          <Section style={styles.content}>
            {children}
          </Section>
          
          {/* Footer */}
          <Hr style={styles.divider} />
          <Section style={styles.footer}>
            <Text style={styles.footerText}>
              MyApp Inc. · 123 Main St · San Francisco, CA 94105
            </Text>
            <Text style={styles.footerText}>
              <a href="{{unsubscribe_url}}" style={styles.link}>Unsubscribe</a>
              {" · "}
              <a href="https://yourapp.com/privacy" style={styles.link}>Privacy Policy</a>
            </Text>
          </Section>
        </Container>
      </Body>
    </Html>
  )
}

const styles = {
  body: { backgroundColor: "#f5f5f5", fontFamily: "Inter, Arial, sans-serif" },
  container: { maxWidth: "600px", margin: "0 auto", backgroundColor: "#ffffff", borderRadius: "8px", overflow: "hidden" },
  header: { padding: "24px 32px", borderBottom: "1px solid #e5e5e5" },
  content: { padding: "32px" },
  divider: { borderColor: "#e5e5e5", margin: "0 32px" },
  footer: { padding: "24px 32px" },
  footerText: { fontSize: "12px", color: "#6b7280", textAlign: "center" as const, margin: "4px 0" },
  link: { color: "#6b7280", textDecoration: "underline" },
}

Welcome Email

// emails/templates/welcome.tsx
import { Button, Heading, Text } from "@react-email/components"
import { EmailLayout } from "../components/layout/email-layout"

interface WelcomeEmailProps {
  name: "string"
  confirmUrl: string
  trialDays?: number
}

export function WelcomeEmail({ name, confirmUrl, trialDays = 14 }: WelcomeEmailProps) {
  return (
    <EmailLayout preview={`Welcome to MyApp, ${name}! Confirm your email to get started.`}>
      <Heading style={styles.h1}>Welcome to MyApp, {name}!</Heading>
      <Text style={styles.text}>
        We're excited to have you on board. You've got {trialDays} days to explore everything MyApp has to offer — no credit card required.
      </Text>
      <Text style={styles.text}>
        First, confirm your email address to activate your account:
      </Text>
      <Button href={confirmUrl} style={styles.button}>
        Confirm Email Address
      </Button>
      <Text style={styles.hint}>
        Button not working? Copy and paste this link into your browser:
        <br />
        <a href={confirmUrl} style={styles.link}>{confirmUrl}</a>
      </Text>
      <Text style={styles.text}>
        Once confirmed, you can:
      </Text>
      <ul style={styles.list}>
        <li>Connect your first project in 2 minutes</li>
        <li>Invite your team (free for up to 3 members)</li>
        <li>Set up Slack notifications</li>
      </ul>
    </EmailLayout>
  )
}

export default WelcomeEmail

const styles = {
  h1: { fontSize: "28px", fontWeight: "700", color: "#111827", margin: "0 0 16px" },
  text: { fontSize: "16px", lineHeight: "1.6", color: "#374151", margin: "0 0 16px" },
  button: { backgroundColor: "#4f46e5", color: "#ffffff", borderRadius: "6px", fontSize: "16px", fontWeight: "600", padding: "12px 24px", textDecoration: "none", display: "inline-block", margin: "8px 0 24px" },
  hint: { fontSize: "13px", color: "#6b7280" },
  link: { color: "#4f46e5" },
  list: { fontSize: "16px", lineHeight: "1.8", color: "#374151", paddingLeft: "20px" },
}

Invoice Email

// emails/templates/invoice.tsx
import { Row, Column, Section, Heading, Text, Hr, Button } from "@react-email/components"
import { EmailLayout } from "../components/layout/email-layout"

interface InvoiceItem { description: string; amount: number }

interface InvoiceEmailProps {
  name: "string"
  invoiceNumber: string
  invoiceDate: string
  dueDate: string
  items: InvoiceItem[]
  total: number
  currency: string
  downloadUrl: string
}

export function InvoiceEmail({ name, invoiceNumber, invoiceDate, dueDate, items, total, currency = "USD", downloadUrl }: InvoiceEmailProps) {
  const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency })

  return (
    <EmailLayout preview={`Invoice ${invoiceNumber} - ${formatter.format(total / 100)}`}>
      <Heading style={styles.h1}>Invoice #{invoiceNumber}</Heading>
      <Text style={styles.text}>Hi {name},</Text>
      <Text style={styles.text}>Here's your invoice from MyApp. Thank you for your continued support.</Text>

      {/* Invoice Meta */}
      <Section style={styles.metaBox}>
        <Row>
          <Column><Text style={styles.metaLabel}>Invoice Date</Text><Text style={styles.metaValue}>{invoiceDate}</Text></Column>
          <Column><Text style={styles.metaLabel}>Due Date</Text><Text style={styles.metaValue}>{dueDate}</Text></Column>
          <Column><Text style={styles.metaLabel}>Amount Due</Text><Text style={styles.metaValueLarge}>{formatter.format(total / 100)}</Text></Column>
        </Row>
      </Section>

      {/* Line Items */}
      <Section style={styles.table}>
        <Row style={styles.tableHeader}>
          <Column><Text style={styles.tableHeaderText}>Description</Text></Column>
          <Column><Text style={{ ...styles.tableHeaderText, textAlign: "right" }}>Amount</Text></Column>
        </Row>
        {items.map((item, i) => (
          <Row key={i} style={i % 2 === 0 ? styles.tableRowEven : styles.tableRowOdd}>
            <Column><Text style={styles.tableCell}>{item.description}</Text></Column>
            <Column><Text style={{ ...styles.tableCell, textAlign: "right" }}>{formatter.format(item.amount / 100)}</Text></Column>
          </Row>
        ))}
        <Hr style={styles.divider} />
        <Row>
          <Column><Text style={styles.totalLabel}>Total</Text></Column>
          <Column><Text style={styles.totalValue}>{formatter.format(total / 100)}</Text></Column>
        </Row>
      </Section>

      <Button href={downloadUrl} style={styles.button}>Download PDF Invoice</Button>
    </EmailLayout>
  )
}

export default InvoiceEmail

const styles = {
  h1: { fontSize: "24px", fontWeight: "700", color: "#111827", margin: "0 0 16px" },
  text: { fontSize: "15px", lineHeight: "1.6", color: "#374151", margin: "0 0 12px" },
  metaBox: { backgroundColor: "#f9fafb", borderRadius: "8px", padding: "16px", margin: "16px 0" },
  metaLabel: { fontSize: "12px", color: "#6b7280", fontWeight: "600", textTransform: "uppercase" as const, margin: "0 0 4px" },
  metaValue: { fontSize: "14px", color: "#111827", margin: 0 },
  metaValueLarge: { fontSize: "20px", fontWeight: "700", color: "#4f46e5", margin: 0 },
  table: { width: "100%", margin: "16px 0" },
  tableHeader: { backgroundColor: "#f3f4f6", borderRadius: "4px" },
  tableHeaderText: { fontSize: "12px", fontWeight: "600", color: "#374151", padding: "8px 12px", textTransform: "uppercase" as const },
  tableRowEven: { backgroundColor: "#ffffff" },
  tableRowOdd: { backgroundColor: "#f9fafb" },
  tableCell: { fontSize: "14px", color: "#374151", padding: "10px 12px" },
  divider: { borderColor: "#e5e5e5", margin: "8px 0" },
  totalLabel: { fontSize: "16px", fontWeight: "700", color: "#111827", padding: "8px 12px" },
  totalValue: { fontSize: "16px", fontWeight: "700", color: "#111827", textAlign: "right" as const, padding: "8px 12px" },
  button: { backgroundColor: "#4f46e5", color: "#fff", borderRadius: "6px", padding: "12px 24px", fontSize: "15px", fontWeight: "600", textDecoration: "none" },
}

Unified Send Function

// emails/lib/send.ts
import { Resend } from "resend"
import { render } from "@react-email/render"
import { WelcomeEmail } from "../templates/welcome"
import { InvoiceEmail } from "../templates/invoice"
import { addTrackingParams } from "./tracking"

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

type EmailPayload =
  | { type: "welcome"; props: Parameters<typeof WelcomeEmail>[0] }
  | { type: "invoice"; props: Parameters<typeof InvoiceEmail>[0] }

export async function sendEmail(to: string, payload: EmailPayload) {
  const templates = {
    welcome: { component: WelcomeEmail, subject: "Welcome to MyApp — confirm your email" },
    invoice: { component: InvoiceEmail, subject: `Invoice from MyApp` },
  }

  const template = templates[payload.type]
  const html = render(template.component(payload.props as any))
  const trackedHtml = addTrackingParams(html, { campaign: payload.type })

  const result = await resend.emails.send({
    from: "MyApp <[email protected]>",
    to,
    subject: template.subject,
    html: trackedHtml,
    tags: [{ name: "email-type", value: payload.type }],
  })

  return result
}

Preview Server Setup

// package.json scripts
{
  "scripts": {
    "email:dev": "email dev --dir emails/templates --port 3001",
    "email:build": "email export --dir emails/templates --outDir emails/out"
  }
}

// Run: npm run email:dev
// Opens: http://localhost:3001
// Shows all templates with live preview and hot reload

i18n Support

// emails/i18n/en.ts
export const en = {
  welcome: {
    preview: (name: string) => `Welcome to MyApp, ${name}!`,
    heading: (name: string) => `Welcome to MyApp, ${name}!`,
    body: (days: number) => `You've got ${days} days to explore everything.`,
    cta: "Confirm Email Address",
  },
}

// emails/i18n/de.ts
export const de = {
  welcome: {
    preview: (name: string) => `Willkommen bei MyApp, ${name}!`,
    heading: (name: string) => `Willkommen bei MyApp, ${name}!`,
    body: (days: number) => `Du hast ${days} Tage Zeit, alles zu erkunden.`,
    cta: "E-Mail-Adresse bestätigen",
  },
}

// Usage in template
import { en, de } from "../i18n"
const t = locale === "de" ? de : en

Spam Score Optimization Checklist

  • Sender domain has SPF, DKIM, and DMARC records configured
  • From address uses your own domain (not gmail.com/hotmail.com)
  • Subject line under 50 characters, no ALL CAPS, no "FREE!!!"
  • Text-to-image ratio: at least 60% text
  • Plain text version included alongside HTML
  • Unsubscribe link in every marketing email (CAN-SPAM, GDPR)
  • No URL shorteners — use full branded links
  • No red-flag words: "guarantee", "no risk", "limited time offer" in subject
  • Single CTA per email — no 5 different buttons
  • Image alt text on every image
  • HTML validates — no broken tags
  • Test with Mail-Tester.com before first send (target: 9+/10)

Analytics Tracking

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

export function addTrackingParams(html: string, params: TrackingParams): string {
  const utmString = new URLSearchParams({
    utm_source: params.source ?? "email",
    utm_medium: params.medium ?? "transactional",
    utm_campaign: params.campaign,
  }).toString()

  // Add UTM params to all links in the email
  return html.replace(/href="(https?:\/\/[^"]+)"/g, (match, url) => {
    const separator = url.includes("?") ? "&" : "?"
    return `href="${url}${separator}${utmString}"`
  })
}

Common Pitfalls

  • Inline styles required — most email clients strip <head> styles; React Email handles this
  • Max width 600px — anything wider breaks on Gmail mobile
  • No flexbox/grid — use <Row> and <Column> from react-email, not CSS grid
  • Dark mode media queries — must use !important to override inline styles
  • Missing plain text — all major providers have a plain text field; always populate it
  • Transactional vs marketing — use separate sending domains/IPs to protect deliverability
1---
2name: "email-template-builder"
3description: "Build complete transactional email systems: React Email templates, provider integration (Resend, Postmark, SendGrid, AWS SES), preview server, i18n support, dark mode, spam optimization, analytics tracking. Use when adding transactional email to a new product, migrating between email providers, refactoring legacy email templates for accessibility, or adding internationalization to existing templates."
4---
5 
6# Email Template Builder
7 
8**Tier:** POWERFUL
9**Category:** Engineering Team
10**Domain:** Transactional Email / Communications Infrastructure
11 
12---
13 
14## Overview
15 
16Build complete transactional email systems: React Email templates, provider integration, preview server, i18n support, dark mode, spam optimization, and analytics tracking. Output production-ready code for Resend, Postmark, SendGrid, or AWS SES.
17 
18---
19 
20## Core Capabilities
21 
22- React Email templates (welcome, verification, password reset, invoice, notification, digest)
23- MJML templates for maximum email client compatibility
24- Multi-provider support with unified sending interface
25- Local preview server with hot reload
26- i18n/localization with typed translation keys
27- Dark mode support using media queries
28- Spam score optimization checklist
29- Open/click tracking with UTM parameters
30 
31---
32 
33## When to Use
34 
35- Setting up transactional email for a new product
36- Migrating from a legacy email system
37- Adding new email types (invoice, digest, notification)
38- Debugging email deliverability issues
39- Implementing i18n for email templates
40 
41---
42 
43## Project Structure
44 
45```
46emails/
47├── components/
48│ ├── layout/
49│ │ ├── email-layout.tsx # Base layout with brand header/footer
50│ │ └── email-button.tsx # CTA button component
51│ ├── partials/
52│ │ ├── header.tsx
53│ │ └── footer.tsx
54├── templates/
55│ ├── welcome.tsx
56│ ├── verify-email.tsx
57│ ├── password-reset.tsx
58│ ├── invoice.tsx
59│ ├── notification.tsx
60│ └── weekly-digest.tsx
61├── lib/
62│ ├── send.ts # Unified send function
63│ ├── providers/
64│ │ ├── resend.ts
65│ │ ├── postmark.ts
66│ │ └── ses.ts
67│ └── tracking.ts # UTM + analytics
68├── i18n/
69│ ├── en.ts
70│ └── de.ts
71└── preview/ # Dev preview server
72 └── server.ts
73```
74 
75---
76 
77## Base Email Layout
78 
79```tsx
80// emails/components/layout/email-layout.tsx
81import {
82 Body, Container, Head, Html, Img, Preview, Section, Text, Hr, Font
83} from "@react-email/components"
84 
85interface EmailLayoutProps {
86 preview: string
87 children: React.ReactNode
88}
89 
90export function EmailLayout({ preview, children }: EmailLayoutProps) {
91 return (
92 <Html lang="en">
93 <Head>
94 <Font
95 fontFamily="Inter"
96 fallbackFontFamily="Arial"
97 webFont={{ url: "https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2", format: "woff2" }}
98 fontWeight={400}
99 fontStyle="normal"
100 />
101 {/* Dark mode styles */}
102 <style>{`
103 @media (prefers-color-scheme: dark) {
104 .email-body { background-color: #0f0f0f !important; }
105 .email-container { background-color: #1a1a1a !important; }
106 .email-text { color: #e5e5e5 !important; }
107 .email-heading { color: #ffffff !important; }
108 .email-divider { border-color: #333333 !important; }
109 }
110 `}</style>
111 </Head>
112 <Preview>{preview}</Preview>
113 <Body className="email-body" style={styles.body}>
114 <Container className="email-container" style={styles.container}>
115 {/* Header */}
116 <Section style={styles.header}>
117 <Img src="https://yourapp.com/logo.png" width={120} height={40} alt="MyApp" />
118 </Section>
119 
120 {/* Content */}
121 <Section style={styles.content}>
122 {children}
123 </Section>
124 
125 {/* Footer */}
126 <Hr style={styles.divider} />
127 <Section style={styles.footer}>
128 <Text style={styles.footerText}>
129 MyApp Inc. · 123 Main St · San Francisco, CA 94105
130 </Text>
131 <Text style={styles.footerText}>
132 <a href="{{unsubscribe_url}}" style={styles.link}>Unsubscribe</a>
133 {" · "}
134 <a href="https://yourapp.com/privacy" style={styles.link}>Privacy Policy</a>
135 </Text>
136 </Section>
137 </Container>
138 </Body>
139 </Html>
140 )
141}
142 
143const styles = {
144 body: { backgroundColor: "#f5f5f5", fontFamily: "Inter, Arial, sans-serif" },
145 container: { maxWidth: "600px", margin: "0 auto", backgroundColor: "#ffffff", borderRadius: "8px", overflow: "hidden" },
146 header: { padding: "24px 32px", borderBottom: "1px solid #e5e5e5" },
147 content: { padding: "32px" },
148 divider: { borderColor: "#e5e5e5", margin: "0 32px" },
149 footer: { padding: "24px 32px" },
150 footerText: { fontSize: "12px", color: "#6b7280", textAlign: "center" as const, margin: "4px 0" },
151 link: { color: "#6b7280", textDecoration: "underline" },
152}
153```
154 
155---
156 
157## Welcome Email
158 
159```tsx
160// emails/templates/welcome.tsx
161import { Button, Heading, Text } from "@react-email/components"
162import { EmailLayout } from "../components/layout/email-layout"
163 
164interface WelcomeEmailProps {
165 name: "string"
166 confirmUrl: string
167 trialDays?: number
168}
169 
170export function WelcomeEmail({ name, confirmUrl, trialDays = 14 }: WelcomeEmailProps) {
171 return (
172 <EmailLayout preview={`Welcome to MyApp, ${name}! Confirm your email to get started.`}>
173 <Heading style={styles.h1}>Welcome to MyApp, {name}!</Heading>
174 <Text style={styles.text}>
175 We're excited to have you on board. You've got {trialDays} days to explore everything MyApp has to offer — no credit card required.
176 </Text>
177 <Text style={styles.text}>
178 First, confirm your email address to activate your account:
179 </Text>
180 <Button href={confirmUrl} style={styles.button}>
181 Confirm Email Address
182 </Button>
183 <Text style={styles.hint}>
184 Button not working? Copy and paste this link into your browser:
185 <br />
186 <a href={confirmUrl} style={styles.link}>{confirmUrl}</a>
187 </Text>
188 <Text style={styles.text}>
189 Once confirmed, you can:
190 </Text>
191 <ul style={styles.list}>
192 <li>Connect your first project in 2 minutes</li>
193 <li>Invite your team (free for up to 3 members)</li>
194 <li>Set up Slack notifications</li>
195 </ul>
196 </EmailLayout>
197 )
198}
199 
200export default WelcomeEmail
201 
202const styles = {
203 h1: { fontSize: "28px", fontWeight: "700", color: "#111827", margin: "0 0 16px" },
204 text: { fontSize: "16px", lineHeight: "1.6", color: "#374151", margin: "0 0 16px" },
205 button: { backgroundColor: "#4f46e5", color: "#ffffff", borderRadius: "6px", fontSize: "16px", fontWeight: "600", padding: "12px 24px", textDecoration: "none", display: "inline-block", margin: "8px 0 24px" },
206 hint: { fontSize: "13px", color: "#6b7280" },
207 link: { color: "#4f46e5" },
208 list: { fontSize: "16px", lineHeight: "1.8", color: "#374151", paddingLeft: "20px" },
209}
210```
211 
212---
213 
214## Invoice Email
215 
216```tsx
217// emails/templates/invoice.tsx
218import { Row, Column, Section, Heading, Text, Hr, Button } from "@react-email/components"
219import { EmailLayout } from "../components/layout/email-layout"
220 
221interface InvoiceItem { description: string; amount: number }
222 
223interface InvoiceEmailProps {
224 name: "string"
225 invoiceNumber: string
226 invoiceDate: string
227 dueDate: string
228 items: InvoiceItem[]
229 total: number
230 currency: string
231 downloadUrl: string
232}
233 
234export function InvoiceEmail({ name, invoiceNumber, invoiceDate, dueDate, items, total, currency = "USD", downloadUrl }: InvoiceEmailProps) {
235 const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency })
236 
237 return (
238 <EmailLayout preview={`Invoice ${invoiceNumber} - ${formatter.format(total / 100)}`}>
239 <Heading style={styles.h1}>Invoice #{invoiceNumber}</Heading>
240 <Text style={styles.text}>Hi {name},</Text>
241 <Text style={styles.text}>Here's your invoice from MyApp. Thank you for your continued support.</Text>
242 
243 {/* Invoice Meta */}
244 <Section style={styles.metaBox}>
245 <Row>
246 <Column><Text style={styles.metaLabel}>Invoice Date</Text><Text style={styles.metaValue}>{invoiceDate}</Text></Column>
247 <Column><Text style={styles.metaLabel}>Due Date</Text><Text style={styles.metaValue}>{dueDate}</Text></Column>
248 <Column><Text style={styles.metaLabel}>Amount Due</Text><Text style={styles.metaValueLarge}>{formatter.format(total / 100)}</Text></Column>
249 </Row>
250 </Section>
251 
252 {/* Line Items */}
253 <Section style={styles.table}>
254 <Row style={styles.tableHeader}>
255 <Column><Text style={styles.tableHeaderText}>Description</Text></Column>
256 <Column><Text style={{ ...styles.tableHeaderText, textAlign: "right" }}>Amount</Text></Column>
257 </Row>
258 {items.map((item, i) => (
259 <Row key={i} style={i % 2 === 0 ? styles.tableRowEven : styles.tableRowOdd}>
260 <Column><Text style={styles.tableCell}>{item.description}</Text></Column>
261 <Column><Text style={{ ...styles.tableCell, textAlign: "right" }}>{formatter.format(item.amount / 100)}</Text></Column>
262 </Row>
263 ))}
264 <Hr style={styles.divider} />
265 <Row>
266 <Column><Text style={styles.totalLabel}>Total</Text></Column>
267 <Column><Text style={styles.totalValue}>{formatter.format(total / 100)}</Text></Column>
268 </Row>
269 </Section>
270 
271 <Button href={downloadUrl} style={styles.button}>Download PDF Invoice</Button>
272 </EmailLayout>
273 )
274}
275 
276export default InvoiceEmail
277 
278const styles = {
279 h1: { fontSize: "24px", fontWeight: "700", color: "#111827", margin: "0 0 16px" },
280 text: { fontSize: "15px", lineHeight: "1.6", color: "#374151", margin: "0 0 12px" },
281 metaBox: { backgroundColor: "#f9fafb", borderRadius: "8px", padding: "16px", margin: "16px 0" },
282 metaLabel: { fontSize: "12px", color: "#6b7280", fontWeight: "600", textTransform: "uppercase" as const, margin: "0 0 4px" },
283 metaValue: { fontSize: "14px", color: "#111827", margin: 0 },
284 metaValueLarge: { fontSize: "20px", fontWeight: "700", color: "#4f46e5", margin: 0 },
285 table: { width: "100%", margin: "16px 0" },
286 tableHeader: { backgroundColor: "#f3f4f6", borderRadius: "4px" },
287 tableHeaderText: { fontSize: "12px", fontWeight: "600", color: "#374151", padding: "8px 12px", textTransform: "uppercase" as const },
288 tableRowEven: { backgroundColor: "#ffffff" },
289 tableRowOdd: { backgroundColor: "#f9fafb" },
290 tableCell: { fontSize: "14px", color: "#374151", padding: "10px 12px" },
291 divider: { borderColor: "#e5e5e5", margin: "8px 0" },
292 totalLabel: { fontSize: "16px", fontWeight: "700", color: "#111827", padding: "8px 12px" },
293 totalValue: { fontSize: "16px", fontWeight: "700", color: "#111827", textAlign: "right" as const, padding: "8px 12px" },
294 button: { backgroundColor: "#4f46e5", color: "#fff", borderRadius: "6px", padding: "12px 24px", fontSize: "15px", fontWeight: "600", textDecoration: "none" },
295}
296```
297 
298---
299 
300## Unified Send Function
301 
302```typescript
303// emails/lib/send.ts
304import { Resend } from "resend"
305import { render } from "@react-email/render"
306import { WelcomeEmail } from "../templates/welcome"
307import { InvoiceEmail } from "../templates/invoice"
308import { addTrackingParams } from "./tracking"
309 
310const resend = new Resend(process.env.RESEND_API_KEY)
311 
312type EmailPayload =
313 | { type: "welcome"; props: Parameters<typeof WelcomeEmail>[0] }
314 | { type: "invoice"; props: Parameters<typeof InvoiceEmail>[0] }
315 
316export async function sendEmail(to: string, payload: EmailPayload) {
317 const templates = {
318 welcome: { component: WelcomeEmail, subject: "Welcome to MyApp — confirm your email" },
319 invoice: { component: InvoiceEmail, subject: `Invoice from MyApp` },
320 }
321 
322 const template = templates[payload.type]
323 const html = render(template.component(payload.props as any))
324 const trackedHtml = addTrackingParams(html, { campaign: payload.type })
325 
326 const result = await resend.emails.send({
327 from: "MyApp <[email protected]>",
328 to,
329 subject: template.subject,
330 html: trackedHtml,
331 tags: [{ name: "email-type", value: payload.type }],
332 })
333 
334 return result
335}
336```
337 
338---
339 
340## Preview Server Setup
341 
342```typescript
343// package.json scripts
344{
345 "scripts": {
346 "email:dev": "email dev --dir emails/templates --port 3001",
347 "email:build": "email export --dir emails/templates --outDir emails/out"
348 }
349}
350 
351// Run: npm run email:dev
352// Opens: http://localhost:3001
353// Shows all templates with live preview and hot reload
354```
355 
356---
357 
358## i18n Support
359 
360```typescript
361// emails/i18n/en.ts
362export const en = {
363 welcome: {
364 preview: (name: string) => `Welcome to MyApp, ${name}!`,
365 heading: (name: string) => `Welcome to MyApp, ${name}!`,
366 body: (days: number) => `You've got ${days} days to explore everything.`,
367 cta: "Confirm Email Address",
368 },
369}
370 
371// emails/i18n/de.ts
372export const de = {
373 welcome: {
374 preview: (name: string) => `Willkommen bei MyApp, ${name}!`,
375 heading: (name: string) => `Willkommen bei MyApp, ${name}!`,
376 body: (days: number) => `Du hast ${days} Tage Zeit, alles zu erkunden.`,
377 cta: "E-Mail-Adresse bestätigen",
378 },
379}
380 
381// Usage in template
382import { en, de } from "../i18n"
383const t = locale === "de" ? de : en
384```
385 
386---
387 
388## Spam Score Optimization Checklist
389 
390- [ ] Sender domain has SPF, DKIM, and DMARC records configured
391- [ ] From address uses your own domain (not gmail.com/hotmail.com)
392- [ ] Subject line under 50 characters, no ALL CAPS, no "FREE!!!"
393- [ ] Text-to-image ratio: at least 60% text
394- [ ] Plain text version included alongside HTML
395- [ ] Unsubscribe link in every marketing email (CAN-SPAM, GDPR)
396- [ ] No URL shorteners — use full branded links
397- [ ] No red-flag words: "guarantee", "no risk", "limited time offer" in subject
398- [ ] Single CTA per email — no 5 different buttons
399- [ ] Image alt text on every image
400- [ ] HTML validates — no broken tags
401- [ ] Test with Mail-Tester.com before first send (target: 9+/10)
402 
403---
404 
405## Analytics Tracking
406 
407```typescript
408// emails/lib/tracking.ts
409interface TrackingParams {
410 campaign: string
411 medium?: string
412 source?: string
413}
414 
415export function addTrackingParams(html: string, params: TrackingParams): string {
416 const utmString = new URLSearchParams({
417 utm_source: params.source ?? "email",
418 utm_medium: params.medium ?? "transactional",
419 utm_campaign: params.campaign,
420 }).toString()
421 
422 // Add UTM params to all links in the email
423 return html.replace(/href="(https?:\/\/[^"]+)"/g, (match, url) => {
424 const separator = url.includes("?") ? "&" : "?"
425 return `href="${url}${separator}${utmString}"`
426 })
427}
428```
429 
430---
431 
432## Common Pitfalls
433 
434- **Inline styles required** — most email clients strip `<head>` styles; React Email handles this
435- **Max width 600px** — anything wider breaks on Gmail mobile
436- **No flexbox/grid** — use `<Row>` and `<Column>` from react-email, not CSS grid
437- **Dark mode media queries** — must use `!important` to override inline styles
438- **Missing plain text** — all major providers have a plain text field; always populate it
439- **Transactional vs marketing** — use separate sending domains/IPs to protect deliverability
440 

Discussion