Built-in templates for verification, password reset, and welcome emails. A Handlebars-like template engine for custom emails with variables, conditionals, loops, and layouts.
LumenJS ships three production-ready email templates. They render responsive, table-based HTML that works in Outlook, Gmail, Apple Mail, and all major email clients.
verify-email: sent after user registration when email verification is enabled. Includes a personalized greeting, explanation text, a purple CTA button linking to the verification URL, and a plain-text fallback link.
import { renderEmailTemplate } from '@nuraly/lumenjs/email'; const html = renderEmailTemplate(config, 'verify-email', { appName: 'My App', url: 'https://myapp.com/__nk_auth/verify-email?token=abc123', userName: 'Alice', });
password-reset: sent when a user requests a password reset. Shows a greeting, instructions, a CTA button, a 1-hour expiry notice, and a copy-paste link.
const html = renderEmailTemplate(config, 'password-reset', { appName: 'My App', url: 'https://myapp.com/__nk_auth/reset-password?token=xyz789', userName: 'Bob', });
welcome: sent after email verification succeeds. Includes a greeting, a welcome message, and a "Sign in" CTA button linking to the login page.
const html = renderEmailTemplate(config, 'welcome', { appName: 'My App', url: 'https://myapp.com/login', // used as loginUrl fallback loginUrl: 'https://myapp.com/login', // explicit login page URL userName: 'Alice', });
The template engine uses a Handlebars-like syntax for file-based HTML templates. It supports variables, conditionals, loops, buttons, and layouts.
Variables: double curly braces, HTML-escaped by default:
<!-- Escaped (safe for user input) --> <p>Hello, {{userName}}!</p> <p>Your email: {{email}}</p> <!-- Dotted path access --> <p>{{user.name}} from {{user.company}}</p>
Raw (unescaped): triple curly braces, use for pre-rendered HTML:
<!-- Outputs HTML without escaping --> {{{customHtml}}} {{{richDescription}}}
{{{raw}}} for trusted content. User-submitted data should always use {{escaped}} to prevent XSS in email clients that render JavaScript (like some webmail apps).
Conditionals: render blocks based on truthy values:
{{#if userName}} <p>Hi {{userName}},</p> {{/if}} {{#if hasDiscount}} <p>You have a special discount waiting!</p> {{/if}}
Loops: iterate over arrays. Inside the loop, access item properties directly and use {{@index}} for the zero-based index:
{{#each items}} <tr> <td>{{@index}}</td> <td>{{name}}</td> <td>{{price}}</td> </tr> {{/each}} <!-- Primitive arrays --> {{#each tags}} <span>{{.}}</span> {{/each}}
CTA Button: renders a centered, purple call-to-action button (table-based for email client compatibility):
{{#button url="{{url}}" text="Verify email"}} <!-- Variables are interpolated inside url and text --> {{#button url="{{resetUrl}}" text="Reset password"}}
Layout wrapper: wraps the entire template in the base email layout (app icon, centered card, footer):
{{#layout}} <h1 style="font-size:22px; font-weight:800;">Hello!</h1> <p>Welcome to {{appName}}.</p> {{#button url="{{url}}" text="Get started"}} {{/layout}}
Place .html files in an emails/ directory at your project root. The filename (without extension) becomes the template name.
my-project/ lumenjs.email.ts emails/ verify-email.html ← overrides built-in verify-email password-reset.html ← overrides built-in password-reset welcome.html ← overrides built-in welcome order-confirmation.html ← custom template invite.html ← custom template
Example file-based template (emails/order-confirmation.html):
{{#layout}} <h1 style="font-size:22px; font-weight:800; color:#0f1419;">Order Confirmed</h1> <p style="font-size:15px; color:#536471;"> Hi {{userName}}, your order #{{orderId}} has been confirmed. </p> <table style="width:100%; font-size:14px; border-collapse:collapse;"> <tr><th style="text-align:left; padding:8px; border-bottom:1px solid #eee;">Item</th><th style="text-align:right; padding:8px; border-bottom:1px solid #eee;">Price</th></tr> {{#each items}} <tr> <td style="padding:8px; border-bottom:1px solid #f5f5f5;">{{name}}</td> <td style="text-align:right; padding:8px; border-bottom:1px solid #f5f5f5;">${{price}}</td> </tr> {{/each}} </table> {{#button url="{{url}}" text="View order"}} {{/layout}}
Render the template from an API route:
import { renderEmailTemplate, sendEmail, loadEmailConfig } from '@nuraly/lumenjs/email'; const config = await loadEmailConfig(process.cwd()); const html = renderEmailTemplate(config, 'order-confirmation', { appName: 'My Store', url: 'https://mystore.com/orders/456', userName: 'Alice', orderId: '456', items: [ { name: 'Widget', price: '29.99' }, { name: 'Gadget', price: '49.99' }, ], }); await sendEmail(config, { to: '[email protected]', subject: 'Order #456 confirmed', html, });
Resolution order: when rendering a template by name, LumenJS checks:
emails/{name}.html (compiled with the template engine)config.templates[name] from lumenjs.email.tsverify-email, password-reset, or welcome templateemails/verify-email.html file into your project and it will automatically override the built-in verification template. No config changes needed.
For programmatic control, define template functions directly in lumenjs.email.ts via the templates option. Each function receives a TemplateData object and returns an HTML string.
// lumenjs.email.ts import type { EmailConfig } from '@nuraly/lumenjs/email'; import { renderTemplate, renderButton } from '@nuraly/lumenjs/email'; export default { provider: 'resend', from: 'App <[email protected]>', resend: { apiKey: process.env.RESEND_API_KEY }, templates: { // Override the built-in verify-email template 'verify-email': (data) => renderTemplate(data.appName, ` <h1 style="font-size:22px; color:#0f1419;">Please confirm your email</h1> <p>Hi ${data.userName || 'there'},</p> <p>Click below to verify your account.</p> ${renderButton('Confirm', data.url)} `), // Add a completely custom template 'invite': (data) => renderTemplate(data.appName, ` <h1 style="font-size:22px; color:#0f1419;">You're invited!</h1> <p>${data.inviterName} invited you to join ${data.appName}.</p> ${renderButton('Accept invite', data.url)} `), }, } satisfies EmailConfig;
The TemplateData interface:
| Field | Type | Description |
|---|---|---|
appName | string | Application name (from config title) |
url | string | Primary action URL |
userName | string? | Recipient's display name |
email | string? | Recipient's email address |
loginUrl | string? | Login page URL (used by welcome template) |
[key: string] | any | Arbitrary extra data (arrays, objects, etc.) |
Helper functions available for custom templates:
renderTemplate(appName, content, footerText?): wraps content in the base email layout (centered card, app icon, footer)renderButton(text, url): renders a purple CTA button (table-based, works in all email clients)