# Email Templates - Usage

> How to consume the canonical templates from each Marbl product. Three integration patterns, depending on the project's runtime.

---

## Pattern A - TypeScript (Cloudflare Workers, Node services)

For Luma worker (`luma-api`), Atlas, Nura webhooks, Theia, future TS-runtime products.

### Setup

Mirror the canonical `.ts` files into your project's `src/emails/` directory. A small sync script keeps them in step.

```
your-project/
├── src/emails/
│   ├── welcome.ts              # mirror of marbl-codes/src/website/components/emails/templates/welcome.ts
│   ├── verification.ts         # mirror
│   └── ...
└── scripts/sync-emails.mjs     # one-off sync from canonical
```

Sync script (drop into `scripts/sync-emails.mjs`):

```js
import { copyFileSync } from 'node:fs';
import { join } from 'node:path';

// Point this at your local marbl-codes checkout. In CI, fix the path or
// clone marbl-codes as a step before running this script.
const SRC = process.env.MARBL_CODES_PATH
  ? join(process.env.MARBL_CODES_PATH, 'src/website/components/emails/templates')
  : '../marbl-codes/src/website/components/emails/templates';
const DEST = 'src/emails';
const templates = ['welcome.ts', 'verification.ts', 'lead-notification.ts', 'conversation-summary.ts', 'daily-digest.ts'];

for (const file of templates) {
  copyFileSync(join(SRC, file), join(DEST, file));
  console.log(`✓ ${file}`);
}
```

Run `node scripts/sync-emails.mjs` whenever the canonical updates. For CI: pin a `MARBL_TEMPLATES_VERSION` to a marbl-codes commit SHA and assert that the synced file hashes match what that commit shipped.

### Sending a welcome email

```ts
import { buildWelcomeEmail, LUMA_WELCOME } from './emails/welcome';

const { html, text } = buildWelcomeEmail(
  { ...LUMA_WELCOME, unsubscribeUrl: `https://luma.marbl.codes/unsubscribe?token=${token}` },
  user.firstName,
);

await resend.emails.send({
  from: 'Luma <hello@marbl.codes>',
  to: user.email,
  subject: `${LUMA_WELCOME.headline} | ${LUMA_WELCOME.product}`,
  html,
  text,
});
```

### Sending a verification email

```ts
import { buildVerificationEmail, LUMA_VERIFICATION } from './emails/verification';

const { html, text } = buildVerificationEmail(LUMA_VERIFICATION, user.firstName, verificationToken);

await resend.emails.send({
  from: 'Luma <hello@marbl.codes>',
  to: user.email,
  subject: `Verify your ${LUMA_VERIFICATION.product} subscription`,
  html,
  text,
});
```

### Sending a daily digest

```ts
import { buildDailyDigestEmail, LUMA_DAILY_DIGEST } from './emails/daily-digest';

const { html, text } = buildDailyDigestEmail(
  LUMA_DAILY_DIGEST,
  editions,                                    // [{ edition, headline, slug }, ...]
  articles,                                    // [{ title, slug, category }, ...]
  subscriber.firstName,
  `https://luma.marbl.codes/unsubscribe?token=${unsubToken}`,
);
```

---

## Pattern B - Node ESM pipelines (.mjs)

For Luma's GitHub Actions pipelines (`scripts/pipelines/`) which run as `.mjs`.

Node 24 strips TypeScript types natively. Vendor a copy of the canonical `.ts` files into the pipeline's working directory (same approach as Pattern A), then import locally:

```js
import { pathToFileURL } from 'node:url';
import { join } from 'node:path';

const tpl = (name) => pathToFileURL(join(import.meta.dirname, 'email-templates', name)).href;
const { buildDailyDigestEmail, LUMA_DAILY_DIGEST } = await import(tpl('daily-digest.ts'));

const { html, text } = buildDailyDigestEmail(LUMA_DAILY_DIGEST, editions, articles, firstName, unsubUrl);
```

This is exactly the pattern used by `marbl-codes/scripts/render-email-samples.mjs` against the canonical templates in this repo - reference implementation worth reading.

Cross-platform note: always go through `pathToFileURL()` rather than hand-building a `file://` string. Windows paths with drive letters fail otherwise.

---

## Pattern C - HTML with placeholder substitution

For Subscribe (`templates/email/*.html`) and any project that ships pre-rendered HTML with `{{variable}}` placeholders rather than calling a build function.

### Approach

1. Start with the rendered sample: `marbl-codes/src/website/components/emails/samples/<template>.html`
2. Replace the mock content with `{{placeholder}}` tokens
3. Drop into your `templates/email/` directory
4. Substitute at send time

### Example - welcome.html for Subscribe

Take `samples/luma-welcome.html`, swap mock data for placeholders:

```html
<!-- before (sample) -->
<h1 style="...">Boom. You're verified.</h1>
<p style="...">Hey Richard - You picked the right signal...</p>

<!-- after (Subscribe template) -->
<h1 style="...">{{headline}}</h1>
<p style="...">Hey {{firstName}} - {{subtext}}</p>
```

At send time, substitute the tokens with the same values you'd pass to `LUMA_WELCOME` if you were on Pattern A.

### When to use Pattern C vs Pattern A

- Use Pattern C if your project is genuinely HTML-only (no Node runtime that could evaluate `.ts`)
- Use Pattern A everywhere else - the typed config is much harder to drift

Most projects should be on Pattern A. Pattern C exists because Subscribe was built before the canonical did.

---

## Migration checklist for an existing consumer

When migrating a project that has its own copy of these templates:

1. **Diff against canonical** - run a literal file compare to see what's drifted
2. **Adopt the brand rules** - check `BRAND-RULES.md`; the most common drifts are beige body text (`#F3E2C8` should be `rgba(255, 255, 255, 0.7)`) and 8px button radii (should be 12px)
3. **Drop the icons** - all emoji + unicode + CTA arrows go
4. **Verify configs** - if the project has product-specific config tweaks (custom CTAs, alt subjects), promote them to the canonical as a new export (e.g. `THEIA_WELCOME`) rather than keeping a local-only variant
5. **Replace the local file** - either via the sync script above, or just `cp` from the canonical
6. **Render & visually compare** - render the new template with realistic mock data and eyeball against the live preview
7. **Send a test** - to a real Gmail + Apple Mail + Outlook inbox before flipping production traffic

## Known drift to fix

| Consumer | File | Status |
|----------|------|--------|
| Luma worker | `workers/luma-api/src/emails/welcome.ts` | Drifted - still has icons + beige text |
| Luma worker | `workers/luma-api/src/emails/verification.ts` | Drifted - check brand-rules pass |
| Luma pipeline | `scripts/pipelines/email-templates/daily-digest.mjs` | Drifted - pre-pill version |
| Atlas | `src/emails/welcome.ts` | Drifted - pre lock-in |
| Atlas | `src/emails/verification.ts` | Drifted - pre lock-in |
| Theia | `site/src/emails/verification.ts` | Drifted - pre lock-in |
| Nura | `src/website/functions/api/emails/lead-notification.ts` | Drifted - pre lock-in |
| Nura | `src/website/functions/api/emails/conversation-summary.ts` | Drifted - pre lock-in |
| Subscribe | `templates/email/welcome.html` | Pattern C - separate update path |

Sweep these in a single dedicated session - don't migrate one-at-a-time; the brand discontinuity is the bigger cost.
