From atum-stack-web
HTML email template authoring pattern library for web projects — React Email (component-based JSX templates, preview dev server, integration with Resend/Postmark/SendGrid), MJML (markup language that compiles to cross-client HTML tables), Handlebars + juice (inline CSS postprocessing), email-specific HTML quirks (tables for layout, inline CSS only, Outlook conditional comments, Gmail 102 KB clipping, dark mode CSS variables, CSS custom properties), responsive email patterns (media queries vs fluid layouts), preheader text, alt text for images, testing across clients (Litmus, Email on Acid, Mailpit). Use when designing or maintaining HTML email templates: authoring welcome emails, password resets, order confirmations, receipts, newsletters, or any branded communication. Differentiates from email-transactional (backend sending logic) by focusing on the template authoring and cross-client rendering. Complements ghost-expert (Ghost newsletters), sanity-expert (Sanity-managed email content), and wordpress-expert (WooCommerce email templates).
npx claudepluginhub arnwaldn/atum-plugins-collection --plugin atum-stack-webThis skill uses the workspace's default tool permissions.
Les emails HTML sont **le pire** format web à maintenir : les clients (Outlook, Gmail, Apple Mail, Yahoo) supportent un sous-ensemble différent de CSS et HTML. Ces patterns couvrent les solutions modernes pour s'en sortir.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides implementation of event-driven hooks in Claude Code plugins using prompt-based validation and bash commands for PreToolUse, Stop, and session events.
Les emails HTML sont le pire format web à maintenir : les clients (Outlook, Gmail, Apple Mail, Yahoo) supportent un sous-ensemble différent de CSS et HTML. Ces patterns couvrent les solutions modernes pour s'en sortir.
Projet React / Next.js ?
└── React Email (recommandé 2025) — DX imbattable, preview local
Projet non-React (Python, PHP, Go) ?
├── MJML (compile vers HTML cross-client)
└── Handlebars + juice (inline CSS)
Besoin du meilleur cross-client sans overhead ?
└── MJML
Besoin de templates managés par le client ?
└── SendGrid Dynamic Templates ou Postmark Templates (UI dans le provider)
npm install @react-email/components @react-email/render
npm install -D react-email
my-project/
├── emails/
│ ├── WelcomeEmail.tsx
│ ├── PasswordReset.tsx
│ ├── OrderConfirmation.tsx
│ └── _components/
│ ├── EmailLayout.tsx
│ └── EmailButton.tsx
└── package.json
// emails/_components/EmailLayout.tsx
import {
Body,
Container,
Font,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from '@react-email/components'
export function EmailLayout({
preview,
children,
}: {
preview: string
children: React.ReactNode
}) {
return (
<Html>
<Head>
<Font
fontFamily="Inter"
fallbackFontFamily="Arial"
webFont={{
url: 'https://fonts.gstatic.com/s/inter/v13/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2',
format: 'woff2',
}}
fontWeight={400}
fontStyle="normal"
/>
</Head>
<Preview>{preview}</Preview>
<Tailwind>
<Body className="bg-gray-100 py-10 font-sans">
<Container className="mx-auto max-w-xl rounded bg-white p-8">
<Section className="mb-6">
<Img
src="https://example.com/logo.png"
width="120"
height="40"
alt="MyApp"
/>
</Section>
{children}
</Container>
</Body>
</Tailwind>
</Html>
)
}
// emails/OrderConfirmation.tsx
import { Heading, Text, Button, Section } from '@react-email/components'
import { EmailLayout } from './_components/EmailLayout'
type Props = {
customerName: string
orderId: string
total: string
items: { name: string; quantity: number; price: string }[]
}
export function OrderConfirmation({ customerName, orderId, total, items }: Props) {
return (
<EmailLayout preview={`Order ${orderId} confirmed`}>
<Heading className="text-2xl font-bold">Thanks for your order, {customerName}!</Heading>
<Text>Your order #{orderId} has been confirmed.</Text>
<Section className="my-6">
{items.map((item, i) => (
<div key={i} className="flex justify-between border-b py-2">
<Text className="m-0">
{item.quantity}x {item.name}
</Text>
<Text className="m-0 font-semibold">{item.price}</Text>
</div>
))}
<div className="flex justify-between py-2 text-lg font-bold">
<Text className="m-0">Total</Text>
<Text className="m-0">{total}</Text>
</div>
</Section>
<Button
href={`https://example.com/orders/${orderId}`}
className="rounded bg-black px-6 py-3 text-white"
>
View order
</Button>
</EmailLayout>
)
}
// Default export pour react-email dev server
export default OrderConfirmation
npx react-email dev
# Ouvre http://localhost:3000 avec hot reload
Le dev server liste tous les templates dans emails/, affiche le rendu, permet de tester avec des props mockées.
import { render } from '@react-email/render'
import { OrderConfirmation } from '@/emails/OrderConfirmation'
const html = render(<OrderConfirmation customerName="Arnaud" orderId="123" total="€59.99" items={[...]} />)
// Envoyer via Resend / Postmark / SES
await resend.emails.send({
from: 'orders@example.com',
to: 'user@example.com',
subject: `Order ${orderId} confirmed`,
html,
})
npm install mjml
<!-- emails/welcome.mjml -->
<mjml>
<mj-head>
<mj-title>Welcome to MyApp</mj-title>
<mj-preview>Thanks for signing up!</mj-preview>
<mj-attributes>
<mj-all font-family="Inter, Arial, sans-serif" />
<mj-text font-size="16px" line-height="1.5" color="#333" />
</mj-attributes>
</mj-head>
<mj-body background-color="#f6f6f6">
<mj-section background-color="#ffffff" padding="40px">
<mj-column>
<mj-image src="https://example.com/logo.png" width="120px" alt="MyApp" />
<mj-text font-size="24px" font-weight="bold">Welcome, {{firstName}}!</mj-text>
<mj-text>Thanks for signing up. Get started by exploring your dashboard.</mj-text>
<mj-button href="https://example.com/dashboard" background-color="#000" color="#fff">
Go to dashboard
</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>
import mjml2html from 'mjml'
import Handlebars from 'handlebars'
import fs from 'node:fs/promises'
// Compile au build ou au runtime
const mjmlSource = await fs.readFile('emails/welcome.mjml', 'utf-8')
const { html, errors } = mjml2html(mjmlSource)
// Then Handlebars pour les variables
const template = Handlebars.compile(html)
const rendered = template({ firstName: 'Arnaud' })
// Send via Postmark / SES
npm install handlebars juice
<!-- emails/welcome.hbs -->
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; background: #f6f6f6; margin: 0; padding: 40px; }
.container { max-width: 600px; margin: 0 auto; background: #fff; padding: 40px; border-radius: 4px; }
h1 { font-size: 24px; font-weight: bold; }
.btn { display: inline-block; background: #000; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<h1>Welcome, {{firstName}}!</h1>
<p>Thanks for signing up.</p>
<a href="https://example.com/dashboard" class="btn">Go to dashboard</a>
</div>
</body>
</html>
import Handlebars from 'handlebars'
import juice from 'juice'
import fs from 'node:fs/promises'
const source = await fs.readFile('emails/welcome.hbs', 'utf-8')
const template = Handlebars.compile(source)
const html = template({ firstName: 'Arnaud' })
// Inline CSS pour les clients qui ignorent <style>
const inlined = juice(html)
<!-- Outlook ne gère pas flex/grid -->
<table cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td width="50%">Column 1</td>
<td width="50%">Column 2</td>
</tr>
</table>
<!-- BON (Outlook OK) -->
<p style="font-size: 16px; color: #333;">Text</p>
<!-- MAUVAIS (Gmail app supprime les classes) -->
<p class="text">Text</p>
<!--[if mso]>
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0">
<tr><td>
<![endif]-->
<!-- Contenu moderne -->
<div style="max-width: 600px;">...</div>
<!--[if mso]>
</td></tr>
</table>
<![endif]-->
Gmail tronque les emails > 102 KB. Si ton email fait 110 KB, les utilisateurs voient "Message clipped [View entire message]".
→ Optimiser : compress le HTML, retirer les commentaires, éviter les CSS inutilisés.
<style>
@media (prefers-color-scheme: dark) {
body, .container { background: #1a1a1a !important; color: #fff !important; }
}
</style>
<!-- Meta pour iOS -->
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<img src="https://example.com/hero.jpg" alt="Order #123 confirmed" width="600" height="300" style="display: block;" />
Règle : toujours mettre alt + width + height + display: block (sinon Gmail ajoute un espace).
Les 50-100 premiers chars sont affichés en preview dans l'inbox. Utiliser <Preview> en React Email ou du texte caché :
<div style="display: none; font-size: 1px; color: #f6f6f6;">
Thanks for your order #123 — see details inside
</div>
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="https://example.com" style="height:40px;v-text-anchor:middle;width:200px;" arcsize="10%" strokecolor="#000" fillcolor="#000">
<w:anchorlock/>
<center style="color:#ffffff;font-family:sans-serif;font-size:16px;">Click me</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-- -->
<a href="https://example.com" style="background: #000; color: #fff; padding: 12px 24px; border-radius: 4px; text-decoration: none;">Click me</a>
<!--<![endif]-->
React Email et MJML génèrent ce pattern automatiquement.
@font-face web fonts→ Toujours déclarer un fallback solide : Inter, -apple-system, Arial, sans-serif.
<!-- Outlook VML fallback pour background -->
<td background="hero.jpg" bgcolor="#000">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:600px;height:300px;">
<v:fill type="frame" src="hero.jpg" color="#000" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>Content on top of background</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
# Mailpit — mailserver local + web UI
docker run -d -p 8025:8025 -p 1025:1025 axllent/mailpit
# Config SMTP dans ton backend
SMTP_HOST=localhost SMTP_PORT=1025
Tous les emails envoyés en dev arrivent dans http://localhost:8025.
<style>
@media (max-width: 600px) {
.container { width: 100% !important; }
.column { display: block !important; width: 100% !important; }
}
</style>
<table width="100%" style="max-width: 600px;">
<!-- Contenu -->
</table>
Règle : utiliser fluid layout comme base, media queries comme enhancement.
<style> non-inliné → ignoré par la moitié des clientsalt sur les images → hideux quand les images sont désactivées<button> → Outlook les ignore, utiliser <a> styléemail-transactional (dans atum-stack-backend)ghost-expert (dans atum-cms-ecom)shopify-expert (dans atum-cms-ecom) + Shopify Notificationswordpress-expert + skill woocommerce-patterns (dans atum-cms-ecom)