Developing Personalized Email Campaigns
Personalization goes far beyond {{firstName}} — it involves different content blocks for segments, product recommendations based on purchase history, A/B subject line variants, and content adaptation by language and timezone. All of this requires backend code and data from CRM/analytics.
Personalization Levels
Level 1 — Basic fields: Name, company, last visit date. Simple substitution through a templating engine.
Level 2 — Segmentation: Different content blocks for different user groups.
Level 3 — Behavioral data: Recommendations based on purchase/view history, personalized discounts on abandoned products.
Level 4 — Predictive personalization: ML models to predict optimal send time and content.
Building a Personalized Email
interface PersonalizationContext {
user: User;
segment: 'new' | 'active' | 'at_risk' | 'churned';
recommendedProducts: Product[];
lastViewedCategory: string;
totalOrders: number;
preferredLanguage: 'ru' | 'en';
discount?: { code: string; percent: number; validUntil: Date };
}
async function buildPersonalizedEmail(
userId: string,
campaignId: string
): Promise<{ subject: string; html: string }> {
// Gather context from different sources in parallel
const [user, orders, recentViews, discount] = await Promise.all([
db.users.findById(userId),
db.orders.getRecentByUser(userId, 5),
db.productViews.getRecentByUser(userId, 20),
db.discounts.getPersonalDiscount(userId),
]);
const segment = classifySegment(user, orders);
const recommended = await recommendationEngine.getProducts(userId, recentViews);
const ctx: PersonalizationContext = {
user,
segment,
recommendedProducts: recommended.slice(0, 3),
lastViewedCategory: recentViews[0]?.categoryName ?? '',
totalOrders: orders.length,
preferredLanguage: user.language ?? 'ru',
discount: discount ?? undefined,
};
// Choose email subject based on segment
const subjects: Record<PersonalizationContext['segment'], string> = {
new: `${user.name}, here's what will help you get started`,
active: `${user.name}, new items in "${ctx.lastViewedCategory}" just for you`,
at_risk: `We miss you, ${user.name}! Special offer inside`,
churned: `${user.name}, come back — ${discount?.percent ?? 20}% discount awaits you`,
};
const html = render(<PersonalizedCampaign ctx={ctx} campaignId={campaignId} />);
return { subject: subjects[segment], html };
}
User Segmentation
function classifySegment(user: User, orders: Order[]): PersonalizationContext['segment'] {
const daysSinceRegistration = daysBetween(user.createdAt, new Date());
const daysSinceLastOrder = orders.length > 0
? daysBetween(orders[0].createdAt, new Date())
: Infinity;
if (daysSinceRegistration < 7) return 'new';
if (daysSinceLastOrder < 30) return 'active';
if (daysSinceLastOrder < 90) return 'at_risk';
return 'churned';
}
React Email Component with Conditional Content
function PersonalizedCampaign({ ctx, campaignId }) {
const { user, segment, recommendedProducts, discount } = ctx;
return (
<Html>
<Preview>
{segment === 'churned'
? `${discount?.percent}% discount — just for you`
: `New items specially curated for ${user.name}`}
</Preview>
<Body>
{/* Hero depends on segment */}
{segment === 'at_risk' || segment === 'churned' ? (
<ReEngagementHero discount={discount} userName={user.name} />
) : (
<StandardHero userName={user.name} />
)}
{/* Personal recommendations */}
{recommendedProducts.length > 0 && (
<Section>
<Heading>Recommended for you</Heading>
<Row>
{recommendedProducts.map(product => (
<Column key={product.id}>
<ProductCard
product={product}
utm={`utm_campaign=${campaignId}&utm_content=rec-${product.id}`}
discount={discount}
/>
</Column>
))}
</Row>
</Section>
)}
{/* Personal promo code — only for at_risk and churned */}
{discount && (segment === 'at_risk' || segment === 'churned') && (
<Section style={{ background: '#fef3c7', padding: 24, borderRadius: 8 }}>
<Text>Your personal promo code:</Text>
<Text style={{ fontSize: 28, fontWeight: 800, letterSpacing: 4 }}>
{discount.code}
</Text>
<Text style={{ color: '#92400e' }}>
{discount.percent}% discount until {formatDate(discount.validUntil)}
</Text>
</Section>
)}
<Footer unsubscribeUrl={generateUnsubscribeUrl(user.id)} />
</Body>
</Html>
);
}
Optimal Send Time
// Analyze open history to determine best time
async function getOptimalSendTime(userId: string): Promise<Date> {
const openHistory = await db.emailOpenEvents.getByUser(userId, 90); // 90 days
if (openHistory.length < 5) {
// Not enough data — use default 10:00 by timezone
return getNextOccurrenceOfHour(10, userTimezone);
}
// Find the hour with the most opens
const hourCounts = openHistory.reduce((acc, event) => {
const hour = new Date(event.openedAt).getHours();
acc[hour] = (acc[hour] ?? 0) + 1;
return acc;
}, {} as Record<number, number>);
const bestHour = Number(
Object.entries(hourCounts).sort(([, a], [, b]) => b - a)[0][0]
);
return getNextOccurrenceOfHour(bestHour, userTimezone);
}
Timeline
A personalized campaign with segmentation, recommendations, and conditional content takes 1 week. With ML model for optimal send time — another 3–5 days.







