| name | email-template-builder |
| 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. |
| compatibility | opencode |
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
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=
/>
{/* Dark mode styles */}
{preview}
{/* Header */}
{/* Content */}
{children}
{/* Footer */}
MyApp Inc. · 123 Main St · San Francisco, CA 94105
Unsubscribe
{" · "}
Privacy Policy
)
}
styles = {
: { : , : },
: { : , : , : , : , : },
: { : , : },
: { : },
: { : , : },
: { : },
: { : , : , : , : },
: { : , : },
}
Welcome Email
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>
< =>
First, confirm your email address to activate your account:
Confirm Email Address
Button not working? Copy and paste this link into your browser:
{confirmUrl}
Once confirmed, you can:
Connect your first project in 2 minutes
Invite your team (free for up to 3 members)
Set up Slack notifications
)
}
styles = {
: { : , : , : , : },
: { : , : , : , : },
: { : , : , : , : , : , : , : , : , : },
: { : , : },
: { : },
: { : , : , : , : },
}
Invoice Email
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 })
(
)
}
styles = {
: { : , : , : , : },
: { : , : , : , : },
: { : , : , : , : },
: { : , : , : , : , : },
: { : , : , : },
: { : , : , : , : },
: { : , : },
: { : , : },
: { : , : , : , : , : },
: { : },
: { : },
: { : , : , : },
: { : , : },
: { : , : , : , : },
: { : , : , : , : , : },
: { : , : , : , : , : , : , : },
}
Unified Send Function
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, : },
: { : , : },
}
template = templates[payload.]
html = (template.(payload. ))
trackedHtml = (html, { : payload. })
result = resend..({
: ,
to,
: template.,
: trackedHtml,
: [{ : , : payload. }],
})
result
}
Preview Server Setup
{
"scripts": {
"email:dev": "email dev --dir emails/templates --port 3001",
"email:build": "email export --dir emails/templates --outDir emails/out"
}
}
i18n Support
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",
},
}
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",
},
}
import { en, de } from "../i18n"
const t = locale === "de" ? de : en
Spam Score Optimization Checklist
Analytics Tracking
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()
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