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:
Using Layoutsβ
Create a reusable layout in templates/emails/layouts/default.hbs:
Then create a template that uses this layout:
Partials (Reusable Components)β
Create templates/emails/partials/button.hbs:
Usage in templates:
π§ 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β
Loopsβ
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:
π± Responsive Designβ
Mobile-Friendly Templateβ
π¨ Email-Safe CSSβ
Inline Styles (Most Compatible)β
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β
- Litmus - https://litmus.com
- Email on Acid - https://www.emailonacid.com
- Mailtrap - https://mailtrap.io (catch-all SMTP for development)
- MailHog - Local email testing tool
π Common Templatesβ
Password Resetβ
Email Verificationβ
Order Confirmationβ
π Best Practicesβ
- Keep it Simple - Complex layouts break in email clients
- Test Everywhere - Check in Gmail, Outlook, Apple Mail, etc.
- Use Alt Text - For images that might be blocked
- Include Plain Text - Always provide a text alternative
- Make Links Obvious - Use clear CTAs
- Optimize Images - Compress for faster loading
- Add Unsubscribe Link - Required by law for marketing emails
π Related Documentationβ
β¨ 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.