diff --git a/README.md b/README.md
index abe6e84..4b917c8 100644
--- a/README.md
+++ b/README.md
@@ -2,16 +2,17 @@
📧 **Template-based email system with scheduling and job processing for PayloadCMS**
-⚠️ **Pre-release Warning**: This package is currently in active development (v0.0.x). Breaking changes may occur before v1.0.0. Not recommended for production use.
+✨ **Simplified API**: Starting from v0.1.0, this plugin uses a simplified API that leverages PayloadCMS collections directly for better type safety and flexibility.
## Features
-✅ **Template System**: Create reusable email templates with Handlebars syntax
-✅ **Outbox Scheduling**: Schedule emails for future delivery
-✅ **Job Integration**: Automatic processing via PayloadCMS jobs queue
-✅ **Retry Failed Sends**: Automatic retry mechanism for failed emails
-✅ **Template Variables**: Dynamic content with validation
-✅ **Developer API**: Simple methods for sending emails programmatically
+✅ **Template System**: Create reusable email templates with LiquidJS, Mustache, or simple variables
+✅ **Type Safety**: Full TypeScript support using your generated Payload types
+✅ **Flexible Template Engines**: LiquidJS, Mustache, or bring your own template renderer
+✅ **Email Scheduling**: Schedule emails for future delivery using Payload collections
+✅ **Job Integration**: Automatic processing via PayloadCMS jobs queue
+✅ **Retry Failed Sends**: Automatic retry mechanism for failed emails
+✅ **Payload Native**: Uses Payload collections directly - no custom APIs to learn
## Installation
@@ -49,53 +50,124 @@ export default buildConfig({
})
```
-### 2. Send emails in your code
+### 2. Send emails using Payload collections
```typescript
-import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing'
+import { renderTemplate } from '@xtr-dev/payload-mailing'
-// Send immediately using template slug
-const emailId = await sendEmail(payload, {
- templateSlug: 'welcome-email',
- to: 'user@example.com',
- variables: {
- firstName: 'John',
- welcomeUrl: 'https://yoursite.com/welcome'
+// Option 1: Using templates with variables
+const { html, text, subject } = await renderTemplate(payload, 'welcome-email', {
+ firstName: 'John',
+ welcomeUrl: 'https://yoursite.com/welcome'
+})
+
+// Create email using Payload's collection API (full type safety!)
+const email = await payload.create({
+ collection: 'emails',
+ data: {
+ to: ['user@example.com'],
+ subject,
+ html,
+ text,
+ // Schedule for later (optional)
+ scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
+ // Add any custom fields you've defined
+ priority: 1,
+ customField: 'your-value', // Your custom collection fields work!
}
})
-// Schedule for later
-const scheduledId = await scheduleEmail(payload, {
- templateSlug: 'reminder-email',
- to: 'user@example.com',
- scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
- variables: {
- eventName: 'Product Launch',
- eventDate: new Date('2024-01-15')
+// Option 2: Direct HTML email (no template needed)
+const directEmail = await payload.create({
+ collection: 'emails',
+ data: {
+ to: ['user@example.com'],
+ subject: 'Welcome!',
+ html: '
Welcome John!
Thanks for joining!
',
+ text: 'Welcome John! Thanks for joining!',
}
})
```
+## Why This Approach is Better
+
+- ✅ **Full Type Safety**: Use your generated Payload types
+- ✅ **No Learning Curve**: Just use `payload.create()` like any collection
+- ✅ **Maximum Flexibility**: Add any custom fields to your email collection
+- ✅ **Payload Integration**: Leverage validation, hooks, access control
+- ✅ **Consistent API**: One way to create data in Payload
+
## Configuration
### Plugin Options
```typescript
-interface MailingPluginConfig {
- collections?: {
- templates?: string // default: 'email-templates'
- emails?: string // default: 'emails'
+mailingPlugin({
+ // Template engine (optional)
+ templateEngine: 'liquidjs', // 'liquidjs' | 'mustache' | 'simple'
+
+ // Custom template renderer (optional)
+ templateRenderer: async (template: string, variables: Record) => {
+ return yourCustomEngine.render(template, variables)
+ },
+
+ // Email transport
+ transport: {
+ host: 'smtp.gmail.com',
+ port: 587,
+ auth: { user: '...', pass: '...' }
+ },
+
+ // Collection names (optional)
+ collections: {
+ templates: 'email-templates', // default
+ emails: 'emails' // default
+ },
+
+ // Sending options
+ defaultFrom: 'noreply@yoursite.com',
+ defaultFromName: 'Your Site',
+ retryAttempts: 3, // default
+ retryDelay: 300000, // 5 minutes (default)
+
+ // Advanced options
+ emailWrapper: (email) => ({ // optional layout wrapper
+ ...email,
+ html: `${email.html}`
+ }),
+ richTextEditor: lexicalEditor(), // optional custom editor
+ onReady: async (payload) => { // optional initialization hook
+ console.log('Mailing plugin ready!')
}
- defaultFrom?: string
- transport?: Transporter | MailingTransportConfig
- queue?: string // default: 'default'
- retryAttempts?: number // default: 3
- retryDelay?: number // default: 300000 (5 minutes)
- emailWrapper?: EmailWrapperHook // optional email layout wrapper
- richTextEditor?: RichTextField['editor'] // optional custom rich text editor
- onReady?: (payload: any) => Promise // optional callback after plugin initialization
- initOrder?: 'before' | 'after' // default: 'before'
-}
+})
+```
+
+### Template Engine Options
+
+Choose your preferred template engine:
+
+```typescript
+// LiquidJS (default) - Modern syntax with logic
+mailingPlugin({
+ templateEngine: 'liquidjs' // {% if user.isPremium %}Premium!{% endif %}
+})
+
+// Mustache - Logic-less templates
+mailingPlugin({
+ templateEngine: 'mustache' // {{#user.isPremium}}Premium!{{/user.isPremium}}
+})
+
+// Simple variable replacement
+mailingPlugin({
+ templateEngine: 'simple' // Just {{variable}} replacement
+})
+
+// Custom template renderer
+mailingPlugin({
+ templateRenderer: async (template, variables) => {
+ return handlebars.compile(template)(variables) // Bring your own!
+ }
+})
```
### Transport Configuration
@@ -473,71 +545,50 @@ mailingPlugin({
})
```
-## Handlebars Helpers
+## Template Syntax Reference
-The plugin includes several built-in helpers:
+Depending on your chosen template engine, you can use different syntax:
-- `{{formatDate date 'short'}}` - Format dates (short, long, or default)
-- `{{formatCurrency amount 'USD'}}` - Format currency
-- `{{capitalize string}}` - Capitalize first letter
-- `{{#ifEquals value1 value2}}...{{/ifEquals}}` - Conditional equality
+### LiquidJS (Default)
+- Variables: `{{ user.name }}`
+- Logic: `{% if user.isPremium %}Premium content{% endif %}`
+- Loops: `{% for item in items %}{{ item.name }}{% endfor %}`
+- Filters: `{{ amount | formatCurrency }}`, `{{ date | formatDate: "short" }}`
-## API Methods
+### Mustache
+- Variables: `{{ user.name }}`
+- Logic: `{{#user.isPremium}}Premium content{{/user.isPremium}}`
+- Loops: `{{#items}}{{ name }}{{/items}}`
+- No built-in filters (use variables with pre-formatted data)
-### sendEmail(payload, options)
+### Simple
+- Variables only: `{{ user.name }}`, `{{ amount }}`, `{{ date }}`
-Send an email immediately:
+### Built-in Filters (LiquidJS only)
+- `formatDate` - Format dates: `{{ createdAt | formatDate: "short" }}`
+- `formatCurrency` - Format currency: `{{ amount | formatCurrency: "USD" }}`
+- `capitalize` - Capitalize first letter: `{{ name | capitalize }}`
+
+## Available Helper Functions
```typescript
-const emailId = await sendEmail(payload, {
- templateSlug: 'order-confirmation', // optional - use template slug
- to: ['customer@example.com'], // string or array of emails
- cc: ['manager@example.com'], // optional - array of emails
- bcc: ['archive@example.com'], // optional - array of emails
- from: 'orders@yoursite.com', // optional, uses default
- replyTo: 'support@yoursite.com', // optional
- subject: 'Custom subject', // required if no template
- html: 'Custom HTML
', // required if no template
- text: 'Custom text version', // optional
- variables: { // template variables
- orderNumber: '12345',
- customerName: 'John Doe'
- },
- priority: 1 // optional, 1-10 (1 = highest)
+import {
+ renderTemplate, // Render email templates with variables
+ processEmails, // Process pending emails manually
+ retryFailedEmails, // Retry failed emails
+ getMailing // Get mailing service instance
+} from '@xtr-dev/payload-mailing'
+
+// Render a template
+const { html, text, subject } = await renderTemplate(payload, 'welcome', {
+ name: 'John',
+ activationUrl: 'https://example.com/activate'
})
-```
-### scheduleEmail(payload, options)
-
-Schedule an email for later delivery:
-
-```typescript
-const emailId = await scheduleEmail(payload, {
- templateSlug: 'newsletter',
- to: ['user1@example.com', 'user2@example.com'],
- scheduledAt: new Date('2024-01-15T10:00:00Z'),
- variables: {
- month: 'January',
- highlights: ['Feature A', 'Feature B']
- }
-})
-```
-
-### processEmails(payload)
-
-Manually process pending emails:
-
-```typescript
-import { processEmails } from '@xtr-dev/payload-mailing'
+// Process emails manually
await processEmails(payload)
-```
-### retryFailedEmails(payload)
-
-Manually retry failed emails:
-
-```typescript
-import { retryFailedEmails } from '@xtr-dev/payload-mailing'
+// Retry failed emails
await retryFailedEmails(payload)
```
@@ -716,35 +767,85 @@ import {
} from '@xtr-dev/payload-mailing'
```
+## Migration Guide (v0.0.x → v0.1.0)
+
+**🚨 BREAKING CHANGES**: The API has been simplified to use Payload collections directly.
+
+### Before (v0.0.x)
+```typescript
+import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing'
+
+// Old way
+const emailId = await sendEmail(payload, {
+ templateSlug: 'welcome',
+ to: 'user@example.com',
+ variables: { name: 'John' }
+})
+
+const scheduledId = await scheduleEmail(payload, {
+ templateSlug: 'reminder',
+ to: 'user@example.com',
+ scheduledAt: new Date('2024-01-15T10:00:00Z'),
+ variables: { eventName: 'Launch' }
+})
+```
+
+### After (v0.1.0+)
+```typescript
+import { renderTemplate } from '@xtr-dev/payload-mailing'
+
+// New way - render template first
+const { html, text, subject } = await renderTemplate(payload, 'welcome', {
+ name: 'John'
+})
+
+// Then create email using Payload collections (full type safety!)
+const email = await payload.create({
+ collection: 'emails',
+ data: {
+ to: ['user@example.com'],
+ subject,
+ html,
+ text,
+ // For scheduling
+ scheduledAt: new Date('2024-01-15T10:00:00Z'),
+ // Add any custom fields from your collection
+ customField: 'value',
+ }
+})
+```
+
+### Benefits of Migration
+- ✅ **Full TypeScript support** with your generated Payload types
+- ✅ **Use any custom fields** you add to your email collection
+- ✅ **Leverage Payload's features**: validation, hooks, access control
+- ✅ **One consistent API** - just use `payload.create()`
+- ✅ **No wrapper methods** - direct access to Payload's power
+
## Recent Changes
-### v0.0.x (Latest)
+### v0.1.0 (Latest - Breaking Changes)
-**🔄 Breaking Changes:**
-- Removed email layouts system in favor of `emailWrapper` hook for better flexibility
-- Email fields (`to`, `cc`, `bcc`) now use `hasMany: true` for proper array handling
-- Templates now use slug-based lookup instead of ID-based for developer-friendly API
-- Email collection renamed from "outbox" to "emails"
-- Unified job processing: single `process-email-queue` job handles both pending and failed emails
+**🚀 Major API Simplification:**
+- **REMOVED**: `sendEmail()` and `scheduleEmail()` wrapper methods
+- **REMOVED**: `SendEmailOptions` custom types
+- **ADDED**: Direct Payload collection usage with full type safety
+- **ADDED**: `renderTemplate()` helper for template rendering
+- **ADDED**: Support for LiquidJS, Mustache, and custom template engines
+- **IMPROVED**: Webpack compatibility with proper dynamic imports
-**✨ New Features:**
-- Rich text editor with automatic HTML/text conversion
-- Template slugs for easier template reference
-- `emailWrapper` hook for consistent email layouts
-- Custom rich text editor configuration support
-- Initialization hooks (`onReady`, `initOrder`) for better plugin lifecycle control
-- Improved Handlebars variable interpolation with defensive programming
+**Template Engine Enhancements:**
+- **NEW**: LiquidJS support (default) with modern syntax and logic
+- **NEW**: Mustache support for logic-less templates
+- **NEW**: Custom template renderer hook for maximum flexibility
+- **NEW**: Simple variable replacement as fallback
+- **FIXED**: All webpack compatibility issues resolved
-**🐛 Bug Fixes:**
-- Fixed text version uppercase conversion in headings
-- Fixed Handlebars interpolation issues in text version
-- Improved plugin initialization order to prevent timing issues
-
-**💡 Improvements:**
-- Better admin UI with proper array input controls
-- More robust error handling and logging
-- Enhanced TypeScript definitions
-- Simplified template creation workflow
+**Developer Experience:**
+- **IMPROVED**: Full TypeScript inference using generated Payload types
+- **IMPROVED**: Comprehensive migration guide and documentation
+- **IMPROVED**: Better error handling and async patterns
+- **SIMPLIFIED**: Cleaner codebase with fewer abstractions
## License
diff --git a/package.json b/package.json
index 94d8c49..27d97aa 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@xtr-dev/payload-mailing",
- "version": "0.0.12",
+ "version": "0.1.1",
"description": "Template-based email system with scheduling and job processing for PayloadCMS",
"type": "module",
"main": "dist/index.js",
diff --git a/src/index.ts b/src/index.ts
index 226aed5..74cc6bb 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -16,8 +16,7 @@ export { default as Emails } from './collections/Emails.js'
// Utility functions for developers
export {
getMailing,
- sendEmail,
- scheduleEmail,
+ renderTemplate,
processEmails,
retryFailedEmails,
} from './utils/helpers.js'
\ No newline at end of file
diff --git a/src/services/MailingService.ts b/src/services/MailingService.ts
index adfc803..f835cba 100644
--- a/src/services/MailingService.ts
+++ b/src/services/MailingService.ts
@@ -3,7 +3,7 @@ import { Liquid } from 'liquidjs'
import nodemailer, { Transporter } from 'nodemailer'
import {
MailingPluginConfig,
- SendEmailOptions,
+ TemplateVariables,
MailingService as IMailingService,
EmailTemplate,
QueuedEmail,
@@ -107,69 +107,21 @@ export class MailingService implements IMailingService {
}
}
- async sendEmail(options: SendEmailOptions): Promise {
- const emailId = await this.scheduleEmail({
- ...options,
- scheduledAt: new Date()
- })
+ async renderTemplate(templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> {
+ const template = await this.getTemplateBySlug(templateSlug)
- await this.processEmailItem(emailId)
-
- return emailId
- }
-
- async scheduleEmail(options: SendEmailOptions): Promise {
- let html = options.html || ''
- let text = options.text || ''
- let subject = options.subject || ''
- let templateId: string | undefined = undefined
-
- if (options.templateSlug) {
- const template = await this.getTemplateBySlug(options.templateSlug)
-
- if (template) {
- templateId = template.id
- const variables = options.variables || {}
- const renderedContent = await this.renderEmailTemplate(template, variables)
- html = renderedContent.html
- text = renderedContent.text
- subject = await this.renderTemplate(template.subject, variables)
- } else {
- throw new Error(`Email template not found: ${options.templateSlug}`)
- }
+ if (!template) {
+ throw new Error(`Email template not found: ${templateSlug}`)
}
- if (!subject && !options.subject) {
- throw new Error('Email subject is required')
+ const emailContent = await this.renderEmailTemplate(template, variables)
+ const subject = await this.renderTemplateString(template.subject, variables)
+
+ return {
+ html: emailContent.html,
+ text: emailContent.text,
+ subject
}
-
- if (!html && !options.html) {
- throw new Error('Email HTML content is required')
- }
-
- const queueData = {
- template: templateId,
- to: Array.isArray(options.to) ? options.to : [options.to],
- cc: options.cc ? (Array.isArray(options.cc) ? options.cc : [options.cc]) : undefined,
- bcc: options.bcc ? (Array.isArray(options.bcc) ? options.bcc : [options.bcc]) : undefined,
- from: options.from || this.getDefaultFrom(),
- replyTo: options.replyTo,
- subject: subject || options.subject,
- html,
- text,
- variables: options.variables,
- scheduledAt: options.scheduledAt?.toISOString(),
- status: 'pending' as const,
- attempts: 0,
- priority: options.priority || 5,
- }
-
- const result = await this.payload.create({
- collection: this.emailsCollection as any,
- data: queueData,
- })
-
- return result.id as string
}
async processEmails(): Promise {
@@ -366,7 +318,7 @@ export class MailingService implements IMailingService {
}
}
- private async renderTemplate(template: string, variables: Record): Promise {
+ private async renderTemplateString(template: string, variables: Record): Promise {
// Use custom template renderer if provided
if (this.config.templateRenderer) {
try {
@@ -434,8 +386,8 @@ export class MailingService implements IMailingService {
let text = serializeRichTextToText(template.content)
// Apply template variables to the rendered content
- html = await this.renderTemplate(html, variables)
- text = await this.renderTemplate(text, variables)
+ html = await this.renderTemplateString(html, variables)
+ text = await this.renderTemplateString(text, variables)
return { html, text }
}
diff --git a/src/types/index.ts b/src/types/index.ts
index 14bb179..b29b150 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,5 +1,5 @@
import { Payload } from 'payload'
-import type { CollectionConfig, RichTextField } from 'payload'
+import type { CollectionConfig, RichTextField, TypedCollection } from 'payload'
import { Transporter } from 'nodemailer'
export interface EmailObject {
@@ -83,26 +83,15 @@ export interface QueuedEmail {
updatedAt: string
}
-export interface SendEmailOptions {
- templateSlug?: string
- to: string | string[]
- cc?: string | string[]
- bcc?: string | string[]
- from?: string
- replyTo?: string
- subject?: string
- html?: string
- text?: string
- variables?: Record
- scheduledAt?: Date
- priority?: number
+// Simple helper type for template variables
+export interface TemplateVariables {
+ [key: string]: any
}
export interface MailingService {
- sendEmail(options: SendEmailOptions): Promise
- scheduleEmail(options: SendEmailOptions): Promise
processEmails(): Promise
retryFailedEmails(): Promise
+ renderTemplate(templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }>
}
export interface MailingContext {
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 38212f6..1e37e48 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -1,5 +1,5 @@
import { Payload } from 'payload'
-import { SendEmailOptions } from '../types/index.js'
+import { TemplateVariables } from '../types/index.js'
export const getMailing = (payload: Payload) => {
const mailing = (payload as any).mailing
@@ -9,14 +9,9 @@ export const getMailing = (payload: Payload) => {
return mailing
}
-export const sendEmail = async (payload: Payload, options: SendEmailOptions): Promise => {
+export const renderTemplate = async (payload: Payload, templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> => {
const mailing = getMailing(payload)
- return mailing.service.sendEmail(options)
-}
-
-export const scheduleEmail = async (payload: Payload, options: SendEmailOptions): Promise => {
- const mailing = getMailing(payload)
- return mailing.service.scheduleEmail(options)
+ return mailing.service.renderTemplate(templateSlug, variables)
}
export const processEmails = async (payload: Payload): Promise => {
diff --git a/template-syntax-migration.md b/template-syntax-migration.md
deleted file mode 100644
index 03c3957..0000000
--- a/template-syntax-migration.md
+++ /dev/null
@@ -1,160 +0,0 @@
-# Template Engine Options
-
-The plugin now supports flexible template rendering with multiple options:
-
-1. **String-based Configuration** (easy setup with built-in engines)
-2. **Custom Template Renderer Hook** (maximum flexibility)
-3. **Simple Variable Replacement** (fallback, no dependencies)
-
-## Configuration Options
-
-### String-based Template Engine Configuration
-Easy setup using built-in template engines:
-
-```typescript
-// Using LiquidJS (default, requires: npm install liquidjs)
-mailingPlugin({
- templateEngine: 'liquidjs'
-})
-
-// Using Mustache (requires: npm install mustache)
-mailingPlugin({
- templateEngine: 'mustache'
-})
-
-// Using simple variable replacement (no dependencies)
-mailingPlugin({
- templateEngine: 'simple'
-})
-```
-
-### Custom Template Renderer Hook
-```typescript
-// Example with Handlebars
-import Handlebars from 'handlebars'
-
-mailingPlugin({
- templateRenderer: async (template: string, variables: Record) => {
- const compiled = Handlebars.compile(template)
- return compiled(variables)
- }
-})
-
-// Example with Mustache
-import Mustache from 'mustache'
-
-mailingPlugin({
- templateRenderer: async (template: string, variables: Record) => {
- return Mustache.render(template, variables)
- }
-})
-
-// Example with Nunjucks
-import nunjucks from 'nunjucks'
-
-mailingPlugin({
- templateRenderer: async (template: string, variables: Record) => {
- return nunjucks.renderString(template, variables)
- }
-})
-```
-
-### Using LiquidJS (Optional)
-Install the optional dependency:
-```bash
-npm install liquidjs
-# or
-pnpm add liquidjs
-```
-
-### Fallback Mode
-If no custom renderer is provided and neither LiquidJS nor Mustache are installed, simple `{{variable}}` replacement is used.
-
-## Template Syntax Reference
-
-### Mustache Syntax (Logic-less)
-```mustache
-Hello {{user.name}},
-
-{{#user.isPremium}}
- Welcome to premium! Your balance is {{balance}}.
-{{/user.isPremium}}
-
-{{#orders}}
- Order: {{id}} - {{date}}
-{{/orders}}
-```
-
-### LiquidJS Syntax (With Logic)
-```liquid
-Hello {{user.name}},
-
-{% if user.isPremium %}
- Welcome to premium! Your balance is {{balance | formatCurrency}}.
-{% endif %}
-
-{% for order in orders %}
- Order: {{order.id}} - {{order.date | formatDate: "short"}}
-{% endfor %}
-```
-
-### Simple Variable Replacement
-```
-Hello {{user.name}},
-Your balance is {{balance}}.
-```
-
-## Migration from Handlebars
-
-### Variables
-- **Handlebars**: `{{variable}}`
-- **LiquidJS**: `{{variable}}` (same)
-
-### Conditionals
-- **Handlebars**: `{{#if condition}}content{{/if}}`
-- **LiquidJS**: `{% if condition %}content{% endif %}`
-
-### Loops
-- **Handlebars**: `{{#each items}}{{this}}{{/each}}`
-- **LiquidJS**: `{% for item in items %}{{item}}{% endfor %}`
-
-### Filters/Helpers
-- **Handlebars**: `{{formatDate date "short"}}`
-- **LiquidJS**: `{{date | formatDate: "short"}}`
-
-### Available Filters
-- `formatDate` - Format dates (short, long, or default)
-- `formatCurrency` - Format currency amounts
-- `capitalize` - Capitalize first letter
-
-### Comparison Operations (LiquidJS Advantage)
-- **Handlebars**: Required `{{#ifEquals}}` helper
-- **LiquidJS**: Built-in: `{% if user.role == "admin" %}`
-
-## Example Migration
-
-### Before (Handlebars)
-```handlebars
-Hello {{user.name}},
-
-{{#if user.isPremium}}
- Welcome to premium! Your balance is {{formatCurrency balance}}.
-{{/if}}
-
-{{#each orders}}
- Order: {{this.id}} - {{formatDate this.date "short"}}
-{{/each}}
-```
-
-### After (LiquidJS)
-```liquid
-Hello {{user.name}},
-
-{% if user.isPremium %}
- Welcome to premium! Your balance is {{balance | formatCurrency}}.
-{% endif %}
-
-{% for order in orders %}
- Order: {{order.id}} - {{order.date | formatDate: "short"}}
-{% endfor %}
-```
\ No newline at end of file