Plain text emails from Node.js get the job done, but they look amateurish and are more likely to land in spam. HTML emails with branding, clear CTAs, and a consistent template look professional and build trust with your users. In this final lesson you will build a reusable HTML email template function that generates branded emails for every email type in the MERN Blog, and replace the Gmail SMTP transporter with a production email service (Resend or SendGrid) that provides deliverability guarantees, analytics, and scales beyond Gmail’s 500-email-per-day limit.
Why HTML Emails Need Inline Styles
HTML email is a different beast from web HTML. Email clients (Gmail, Outlook, Apple Mail) strip <style> tags and <head> sections — your carefully written CSS is discarded. The only reliable way to style HTML email is with inline styles on every element. This makes email templates verbose but is unavoidable. For complex templates, tools like MJML or email inlining libraries can help, but for the MERN Blog a simple template function with inline styles is perfectly adequate.
text and html properties in the message object. Your template function should return both versions — the HTML for display and a stripped-down text version for accessibility.The HTML Email Template Function
// server/src/utils/emailTemplates.js
const BRAND_COLOR = '#3b82f6';
const DANGER_COLOR = '#ef4444';
const FONT = "font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;";
// ── Wrapper: consistent header/footer around any content ─────────────────────
const emailWrapper = (content, preview = '') => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MERN Blog</title>
<!-- Preheader text: appears next to subject in inbox -->
<span style="display:none;max-height:0;overflow:hidden;">${preview}</span>
</head>
<body style="margin:0;padding:0;background-color:#f3f4f6;${FONT}">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f3f4f6;padding:40px 20px;">
<tr><td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;overflow:hidden;">
<!-- Header -->
<tr><td style="background:${BRAND_COLOR};padding:24px;text-align:center;">
<h1 style="color:#ffffff;margin:0;font-size:24px;${FONT}">MERN Blog</h1>
</td></tr>
<!-- Body -->
<tr><td style="padding:40px 32px;color:#374151;${FONT}">
${content}
</td></tr>
<!-- Footer -->
<tr><td style="background:#f9fafb;padding:24px;text-align:center;color:#6b7280;font-size:14px;${FONT}">
<p style="margin:0 0 8px;">© ${new Date().getFullYear()} MERN Blog. All rights reserved.</p>
<p style="margin:0;">This email was sent because you have an account on MERN Blog.</p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
`;
// ── Reusable button component ─────────────────────────────────────────────────
const ctaButton = (label, url, color = BRAND_COLOR) =>
`<a href="${url}" style="display:inline-block;background:${color};color:#ffffff;
padding:14px 28px;text-decoration:none;border-radius:6px;font-weight:600;
font-size:16px;margin:24px 0;">${label}</a>`;
// ── Welcome email ─────────────────────────────────────────────────────────────
const welcomeEmail = ({ name, verifyUrl }) => ({
subject: 'Welcome to MERN Blog — please verify your email',
text: `Hi ${name},\n\nWelcome to MERN Blog! Please verify your email:\n${verifyUrl}\n\nThis link expires in 24 hours.`,
html: emailWrapper(`
<h2 style="color:#111827;margin:0 0 16px;">Welcome, ${name}! 🎉</h2>
<p style="line-height:1.6;">Thanks for joining MERN Blog. Please verify your email address to unlock all features.</p>
${ctaButton('Verify Email Address', verifyUrl)}
<p style="color:#6b7280;font-size:14px;">This link expires in 24 hours. If you did not create an account, you can ignore this email.</p>
`, `Welcome to MERN Blog — verify your email to get started`),
});
// ── Password reset email ──────────────────────────────────────────────────────
const passwordResetEmail = ({ name, resetUrl }) => ({
subject: 'MERN Blog — Password Reset Request',
text: `Hi ${name},\n\nReset your password:\n${resetUrl}\n\nThis link expires in 1 hour. If you did not request this, ignore this email.`,
html: emailWrapper(`
<h2 style="color:#111827;margin:0 0 16px;">Password Reset</h2>
<p style="line-height:1.6;">Hi ${name}, we received a request to reset your MERN Blog password.</p>
${ctaButton('Reset Your Password', resetUrl, DANGER_COLOR)}
<p style="color:#6b7280;font-size:14px;">This link expires in 1 hour. If you did not request a password reset, please secure your account immediately.</p>
`, `Reset your MERN Blog password`),
});
// ── Password changed alert ────────────────────────────────────────────────────
const passwordChangedEmail = ({ name }) => ({
subject: 'Your MERN Blog password was changed',
text: `Hi ${name},\n\nYour password was successfully changed. If this was not you, contact support immediately.`,
html: emailWrapper(`
<h2 style="color:#111827;margin:0 0 16px;">Password Changed</h2>
<p style="line-height:1.6;">Hi ${name}, your MERN Blog password was changed successfully.</p>
<p style="line-height:1.6;">If you did not make this change, please <a href="${process.env.CLIENT_URL}/contact" style="color:#3b82f6;">contact support</a> immediately.</p>
`, `Your MERN Blog password was changed`),
});
module.exports = { welcomeEmail, passwordResetEmail, passwordChangedEmail };
Using Templates in Controllers
// Clean controller — template details hidden in emailTemplates.js
const { welcomeEmail, passwordResetEmail } = require('../utils/emailTemplates');
const sendEmail = require('../utils/sendEmail');
// Registration:
const { subject, text, html } = welcomeEmail({ name: user.name, verifyUrl });
sendEmail({ to: user.email, subject, text, html })
.catch(err => console.error('Welcome email failed:', err.message));
// Password reset:
const { subject, text, html } = passwordResetEmail({ name: user.name, resetUrl });
sendEmail({ to: user.email, subject, text, html })
.catch(err => console.error('Reset email failed:', err.message));
Switching to a Production Email Service — Resend
npm install resend
// server/src/utils/sendEmail.js — production version using Resend SDK
const { Resend } = require('resend');
const resend = new Resend(process.env.RESEND_API_KEY);
const sendEmail = async ({ to, subject, text, html }) => {
const { data, error } = await resend.emails.send({
from: process.env.EMAIL_FROM || 'MERN Blog <noreply@mernblog.com>',
to,
subject,
text,
html,
});
if (error) throw new Error(error.message);
return data;
};
module.exports = sendEmail;
// Advantages over Gmail SMTP:
// ✓ No 500/day limit (free tier: 3,000/month)
// ✓ Delivery analytics (opens, clicks, bounces)
// ✓ Automatic unsubscribe handling
// ✓ Better spam deliverability
// ✓ No SMTP connection management
Common Mistakes
Mistake 1 — Using external CSS or linked stylesheets
❌ Wrong — external CSS stripped by email clients:
// In email HTML:
`<link rel="stylesheet" href="/email.css">`
// Gmail strips <head> content — all styles ignored
✅ Correct — inline every style on every element.
Mistake 2 — Embedding large images as base64
❌ Wrong — base64 image makes email too large:
`<img src="data:image/png;base64,iVBOR..." />`
// Email over 102KB → Gmail clips the message
✅ Correct — reference hosted URLs:
`<img src="https://res.cloudinary.com/.../logo.png" width="120" alt="MERN Blog" />` // ✓
Mistake 3 — Not testing HTML email in multiple clients
❌ Wrong — only testing in your own Gmail — then discovering Outlook renders nothing correctly in production.
✅ Correct — use a service like Litmus or Email on Acid to preview your templates across 90+ email clients, or test with at least Gmail, Apple Mail, and Outlook before deploying.
Quick Reference
| Task | Approach |
|---|---|
| Style HTML email | Inline styles only — no <style> tags, no external CSS |
| Images in email | Hosted URLs (Cloudinary CDN) — never base64 |
| Keep email small | Under 102KB total to avoid Gmail clipping |
| Plain text | Always include alongside HTML |
| Production service | Resend, SendGrid, or Postmark — not Gmail SMTP |
| Test rendering | Gmail, Apple Mail, Outlook at minimum |
| Preheader text | Hidden text after subject — shows in inbox preview |