Template Design con React Email y Resend: El Sistema de Capas que Previene el Caos en Producción
Aprende a construir templates de email mantenibles con React Email y Resend. Sistema de componentes jerárquicos para brand consistency.
El 90% de los Sistemas de Plantillas de Email Se Convierten en Código Legacy en 6 Meses
Vuestra start-up lanza el MVP. El email de bienvenida funciona. También el de reset password. Y el de confirmación de pedido. Todo parece under control.
Seis meses después: 14 tipos de notificación diferentes. Cada una con sus estilos inline inconsistentes. Un archivo HTML de 800 líneas que nadie sabe cómo modificar sin romper algo. Cambios de marca que requieren actualizar 8 templates manualmente.
*El problema real no es escribir email templates. Es diseñar un sistema que escale sin convertirse en deuda técnica.*
La mayoría de desarrolladores tratan los emails como second-class citizens. Usan strings concatenadas, estilos inline caóticos, y componentes reutilizables que no se reutilizan en absoluto. El resultado: un sistema que funciona, pero que no se mantiene.
Voy a mostraros cómo construir un sistema de templates mantenible con React Email y Resend. Paso a paso. Con código real. Sin atajos.
Por Qué Vuestros Emails Son un Caos de Estilos Inline
Apple Mail, Outlook 365, Gmail en Android. Cada cliente interpreta el HTML del email de forma diferente.
Gmail ignora la mayoría de selectores CSS. Outlook usa motores de renderizado que existían en 2007. Apple Mail soporta cosas que otros browsers llevan años implementando.
La mayoría de developers responden a esta realidad con la estrategia del mínimo común denominador: estilos inline en cada elemento, tablas para el layout, nada de CSS moderno. Funciona, pero el código resultante es inmantenible.
Ejemplo del caos que veis típicamente:
```jsx
// Este código funciona. Es un infierno mantenerlo.
const WelcomeEmail = () => {
return (
<html>
<body style={{ backgroundColor: '#f5f5f5' }}>
<table style={{ width: '100%', maxWidth: '600px', margin: '0 auto' }}>
<tr>
<td style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<h1 style={{ fontSize: '24px', color: '#333' }}>Bienvenido</h1>
<p style={{ fontSize: '16px', lineHeight: '1.5', color: '#666' }}>
Gracias por registrarte en nuestra plataforma.
</p>
</td>
</tr>
</table>
</body>
</html>
);
};
```
Repetís este patrón 14 veces. Cada template con sus propios valores hardcodeados. Colores que no coinciden. Fonts que cambian entre emails. Cuando el brand team pide actualizar el color primario, tenéis un problema.
El Framework de Componentes Jerárquicos para Email Templates
La solución no es escribir mejor HTML inline. Es crear una arquitectura de componentes que enforce consistencia a nivel de sistema.
Os presento el Sistema de Capas para Email Templates. Tres niveles que se construyen uno sobre otro:
Capa 1: Tokens de Diseño Centralizados
Primero, definís los valores que se repiten: colores, spacing, tipografía. No en cada componente. En un lugar único.
```jsx
// design-tokens.ts
export const tokens = {
colors: {
primary: '#2563eb',
primaryDark: '#1d4ed8',
secondary: '#64748b',
background: '#ffffff',
surface: '#f8fafc',
text: {
primary: '#0f172a',
secondary: '#475569',
inverse: '#ffffff',
},
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
section: '48px',
},
typography: {
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
heading: {
xl: { fontSize: '28px', lineHeight: '1.2', fontWeight: 700 },
lg: { fontSize: '22px', lineHeight: '1.3', fontWeight: 600 },
md: { fontSize: '18px', lineHeight: '1.4', fontWeight: 600 },
},
body: {
lg: { fontSize: '16px', lineHeight: '1.6' },
md: { fontSize: '14px', lineHeight: '1.5' },
},
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '12px',
},
};
```
Estos tokens son la única fuente de verdad. Cambiáis el color primario aquí, se actualiza en todos los emails.
Capa 2: Componentes Base Email-Ready
Segundo, creáis componentes que ya incorporan las limitaciones de email clients. No escribís `<div>`, escribís componentes que saben renderizarse correctamente en Outlook y Gmail.
```jsx
// components/base/index.tsx
import { tokens } from '../design-tokens';
import { Column, Row, Section, Spacer } from '@react-email/components';
interface TextProps {
children: React.ReactNode;
variant?: 'heading-xl' | 'heading-lg' | 'heading-md' | 'body-lg' | 'body-md';
color?: 'primary' | 'secondary' | 'inverse';
}
export const Text = ({ children, variant = 'body-lg', color = 'primary' }: TextProps) => {
const style = {
...tokens.typography[variant.includes('heading') ? 'heading' : 'body'][variant.replace('heading-', '').replace('body-', '')],
color: tokens.colors.text[color],
fontFamily: tokens.typography.fontFamily,
margin: '0 0 16px 0',
};
return <p style={style}>{children}</p>;
};
interface ButtonProps {
children: React.ReactNode;
href: string;
variant?: 'primary' | 'secondary';
}
export const Button = ({ children, href, variant = 'primary' }: ButtonProps) => {
const isPrimary = variant === 'primary';
const bgColor = isPrimary ? tokens.colors.primary : 'transparent';
const textColor = isPrimary ? tokens.colors.text.inverse : tokens.colors.primary;
const border = isPrimary ? 'none' : `2px solid ${tokens.colors.primary}`;
return (
<a
href={href}
style={{
display: 'inline-block',
padding: '12px 24px',
backgroundColor: bgColor,
border,
borderRadius: tokens.borderRadius.md,
color: textColor,
fontFamily: tokens.typography.fontFamily,
fontSize: '16px',
fontWeight: 600,
textDecoration: 'none',
}}
>
{children}
</a>
);
};
interface CardProps {
children: React.ReactNode;
}
export const Card = ({ children }: CardProps) => (
<table cellPadding="0" cellSpacing="0" style={{ width: '100%' }}>
<tr>
<td
style={{
backgroundColor: tokens.colors.surface,
borderRadius: tokens.borderRadius.lg,
padding: tokens.spacing.lg,
}}
>
{children}
</td>
</tr>
</table>
);
```
Estos componentes ya saben cómo comportarse en email clients. El `Card` usa `<table>` internamente porque es lo que funciona en Outlook. El `Button` usa `<a>` con estilos inline porque los `<button>` no se estilizan consistentemente.
Capa 3: Templates Específicos por Tipo de Notificación
Tercero, construís los templates concretos. Éstos compilan los tokens y componentes base en emails reales.
```jsx
// templates/welcome.tsx
import { Html } from '@react-email/html';
import { Head } from '@react-email/head';
import { Body } from '@react-email/body';
import { Container } from '@react-email/components';
import { tokens } from '../design-tokens';
import { Text, Button, Card, Spacer } from '../components/base';
interface WelcomeTemplateProps {
userName: string;
confirmUrl: string;
}
export const WelcomeTemplate = ({ userName, confirmUrl }: WelcomeTemplateProps) => {
return (
<Html>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<Body style={{ backgroundColor: tokens.colors.surface, margin: 0, padding: 0 }}>
<Container style={{ maxWidth: '600px', margin: '0 auto', padding: `${tokens.spacing.xl} ${tokens.spacing.md}` }}>
{/ Header /}
<table cellPadding="0" cellSpacing="0" style={{ width: '100%', marginBottom: tokens.spacing.lg }}>
<tr>
<td style={{ textAlign: 'center' }}>
<img
src="https://vuestra-app.com/logo.png"
alt="Logo"
width="120"
style={{ border: 0 }}
/>
</td>
</tr>
</table>
{/ Main Content /}
<Card>
<Text variant="heading-lg">¡Bienvenido, {userName}!</Text>
<Text variant="body-lg" color="secondary">
Gracias por unirte a nuestra plataforma. Tu cuenta está lista, pero antes de empezar necesitamos verificar tu email.
</Text>
<Spacer height="md" />
<div style={{ textAlign: 'center' }}>
<Button href={confirmUrl}>Confirmar mi email</Button>
</div>
</Card>
<Spacer height="lg" />
<Text variant="body-md" color="secondary" style={{ textAlign: 'center' }}>
Si no solicitaste esta cuenta, ignora este email.
</Text>
</Container>
</Body>
</Html>
);
};
```
Este template sabe cómo renderizarse correctamente. Cada componente heredado de las capas anteriores enforce el diseño system. Cuando vuestro brand team cambie el color primario, solo tocáis `design-tokens.ts`.
Envío Real con Resend: El Paso que Falta
Construir el template es la mitad del trabajo. La otra mitad es enviarlo correctamente.
```bash
npm install resend @react-email/components
```
Configuráis el cliente de Resend:
```typescript
// lib/resend.ts
import { Resend } from 'resend';
export const resend = new Resend(process.env.RESEND_API_KEY);
```
Definís una función helper que simplifica el envío:
```typescript
// lib/email.ts
import React from 'react';
import { render } from '@react-email/components';
import { resend } from './resend';
import { WelcomeTemplate } from '../templates/welcome';
interface SendEmailOptions {
to: string;
template: React.ReactElement;
subject: string;
from?: string;
}
export async function sendEmail({ to, template, subject, from = 'noreply@vuestra-app.com' }: SendEmailOptions) {
const html = await render(template, {
pretty: true,
});
const { data, error } = await resend.emails.send({
from,
to,
subject,
html,
});
if (error) {
console.error('Error enviando email:', error);
throw new Error(`Failed to send email: ${error.message}`);
}
return data;
}
```
Uso real en una API route:
```typescript
// app/api/auth/welcome/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { sendEmail } from '@/lib/email';
import { WelcomeTemplate } from '@/templates/welcome';
export async function POST(req: NextRequest) {
const { email, userName } = await req.json();
await sendEmail({
to: email,
subject: 'Bienvenido a la plataforma',
template: <WelcomeTemplate userName={userName} confirmUrl={`https://vuestra-app.com/confirm?token=abc123`} />,
});
return NextResponse.json({ success: true });
}
```
Este patrón escala. Cada tipo de notificación tiene su template. Cada template usa los tokens centralizados. Cambiáis el brand una vez, se actualiza en todos los emails.
La Partición de Notificaciones: El Error que Crea 200 Líneas de Código Muerto
La mayoría de sistemas de email tienen un solo archivo `sendEmail.ts` con 50 funciones diferentes. Una para cada notificación. Mezcladas. Sin organización.
Esto es un antipatrón.
A medida que vuestra app crece, termináis con `sendWelcomeEmail`, `sendPasswordResetEmail`, `sendOrderConfirmationEmail`, `sendInvoiceEmail`, `sendTrialEndingEmail`, `sendTrialEndedEmail`. Todas en el mismo archivo. Dependencias compartidas que no tienen sentido. Lógica de negocio mezclada con lógica de email.
La solución: particionar por dominio de negocio, no por tipo de email.
```typescript
// emails/auth/welcome.ts
export async function sendWelcomeEmail(user: User) {
return sendEmail({
to: user.email,
subject: '¡Bienvenido a la plataforma!',
template: <WelcomeTemplate userName={user.name} confirmUrl={generateConfirmUrl(user.id)} />,
});
}
// emails/auth/password-reset.ts
export async function sendPasswordResetEmail(user: User) {
return sendEmail({
to: user.email,
subject: 'Restablecer contraseña',
template: <PasswordResetTemplate resetUrl={generateResetUrl(user.id)} />,
});
}
// emails/orders/order-confirmation.ts
export async function sendOrderConfirmation(order: Order) {
return sendEmail({
to: order.customerEmail,
subject: `Confirmación de pedido #${order.id}`,
template: <OrderConfirmationTemplate order={order} />,
});
}
```
Cada dominio tiene su carpeta. Cada template está donde esperáis encontrarlo. Cuando añadís una nueva notificación, sabéis exactamente dónde va.
Preview y Testing: El Workflow que Previene Emails Rotos en Producción
No mandáis código a producción sin tests. ¿Por qué mandáis emails sin verificarlos antes?
React Email incluye un servidor de preview que funciona durante desarrollo:
```bash
npx react-email dev
```
Este comando levanta una interfaz web donde veis cada template renderizado. Cambiáis un color en `design-tokens.ts`, recargáis, y verificáis que todo sigue funcionando.
Para testing automatizado, usáis `@react-email/testing`:
```typescript
// __tests__/templates/welcome.spec.tsx
import { render } from '@react-email/react-email-testing';
import { WelcomeTemplate } from '../../templates/welcome';
describe('WelcomeTemplate', () => {
it('renders user name correctly', () => {
const { getByText } = render(
<WelcomeTemplate
userName="María García"
confirmUrl="https://example.com/confirm"
/>
);
expect(getByText('¡Bienvenido, María García!')).toBeTruthy();
});
it('contains confirm button with correct href', () => {
const { getByText } = render(
<WelcomeTemplate
userName="María García"
confirmUrl="https://example.com/confirm?token=xyz"
/>
);
const button = getByText('Confirmar mi email');
expect(button).toHaveAttribute('href', 'https://example.com/confirm?token=xyz');
});
});
```
Este test verifica que el contenido es correcto y los links funcionan. No es un test de renderizado visual (eso lo hacéis en el servidor de preview), pero captura regresiones de contenido.
Resumen: El Sistema Completo
Un sistema de email templates mantenible tiene tres ingredientes:
Tokens centralizados: Un archivo con colores, spacing, tipografía. Cambiáis una vez, se actualiza en todos los emails.
Componentes base email-ready: Abstracciones que saben renderizarse en clientesproblemáticos como Outlook.
Templates por dominio: Organización clara que escala cuando añadís nuevas notificaciones.
La próxima vez que alguien os pida cambiar el color primario de todos los emails, no vais a temblar. Abrís `design-tokens.ts`, cambiáis un valor, y el deploy se encarga del resto.
*El mejor sistema de email templates es el que no notáis. El que os permite añadir una nueva notificación en 15 minutos sin pensar en estilos inline ni compatibilidad con clientes.*
Empezad con los tokens. Construid componentes base. Después los templates. En ese orden. Sin atajos.
Vuestra bandeja de entrada del brand team os lo agradecerá.
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

