# Adding an Email Template

> **[Docs Overview](../overview.md)** / [Email](./README.md) / Adding an Email Template

Recipe for adding or modifying a transactional email. For provider configuration (Resend vs SMTP, env vars, MailHog) read [EMAIL_SERVICE.md](../EMAIL_SERVICE.md) first - that's not repeated here.

---

## Overview

Templates in this codebase are **plain TypeScript functions that return HTML and plain-text strings.** There's no JSX-email, no MJML, no Handlebars. Each template ships two functions: `generateXxxEmailHtml()` and `generateXxxEmailText()`. The email service (`lib/email/index.ts`) is provider-agnostic - you call `sendEmail({ to, subject, html, text })` and it routes through whichever provider `EMAIL_PROVIDER` selects.

**File layout:**

```
lib/email/
  index.ts                      # sendEmail() entry point
  types.ts                      # EmailMessage, EmailResult, EmailProvider
  providers/
    resend.ts                   # Resend API provider
    smtp.ts                     # SMTP provider (used with MailHog locally)
  templates/
    verification-email.ts       # reference template (read first)
    password-reset-email.ts     # reference template (read first)
```

Read `lib/email/templates/verification-email.ts` end-to-end before writing a new template - it's ~120 lines and covers the whole shape.

---

## Step 1: Create the template file

**File:** `lib/email/templates/<name>-email.ts`

Naming convention: kebab-case, always ends in `-email.ts`. Export exactly two functions and a params interface:

```typescript
// lib/email/templates/welcome-email.ts

export interface WelcomeEmailParams {
  dealerName: string;
  dashboardUrl: string;
  baseUrl: string;
}

export function generateWelcomeEmailHtml({
  dealerName,
  dashboardUrl,
  baseUrl,
}: WelcomeEmailParams): string {
  const logoUrl = `${baseUrl}/images/logo/amsoil-logo.png`;

  return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Welcome to AMSOIL DLP</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #f4f4f5;">
  <table role="presentation" style="width: 100%; border-collapse: collapse;">
    <tr>
      <td align="center" style="padding: 40px 20px;">
        <table role="presentation" style="width: 100%; max-width: 600px; border-collapse: collapse; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
          <tr>
            <td style="padding: 40px 40px 20px; text-align: center; background-color: #020618; border-radius: 8px 8px 0 0;">
              <img src="${logoUrl}" alt="AMSOIL" style="width: 150px; height: auto;" />
            </td>
          </tr>
          <tr>
            <td style="padding: 40px;">
              <h1 style="margin: 0 0 20px; font-size: 24px; font-weight: 600; color: #1f2937; text-align: center;">
                Welcome, ${dealerName}
              </h1>
              <p style="margin: 0 0 20px; font-size: 16px; line-height: 1.6; color: #4b5563; text-align: center;">
                Your dealer landing page is ready. Head to the dashboard to customize it.
              </p>
              <table role="presentation" style="width: 100%; border-collapse: collapse;">
                <tr>
                  <td align="center" style="padding: 20px 0;">
                    <a href="${dashboardUrl}"
                       style="display: inline-block; padding: 14px 32px; background-color: #38bdf8; color: #020618; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 8px;">
                      Go to Dashboard
                    </a>
                  </td>
                </tr>
              </table>
            </td>
          </tr>
          <tr>
            <td style="padding: 20px 40px; background-color: #f9fafb; border-radius: 0 0 8px 8px; border-top: 1px solid #e5e7eb;">
              <p style="margin: 0; font-size: 12px; line-height: 1.5; color: #9ca3af; text-align: center;">
                AMSOIL Dealer Landing Pages<br />
                This is an automated message. Please do not reply.
              </p>
            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>
</body>
</html>
  `.trim();
}

export function generateWelcomeEmailText({ dealerName, dashboardUrl }: WelcomeEmailParams): string {
  return `
Welcome, ${dealerName}

Your AMSOIL dealer landing page is ready. Visit your dashboard to customize it:

${dashboardUrl}

---
AMSOIL Dealer Landing Pages
This is an automated message. Please do not reply.
  `.trim();
}
```

Copy the header/footer structure from `verification-email.ts` exactly - it's tuned for Gmail, Outlook, and Apple Mail. The outer `<table>` structure is required for consistent rendering across clients.

---

## Step 2: Wire into the email service

The email service doesn't "pick up" templates automatically - there is no registry. You call the template functions directly from the code path that needs to send the email, then pass the result to `sendEmail`:

```typescript
// e.g. inside an API route or service function
import { sendEmail } from '@/lib/email';
import {
  generateWelcomeEmailHtml,
  generateWelcomeEmailText,
} from '@/lib/email/templates/welcome-email';

const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
const params = {
  dealerName: dealer.businessName,
  dashboardUrl: `${baseUrl}/dashboard`,
  baseUrl,
};

const result = await sendEmail({
  to: user.email,
  subject: 'Welcome to AMSOIL Dealer Landing Pages',
  html: generateWelcomeEmailHtml(params),
  text: generateWelcomeEmailText(params),
});

if (!result.success) {
  authLogger.error({ err: result.error, email: user.email }, '[Welcome] send failed');
  // Don't throw — email failure shouldn't block the primary flow unless email IS the flow
  // (e.g., password reset). See app/api/auth/register/route.ts for a case where it does block.
}
```

`app/api/auth/register/route.ts` and `app/api/auth/forgot-password/route.ts` are the reference call sites - read one of them to see logging and error handling in practice.

---

## Step 3: Local testing with MailHog

MailHog is included in `docker-compose.yml` and runs on every local dev bring-up. It's the configured destination for all outgoing mail when `EMAIL_PROVIDER=smtp`.

```bash
# In .env.local
EMAIL_PROVIDER=smtp
SMTP_HOST=mailhog
SMTP_PORT=1025
```

Then:

```bash
# Bring the stack up (MailHog is part of it)
npm run power-cycle

# Open the MailHog web UI to view captured mail:
open http://localhost:8025
```

Trigger the email by exercising the real code path - e.g., register a user to send the verification email, or hit the forgot-password endpoint. **There is no `/dev/email/preview` route and no standalone template preview harness.** If you need a tighter loop during template development, write a one-off script or a Jest snapshot that invokes `generateXxxEmailHtml()` and pipes the output to a file, then open it in a browser.

**MailHog gotchas:**

- Emails don't leave the container. If MailHog shows nothing, check (a) `EMAIL_PROVIDER=smtp` in `.env.local`, (b) `SMTP_HOST=mailhog` (the Docker service name, not `localhost`) when running inside the Next.js container, or `SMTP_HOST=localhost` if running Next.js outside Docker.
- Restart the Next.js process after changing `.env.local`. The email provider is cached (`cachedProvider` in `lib/email/index.ts`).

---

## Step 4: Production considerations

- **Provider:** Production uses Resend (`EMAIL_PROVIDER=resend`). Set `RESEND_API_KEY` in the deployed environment. The `from` address defaults to `EMAIL_FROM`, which must match a verified domain in Resend.
- **Rate limiting:** The email service has no built-in throttle. Outbound bursts are bounded by Resend's per-key quota. For user-triggered flows (password reset, verification), throttle upstream - see the `sendCount` / `lastSentAt` pattern on `PendingRegistration` in `prisma/schema.prisma` and the register route.
- **Unsubscribe:** Transactional mail (verification, password reset, welcome) doesn't require `List-Unsubscribe` headers, but **any marketing email does.** The codebase doesn't currently send marketing email; if you add one, wire List-Unsubscribe and a suppression list before shipping.
- **Spam checks:** Inline all CSS, keep HTML/text size under 100KB, use a logo URL that resolves to a reachable HTTPS endpoint (AMSOIL logo is served from `${baseUrl}/images/logo/amsoil-logo.png`), and avoid spammy subject lines (no ALL CAPS, no excessive punctuation). Test deliverability with [mail-tester.com](https://www.mail-tester.com/) before broad rollout.
- **SPF/DKIM/DMARC:** Configured at the Resend + Cloudflare DNS level for `amsoil.aimclear.com`. Not something template code touches.

---

## Template variables

Dynamic content is plain template literal interpolation - `${variable}` inside the returned string. No templating engine, no auto-escaping.

**This means escaping is your job.** Any value that could contain user-provided content (dealer name, business name, etc.) should be passed through `escapeHtml()` before interpolation if it's being inserted into the HTML body:

```typescript
function escapeHtml(value: string): string {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

// Use in template
<h1>${escapeHtml(dealerName)}</h1>
```

The existing `verification-email.ts` and `password-reset-email.ts` templates don't escape because their only interpolations are URLs they generate themselves (`verificationUrl`, `resetUrl`) - no user-controlled strings reach the HTML. **If you interpolate user-controlled strings, you must escape them.** URLs inside `href=""` should additionally be validated with `validateCallbackUrl()` from `lib/url-validation.ts` if they come from external input.

---

## Pitfalls

- **Missing plain-text fallback.** Always provide `text` alongside `html`. Some clients strip HTML; spam filters down-weight HTML-only messages. Every existing template ships a plain-text version - follow suit.
- **Tailwind / external CSS.** Email clients don't load external stylesheets and mostly ignore `<style>` tags. Use inline `style=""` on every element that needs styling. This is why the existing templates are verbose.
- **Relative URLs.** Links in email must be absolute. Pass `baseUrl` (normally `process.env.NEXTAUTH_URL`) through the template params and build absolute URLs with it. Never use `/path` links.
- **Unverified `from` address.** In production Resend rejects sends from unverified domains. `EMAIL_FROM` must match a domain verified in the Resend dashboard.
- **Accessibility.** Add `alt=""` to every `<img>`. Use `role="presentation"` on layout tables (already done in the reference templates). Prefer dark-on-light text for readability; don't rely on color alone to convey meaning.
- **HTML email "quirks mode."** Use a full `<!DOCTYPE html>` declaration and a `<meta charset="UTF-8">` - Outlook especially misrenders without them.
- **Logo path.** `${baseUrl}/images/logo/amsoil-logo.png` is the canonical path. Don't hotlink external images.

---

## See Also

- [EMAIL_SERVICE.md](../EMAIL_SERVICE.md) - provider config, switching providers, env vars
- [EMAIL_NORMALIZATION.md](../EMAIL_NORMALIZATION.md) - normalizing the recipient address
- [email/README.md](./README.md) - email section landing page
- `lib/email/templates/verification-email.ts` - the canonical reference template
