Files
meet-hub/EMAIL-TEMPLATE-SYSTEM.md

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-content wrapper 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 + renderer
  • supabase/functions/send-auth-otp/index.ts - Uses master template
  • supabase/functions/send-notification/index.ts - Uses master template
  • supabase/migrations/20250102000005_fix_auth_email_template_content_only.sql - Auth template update
  • email-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