Aller au contenu principal

Email Templates with Handlebars

JifiJs uses Handlebars as its templating engine for creating beautiful, dynamic HTML emails. This guide shows you how to create, customize, and send professional email templates.

🎨 Template Structure​

Directory Layout​

templates/
β”œβ”€β”€ emails/
β”‚ β”œβ”€β”€ layouts/
β”‚ β”‚ └── default.hbs # Base layout
β”‚ β”œβ”€β”€ partials/
β”‚ β”‚ β”œβ”€β”€ header.hbs # Reusable header
β”‚ β”‚ β”œβ”€β”€ footer.hbs # Reusable footer
β”‚ β”‚ └── button.hbs # Reusable button component
β”‚ β”œβ”€β”€ welcome.hbs # Welcome email
β”‚ β”œβ”€β”€ password-reset.hbs # Password reset
β”‚ β”œβ”€β”€ verify-email.hbs # Email verification
β”‚ └── newsletter.hbs # Newsletter template
└── ...

πŸ—οΈ Creating Templates​

Basic Template​

Create templates/emails/welcome.hbs:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to {{appName}}</title>
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
color: white;
padding: 30px;
text-align: center;
border-radius: 8px 8px 0 0;
}
.content {
background: #ffffff;
padding: 30px;
border: 1px solid #e5e7eb;
}
.button {
display: inline-block;
padding: 12px 24px;
background: #6366f1;
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
}
.footer {
text-align: center;
padding: 20px;
color: #6b7280;
font-size: 14px;
}
</style>
</head>
<body>
<div class="header">
<h1>Welcome to {{appName}}!</h1>
</div>

<div class="content">
<p>Hi {{name}},</p>

<p>Thanks for joining {{appName}}! We're excited to have you on board.</p>

<p>To get started, please verify your email address:</p>

<p style="text-align: center; margin: 30px 0;">
<a href="{{verificationLink}}" class="button">Verify Email Address</a>
</p>

<p>If you didn't create this account, you can safely ignore this email.</p>

<p>Best regards,<br>The {{appName}} Team</p>
</div>

<div class="footer">
<p>&copy; {{year}} {{appName}}. All rights reserved.</p>
<p>{{companyAddress}}</p>
</div>
</body>
</html>

Using Layouts​

Create a reusable layout in templates/emails/layouts/default.hbs:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
{{> email-styles}}
</head>
<body>
{{> header}}

<div class="container">
{{{body}}}
</div>

{{> footer}}
</body>
</html>

Then create a template that uses this layout:

---
layout: default
title: Welcome Email
---

<h2>Welcome, {{name}}!</h2>

<p>Your account has been created successfully.</p>

{{> button text="Get Started" url=dashboardUrl}}

Partials (Reusable Components)​

Create templates/emails/partials/button.hbs:

<table border="0" cellpadding="0" cellspacing="0" style="margin: 20px 0;">
<tr>
<td align="center" style="border-radius: 6px;" bgcolor="{{bgColor}}">
<a href="{{url}}" target="_blank" style="
display: inline-block;
padding: 14px 28px;
color: {{textColor}};
text-decoration: none;
border-radius: 6px;
font-weight: 600;
font-size: 16px;
">
{{text}}
</a>
</td>
</tr>
</table>

Usage in templates:

{{> button
text="Verify Email"
url=verificationLink
bgColor="#6366f1"
textColor="#ffffff"
}}

πŸ“§ Sending Templated Emails​

Email Service​

Create services/email.service.ts:

import nodemailer from 'nodemailer';
import handlebars from 'handlebars';
import fs from 'fs/promises';
import path from 'path';

export class EmailService {
private transporter: nodemailer.Transporter;
private templatesCache: Map<string, HandlebarsTemplateDelegate>;

constructor() {
this.transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
});

this.templatesCache = new Map();
this.registerHelpers();
this.registerPartials();
}

private registerHelpers() {
// Format date helper
handlebars.registerHelper('formatDate', (date: Date, format: string) => {
return new Intl.DateTimeFormat('en-US').format(date);
});

// Currency helper
handlebars.registerHelper('currency', (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
});

// Conditional helper
handlebars.registerHelper('ifEquals', function(arg1, arg2, options) {
return (arg1 === arg2) ? options.fn(this) : options.inverse(this);
});
}

private async registerPartials() {
const partialsDir = path.join(__dirname, '../templates/emails/partials');
const files = await fs.readdir(partialsDir);

for (const file of files) {
if (file.endsWith('.hbs')) {
const name = file.replace('.hbs', '');
const content = await fs.readFile(
path.join(partialsDir, file),
'utf-8'
);
handlebars.registerPartial(name, content);
}
}
}

private async loadTemplate(templateName: string): Promise<HandlebarsTemplateDelegate> {
if (this.templatesCache.has(templateName)) {
return this.templatesCache.get(templateName)!;
}

const templatePath = path.join(
__dirname,
'../templates/emails',
`${templateName}.hbs`
);

const templateContent = await fs.readFile(templatePath, 'utf-8');
const compiled = handlebars.compile(templateContent);

this.templatesCache.set(templateName, compiled);

return compiled;
}

async sendEmail(options: {
to: string | string[];
subject: string;
template: string;
data: Record<string, any>;
attachments?: any[];
}) {
const { to, subject, template, data, attachments } = options;

// Add common data
const templateData = {
...data,
appName: process.env.APP_NAME || 'JifiJs',
year: new Date().getFullYear(),
companyAddress: process.env.COMPANY_ADDRESS,
supportEmail: process.env.SUPPORT_EMAIL,
};

// Compile template
const compiledTemplate = await this.loadTemplate(template);
const html = compiledTemplate(templateData);

// Send email
const result = await this.transporter.sendMail({
from: `"${process.env.EMAIL_FROM_NAME}" <${process.env.EMAIL_FROM}>`,
to: Array.isArray(to) ? to.join(', ') : to,
subject,
html,
attachments,
});

return result;
}

// Specific email methods
async sendWelcomeEmail(email: string, name: string, verificationToken: string) {
const verificationLink = `${process.env.APP_URL}/verify-email?token=${verificationToken}`;

return this.sendEmail({
to: email,
subject: `Welcome to ${process.env.APP_NAME}!`,
template: 'welcome',
data: {
name,
verificationLink,
},
});
}

async sendPasswordResetEmail(email: string, name: string, resetToken: string) {
const resetLink = `${process.env.APP_URL}/reset-password?token=${resetToken}`;

return this.sendEmail({
to: email,
subject: 'Password Reset Request',
template: 'password-reset',
data: {
name,
resetLink,
expiresIn: '1 hour',
},
});
}

async sendOrderConfirmation(email: string, orderData: any) {
return this.sendEmail({
to: email,
subject: `Order Confirmation #${orderData.orderNumber}`,
template: 'order-confirmation',
data: orderData,
});
}
}

export default new EmailService();

🎯 Advanced Templates​

Conditional Content​

<p>Hi {{name}},</p>

{{#if isPremium}}
<div class="premium-banner">
<h3>Premium Member Benefits</h3>
<ul>
<li>Priority Support</li>
<li>Advanced Features</li>
<li>No Ads</li>
</ul>
</div>
{{else}}
<p>Upgrade to Premium for exclusive benefits!</p>
{{> button text="Upgrade Now" url=upgradeUrl}}
{{/if}}

Loops​

<h3>Your Recent Orders</h3>

<table>
<thead>
<tr>
<th>Order #</th>
<th>Date</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{{#each orders}}
<tr>
<td>{{this.orderNumber}}</td>
<td>{{formatDate this.date}}</td>
<td>{{currency this.total}}</td>
</tr>
{{/each}}
</tbody>
</table>

{{#unless orders.length}}
<p>You haven't placed any orders yet.</p>
{{/unless}}

Custom Helpers​

// Register custom helpers
handlebars.registerHelper('uppercase', (str: string) => {
return str.toUpperCase();
});

handlebars.registerHelper('truncate', (str: string, length: number) => {
return str.length > length ? str.substring(0, length) + '...' : str;
});

handlebars.registerHelper('pluralize', (count: number, singular: string, plural: string) => {
return count === 1 ? singular : plural;
});

Usage:

<h1>{{uppercase title}}</h1>
<p>{{truncate description 100}}</p>
<p>You have {{itemCount}} {{pluralize itemCount "item" "items"}} in your cart</p>

πŸ“± Responsive Design​

Mobile-Friendly Template​

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* Desktop styles */
.container {
max-width: 600px;
margin: 0 auto;
}

/* Mobile styles */
@media only screen and (max-width: 600px) {
.container {
width: 100% !important;
padding: 10px !important;
}

.button {
display: block !important;
width: 100% !important;
text-align: center !important;
}

h1 {
font-size: 24px !important;
}

.mobile-hide {
display: none !important;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Content here -->
</div>
</body>
</html>

🎨 Email-Safe CSS​

Inline Styles (Most Compatible)​

<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="
background-color: #6366f1;
color: #ffffff;
padding: 20px;
text-align: center;
font-family: Arial, sans-serif;
">
<h1 style="margin: 0; font-size: 28px;">
{{title}}
</h1>
</td>
</tr>
</table>

Email CSS Best Practices​

βœ… Use inline styles - Most reliable across email clients βœ… Use tables for layout - Better support than divs βœ… Use web-safe fonts - Arial, Helvetica, Times New Roman βœ… Specify full hex colors - #000000 instead of #000 βœ… Use padding instead of margin - Better support

❌ Avoid flexbox/grid - Poor email client support ❌ Avoid background images - Blocked by many clients ❌ Avoid JavaScript - Completely blocked ❌ Avoid external CSS - Often stripped ❌ Avoid video/audio - Not supported

πŸ§ͺ Testing Templates​

Preview in Development​

import EmailService from './services/email.service';

// Development route to preview emails
app.get('/dev/email-preview/:template', async (req, res) => {
if (process.env.NODE_ENV !== 'development') {
return res.status(404).send('Not found');
}

const { template } = req.params;

const html = await EmailService.renderTemplate(template, {
name: 'John Doe',
email: 'john@example.com',
verificationLink: 'http://localhost:3000/verify',
});

res.send(html);
});

Testing Tools​

πŸ“š Common Templates​

Password Reset​

<p>Hi {{name}},</p>

<p>We received a request to reset your password. Click the button below to create a new password:</p>

{{> button text="Reset Password" url=resetLink}}

<p style="color: #dc2626;">
<strong>This link expires in {{expiresIn}}.</strong>
</p>

<p>If you didn't request this, you can safely ignore this email.</p>

Email Verification​

<p>Hi {{name}},</p>

<p>Please verify your email address to activate your account:</p>

{{> button text="Verify Email" url=verificationLink}}

<p>Or copy and paste this link:</p>
<p style="color: #6b7280; word-break: break-all;">{{verificationLink}}</p>

Order Confirmation​

<h2>Order Confirmation</h2>

<p>Thank you for your order, {{customerName}}!</p>

<table style="width: 100%; border-collapse: collapse;">
<tr style="background: #f3f4f6;">
<th style="padding: 12px; text-align: left;">Order Number</th>
<td style="padding: 12px;">{{orderNumber}}</td>
</tr>
<tr>
<th style="padding: 12px; text-align: left;">Date</th>
<td style="padding: 12px;">{{formatDate orderDate}}</td>
</tr>
<tr style="background: #f3f4f6;">
<th style="padding: 12px; text-align: left;">Total</th>
<td style="padding: 12px; font-weight: bold;">{{currency total}}</td>
</tr>
</table>

<h3>Items</h3>
{{#each items}}
<div style="border-bottom: 1px solid #e5e7eb; padding: 10px 0;">
<strong>{{this.name}}</strong> x {{this.quantity}}
<span style="float: right;">{{currency this.price}}</span>
</div>
{{/each}}

πŸ“ Best Practices​

  1. Keep it Simple - Complex layouts break in email clients
  2. Test Everywhere - Check in Gmail, Outlook, Apple Mail, etc.
  3. Use Alt Text - For images that might be blocked
  4. Include Plain Text - Always provide a text alternative
  5. Make Links Obvious - Use clear CTAs
  6. Optimize Images - Compress for faster loading
  7. Add Unsubscribe Link - Required by law for marketing emails

✨ Pro Tip: Use a service like Mailtrap.io in development to test emails without sending them to real users. It catches all outgoing emails and lets you preview them.