- Add CSS components: .page-meta badges, .domain-header, .install-banner - Fix invisible tab navigation (explicit color for light/dark modes) - Rewrite generate-docs.py with design system templates - Domain indexes: centered headers with icons, install banners, grid cards - Skill pages: pill badges (domain, skill ID, source), install commands - Agent/command pages: type badges with domain icons - Regenerate all 210 pages (180 skills + 15 agents + 15 commands) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
451 lines
15 KiB
Markdown
451 lines
15 KiB
Markdown
---
|
|
title: "Email Template Builder"
|
|
description: "Email Template Builder - Claude Code skill from the Engineering - Core domain."
|
|
---
|
|
|
|
# Email Template Builder
|
|
|
|
<div class="page-meta" markdown>
|
|
<span class="meta-badge">:material-code-braces: Engineering - Core</span>
|
|
<span class="meta-badge">:material-identifier: `email-template-builder`</span>
|
|
<span class="meta-badge">:material-github: <a href="https://github.com/alirezarezvani/claude-skills/tree/main/engineering-team/email-template-builder/SKILL.md">Source</a></span>
|
|
</div>
|
|
|
|
<div class="install-banner" markdown>
|
|
<span class="install-label">Install:</span> <code>claude /plugin install engineering-skills</code>
|
|
</div>
|
|
|
|
|
|
**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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```typescript
|
|
// 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 <hello@yourapp.com>",
|
|
to,
|
|
subject: template.subject,
|
|
html: trackedHtml,
|
|
tags: [{ name: "email-type", value: payload.type }],
|
|
})
|
|
|
|
return result
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Preview Server Setup
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|