7.8 KiB
Unified Email Template System
Overview
All emails now use a single master template for consistent branding and design. The master template wraps content-only HTML from database templates.
Architecture
Database Template (content only)
↓
Process Shortcodes ({nama}, {platform_name}, etc.)
↓
EmailTemplateRenderer.render() - wraps with master template
↓
Complete HTML Email sent via provider
Master Template
Location: supabase/shared/email-template-renderer.ts
Features:
- Brutalist design (black borders, hard shadows)
- Responsive layout (600px max width)
.tiptap-contentwrapper for auto-styling- Header with brand name + notification ID
- Footer with unsubscribe links
- All CSS included (no external dependencies)
CSS Classes for Content:
.tiptap-content h1, h2, h3- Headings.tiptap-content p- Paragraphs.tiptap-content a- Links (underlined, bold).tiptap-content ul, ol- Lists.tiptap-content table- Tables with brutalist borders.btn- Buttons with hard shadow.otp-box- OTP codes with dashed border.alert-success, .alert-danger, .alert-info- Colored alert boxes
Database Templates
Format
CORRECT (content-only):
<h1>Payment Successful!</h1>
<p>Hello <strong>{nama}</strong>, your payment has been confirmed.</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Order ID</td>
<td>{order_id}</td>
</tr>
</tbody>
</table>
WRONG (full HTML):
<!DOCTYPE html>
<html>
<head>...</head>
<body>
<h1>Payment Successful!</h1>
...
</body>
</html>
Why Content-Only?
The master template provides:
- Email client compatibility (resets, Outlook fixes)
- Consistent header/footer
- Responsive wrapper
- Brutalist styling
Your content just needs the body HTML - no <html>, <head>, or <body> tags.
Usage in Edge Functions
Auth OTP (send-auth-otp)
import { EmailTemplateRenderer } from "../shared/email-template-renderer.ts";
// Fetch template from database
const template = await supabase
.from("notification_templates")
.select("*")
.eq("key", "auth_email_verification")
.single();
// Process shortcodes
let htmlContent = template.email_body_html;
Object.entries(templateVars).forEach(([key, value]) => {
htmlContent = htmlContent.replace(new RegExp(`{${key}}`, 'g'), value);
});
// Wrap with master template
const htmlBody = EmailTemplateRenderer.render({
subject: template.email_subject,
content: htmlContent,
brandName: settings.platform_name || 'ACCESS HUB',
});
// Send via send-email-v2
await fetch(`${supabaseUrl}/functions/v1/send-email-v2`, {
method: 'POST',
body: JSON.stringify({
to: email,
html_body: htmlBody,
// ... other fields
}),
});
Other Notifications (send-notification)
import { EmailTemplateRenderer } from "../shared/email-template-renderer.ts";
// Fetch template and process shortcodes
const htmlContent = replaceVariables(template.body_html || template.body_text, allVariables);
// Wrap with master template
const htmlBody = EmailTemplateRenderer.render({
subject: subject,
content: htmlContent,
brandName: settings.brand_name || "ACCESS HUB",
});
// Send via provider (SMTP, Resend, etc.)
await sendViaSMTP({ html: htmlBody, ... });
Available Shortcodes
See ShortcodeProcessor.DEFAULT_DATA in supabase/shared/email-template-renderer.ts:
User:
{nama}- User name{email}- User email
Order:
{order_id}- Order ID{tanggal_pesanan}- Order date{total}- Total amount{metode_pembayaran}- Payment method
Product:
{produk}- Product name{kategori_produk}- Product category
Access:
{link_akses}- Access link{username_akses}- Access username{password_akses}- Access password
Consulting:
{tanggal_konsultasi}- Consultation date{jam_konsultasi}- Consultation time{link_meet}- Meeting link
And many more...
Creating New Templates
1. Design Content-Only HTML
Use brutalist components:
<h1>Welcome!</h1>
<p>Hello <strong>{nama}</strong>, welcome to <strong>{platform_name}</strong>!</p>
<h2>Your Details</h2>
<table>
<thead>
<tr>
<th>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Email</td>
<td>{email}</td>
</tr>
<tr>
<td>Plan</td>
<td>{plan_name}</td>
</tr>
</tbody>
</table>
<p style="margin-top: 30px;">
<a href="{dashboard_link}" class="btn btn-full">
Go to Dashboard
</a>
</p>
<blockquote class="alert-success">
<strong>Success!</strong> Your account is ready to use.
</blockquote>
2. Add to Database
INSERT INTO notification_templates (
key,
name,
is_active,
email_subject,
email_body_html
) VALUES (
'welcome_email',
'Welcome Email',
true,
'Welcome to {platform_name}!',
'---<h1>Welcome!</h1>...---'
);
3. Use in Edge Function
const template = await getTemplate('welcome_email');
const htmlContent = processShortcodes(template.body_html, {
nama: user.name,
platform_name: settings.brand_name,
email: user.email,
plan_name: user.plan,
dashboard_link: 'https://...',
});
const htmlBody = EmailTemplateRenderer.render({
subject: template.subject,
content: htmlContent,
brandName: settings.brand_name,
});
Migration Notes
Old Templates (Self-Contained HTML)
If you have old templates with full HTML:
Before:
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial; }
.container { max-width: 600px; }
h1 { color: #0066cc; }
</style>
</head>
<body>
<div class="container">
<h1>Welcome!</h1>
<p>Hello...</p>
</div>
</body>
</html>
After (Content-Only):
<h1>Welcome!</h1>
<p>Hello...</p>
Remove:
<!DOCTYPE html><html>,<head>,<body>tags<style>blocks- Container
<div>wrappers - Header/footer HTML
Keep:
- Content HTML only
- Shortcode placeholders
{variable} - Inline styles for special cases (rare)
Benefits
✅ Consistent Branding - All emails have same header/footer
✅ Single Source of Truth - One master template controls design
✅ Easy Updates - Change design in one place
✅ Email Client Compatible - Master template has all the fixes
✅ Less Duplication - No more reinventing styles per template
✅ Auto-Styling - .tiptap-content CSS makes content look good
Files
supabase/shared/email-template-renderer.ts- Master template + renderersupabase/functions/send-auth-otp/index.ts- Uses master templatesupabase/functions/send-notification/index.ts- Uses master templatesupabase/migrations/20250102000005_fix_auth_email_template_content_only.sql- Auth template updateemail-master-template.html- Visual reference of master template
Testing
Test Master Template
# Test with curl
curl -X POST https://lovable.backoffice.biz.id/functions/v1/send-auth-otp \
-H "Authorization: Bearer YOUR_SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id":"TEST_ID","email":"test@example.com"}'
Expected Result
Email should have:
- Black header with "ACCESS HUB" logo
- Notification ID (NOTIF #XXXXXX)
- Content styled with brutalist design
- Gray footer with unsubscribe links
- Responsive on mobile
Troubleshooting
Email Has No Styling
Cause: Template has full HTML, getting double-wrapped
Fix: Remove <html>, <head>, <body> from database template
Styling Not Applied
Cause: Content not in .tiptap-content wrapper
Fix: Master template automatically wraps {{content}} in .tiptap-content div
Broken Layout
Cause: Old template has container divs/wrappers
Fix: Remove container divs, keep only content HTML
Status: ✅ Unified system active Last Updated: 2025-01-02