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
- Run the line below. It pulls the whole folder into
~/.claude/skills/email-template-builder, including the files SKILL.md points to. - Describe your job in plain words. Claude Code follows the skill from there.
npx degit alirezarezvani/claude-skills/engineering-team/skills/email-template-builder#main ~/.claude/skills/email-template-builderFor 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)
- On this page open ⋯ → Download .md.
- Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
- Pick the file and Save. Claude shows the name and description and runs a security scan.
- Check the skill is switched on.
- Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
- ChatGPT: make a Project and paste it into Instructions.
- 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.
Paste into Claude, ChatGPT or Cursor.
Source of Email template builder
Show the full text440 lines
| name | description |
|---|---|
| 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. 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
!importantto 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 | |
| 2 | name "email-template-builder" |
| 3 | description "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 | |
| 16 | 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. |
| 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 | |
| 46 | emails/ |
| 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 | |
| 80 | // emails/components/layout/email-layout.tsx |
| 81 | import { |
| 82 | Body, Container, Head, Html, Img, Preview, Section, Text, Hr, Font |
| 83 | } from "@react-email/components" |
| 84 | |
| 85 | interface EmailLayoutProps { |
| 86 | preview: string |
| 87 | children: React.ReactNode |
| 88 | } |
| 89 | |
| 90 | export 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 | |
| 143 | const 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 | |
| 160 | // emails/templates/welcome.tsx |
| 161 | import { Button, Heading, Text } from "@react-email/components" |
| 162 | import { EmailLayout } from "../components/layout/email-layout" |
| 163 | |
| 164 | interface WelcomeEmailProps { |
| 165 | name: "string" |
| 166 | confirmUrl: string |
| 167 | trialDays?: number |
| 168 | } |
| 169 | |
| 170 | export 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 | |
| 200 | export default WelcomeEmail |
| 201 | |
| 202 | const 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 | |
| 217 | // emails/templates/invoice.tsx |
| 218 | import { Row, Column, Section, Heading, Text, Hr, Button } from "@react-email/components" |
| 219 | import { EmailLayout } from "../components/layout/email-layout" |
| 220 | |
| 221 | interface InvoiceItem { description: string; amount: number } |
| 222 | |
| 223 | interface 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 | |
| 234 | export 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 | |
| 276 | export default InvoiceEmail |
| 277 | |
| 278 | const 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 | |
| 303 | // emails/lib/send.ts |
| 304 | import { Resend } from "resend" |
| 305 | import { render } from "@react-email/render" |
| 306 | import { WelcomeEmail } from "../templates/welcome" |
| 307 | import { InvoiceEmail } from "../templates/invoice" |
| 308 | import { addTrackingParams } from "./tracking" |
| 309 | |
| 310 | const resend = new Resend(process.env.RESEND_API_KEY) |
| 311 | |
| 312 | type EmailPayload = |
| 313 | | { type: "welcome"; props: Parameters<typeof WelcomeEmail>[0] } |
| 314 | | { type: "invoice"; props: Parameters<typeof InvoiceEmail>[0] } |
| 315 | |
| 316 | export 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 | |
| 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 | |
| 361 | // emails/i18n/en.ts |
| 362 | export 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 |
| 372 | export 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 |
| 382 | import { en, de } from "../i18n" |
| 383 | const 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 | |
| 408 | // emails/lib/tracking.ts |
| 409 | interface TrackingParams { |
| 410 | campaign: string |
| 411 | medium?: string |
| 412 | source?: string |
| 413 | } |
| 414 | |
| 415 | export 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
Browse more free Claude skills.