Rework email template
This commit is contained in:
parent
77e35c26c7
commit
6e35717072
9 changed files with 202 additions and 42 deletions
|
|
@ -1,7 +1,14 @@
|
|||
MAILSHIP_SECRET=
|
||||
MAILSHIP_NAME=Mailship
|
||||
MAILSHIP_NAME=Flotilla
|
||||
MAILSHIP_URL=http://localhost:4738
|
||||
BASE_URL=http://localhost:4738
|
||||
# Branding used in email templates. EVENT_VIEWER_URL is the base URL of the
|
||||
# app you link events into (Flotilla by default, or anything handling the same
|
||||
# /spaces/<relay>/<group> and /<nevent> URL shapes).
|
||||
EVENT_VIEWER_URL=https://app.flotilla.social
|
||||
BRAND_NAME=Flotilla
|
||||
BRAND_ACCENT=#7161FF
|
||||
BRAND_LOGO=
|
||||
INDEXER_RELAYS=purplepag.es,relay.damus.io,relay.nostr.band
|
||||
DEFAULT_RELAYS=relay.damus.io,nos.lol
|
||||
SEARCH_RELAYS=relay.nostr.band
|
||||
|
|
|
|||
11
AGENTS.md
11
AGENTS.md
|
|
@ -23,6 +23,17 @@ bd close <id> # Complete work
|
|||
bd dolt push # Push beads data to remote
|
||||
```
|
||||
|
||||
## Email Template Preview
|
||||
|
||||
To preview the rendered digest email while iterating on `src/emails/digest.mjml`:
|
||||
|
||||
```bash
|
||||
pnpm run preview:digest # renders to digest-preview.html
|
||||
open digest-preview.html
|
||||
```
|
||||
|
||||
After changing the template, rerun the script (or it doesn't watch) and refresh the browser.
|
||||
|
||||
## Non-Interactive Shell Commands
|
||||
|
||||
**ALWAYS use non-interactive flags** with file operations to avoid hanging on confirmation prompts.
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -30,6 +30,10 @@ Flotilla ──HTTP──▶ Mailship (POST /subscription/email)
|
|||
| `MAILSHIP_NAME` | ✓ | Name of this Mailship instance |
|
||||
| `MAILSHIP_URL` | ✓ | Public URL of this instance |
|
||||
| `BASE_URL` | ✓ | Base URL for callback URLs (same as MAILSHIP_URL typically) |
|
||||
| `EVENT_VIEWER_URL` | | Base URL of the app event links open in (defaults to Flotilla at `https://app.flotilla.social`, or anything handling the same `/spaces/<relay>/<hash>` and `/<nevent>` URL shapes) |
|
||||
| `BRAND_NAME` | | Name used in email branding (default: `Flotilla`) |
|
||||
| `BRAND_ACCENT` | | Accent color string used in email branding (default: `#7161FF`) |
|
||||
| `BRAND_LOGO` | | URL of the logo image shown in the email header. Defaults to `<EVENT_VIEWER_URL>/logo.png`; if empty/unset, a colored brand name is shown instead |
|
||||
| `POSTMARK_API_KEY` | ✓ | Postmark API key for sending emails |
|
||||
| `POSTMARK_SENDER_ADDRESS` | ✓ | Verified sender email in Postmark |
|
||||
| `DEFAULT_RELAYS` | ✓ | Comma-separated list of default relays |
|
||||
|
|
@ -79,6 +83,16 @@ pnpm run build
|
|||
pnpm run start
|
||||
```
|
||||
|
||||
### Previewing the digest email
|
||||
|
||||
To iterate on the email template, render it with sample data and open the
|
||||
result in your browser:
|
||||
|
||||
```sh
|
||||
pnpm run preview:digest
|
||||
open digest-preview.html
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
### Quick start
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"check": "tsc --noEmit && eslint src",
|
||||
"format": "prettier --write \"src/**/*.{ts,js,json,html}\"",
|
||||
"start": "node dist/index.js",
|
||||
"preview:digest": "node script/render-preview.mjs",
|
||||
"run-alert": "node dist/run.js",
|
||||
"test": "bash test/integration.sh",
|
||||
"test:server": "bash test/integration.sh --server-only"
|
||||
|
|
|
|||
49
script/render-preview.mjs
Normal file
49
script/render-preview.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import {readFileSync, writeFileSync} from 'fs'
|
||||
import {fileURLToPath} from 'url'
|
||||
import {dirname, join} from 'path'
|
||||
import Mustache from 'mustache'
|
||||
import mjml2html from 'mjml'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const templatePath = join(__dirname, '..', 'src', 'emails', 'digest.mjml')
|
||||
|
||||
const sample = {
|
||||
name: 'alice',
|
||||
brandName: 'Flotilla',
|
||||
brandAccent: '#7161FF',
|
||||
brandLogo: 'https://app.flotilla.social/logo.png',
|
||||
UserName: 'Alice',
|
||||
Duration: '24 hours',
|
||||
Total: 12,
|
||||
TopProfiles: 'bob, carol',
|
||||
HasPopular: true,
|
||||
Popular: [
|
||||
{
|
||||
Link: 'https://app.flotilla.social/nevent1qqs...',
|
||||
Timestamp: 'Aug 25, 2026 at 9:00 AM',
|
||||
Icon: 'https://i.pravatar.cc/150?img=32',
|
||||
Name: 'carol',
|
||||
Content: '<p>Does anyone know how relay-based groups work?</p>',
|
||||
Replies: 5,
|
||||
Reactions: 8,
|
||||
},
|
||||
{
|
||||
Link: 'https://app.flotilla.social/spaces/nos.lol/community?at=1700000000',
|
||||
Timestamp: 'Aug 25, 2026 at 10:00 AM',
|
||||
Icon: 'https://i.pravatar.cc/150?img=68',
|
||||
Name: 'bob',
|
||||
Content:
|
||||
'<p>Excited to share our new community space on Flotilla! <a href="https://flotilla.social">Check it out</a>.</p>',
|
||||
Replies: 14,
|
||||
Reactions: 32,
|
||||
},
|
||||
],
|
||||
unsubscribeUrl: 'https://app.flotilla.social/unsubscribe?token=abc123',
|
||||
}
|
||||
|
||||
const source = readFileSync(templatePath, 'utf8')
|
||||
const {html} = mjml2html(Mustache.render(source, sample))
|
||||
const outPath = join(__dirname, '..', 'digest-preview.html')
|
||||
|
||||
writeFileSync(outPath, html)
|
||||
console.log(`Rendered preview written to ${outPath}`)
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { parse, truncate, renderAsHtml } from '@welshman/content'
|
||||
import {
|
||||
TrustedEvent,
|
||||
normalizeRelayUrl,
|
||||
getParentId,
|
||||
getIdFilters,
|
||||
getReplyFilters,
|
||||
|
|
@ -21,6 +22,7 @@ import {
|
|||
REACTION,
|
||||
displayProfile,
|
||||
displayPubkey,
|
||||
getTagValue,
|
||||
} from '@welshman/util'
|
||||
import { Loader, AdapterContext, makeLoader, SocketAdapter } from '@welshman/net'
|
||||
import { Router, addMinimalFallbacks } from '@welshman/router'
|
||||
|
|
@ -28,6 +30,7 @@ import { call } from '@welshman/lib'
|
|||
import { displayDuration, createElement } from './util.js'
|
||||
import type { Subscription } from './alert.js'
|
||||
import { sendDigest } from './mailer.js'
|
||||
import { EVENT_VIEWER_URL } from './env.js'
|
||||
import {
|
||||
profilesByPubkey,
|
||||
loadRelaySelections,
|
||||
|
|
@ -48,7 +51,7 @@ export class Digest {
|
|||
|
||||
loadHandler = async () => {
|
||||
// Default handler for building event links
|
||||
return 'https://coracle.social/'
|
||||
return `${EVENT_VIEWER_URL}/`
|
||||
}
|
||||
|
||||
buildParameters = async (data: DigestData) => {
|
||||
|
|
@ -73,7 +76,8 @@ export class Digest {
|
|||
const handler = await this.loadHandler()
|
||||
const repliesByParentId = groupBy(getParentId, context)
|
||||
const eventsByPubkey = groupBy((e) => e.pubkey, events)
|
||||
const popular = sortBy((e) => -(repliesByParentId.get(e.id)?.length || 0), events).slice(0, 12)
|
||||
const userProfile = profilesByPubkey.get().get(this.sub.pubkey)
|
||||
const sorted = sortBy((e) => e.created_at, events).slice(0, 100)
|
||||
const topProfiles = sortBy(
|
||||
([k, ev]) => -ev.length,
|
||||
Array.from(eventsByPubkey.entries()).filter(([k]) => profilesByPubkey.get().get(k))
|
||||
|
|
@ -82,8 +86,9 @@ export class Digest {
|
|||
return {
|
||||
Total: events.length,
|
||||
Duration: displayDuration(Math.floor(Date.now() / 1000) - this.since),
|
||||
Popular: popular.map((e) => getEventVariables(e)),
|
||||
HasPopular: popular.length > 0,
|
||||
Popular: sorted.map((e) => getEventVariables(e)),
|
||||
HasPopular: sorted.length > 0,
|
||||
UserName: displayProfile(userProfile, this.sub.email.split('@')[0]),
|
||||
TopProfiles: displayList(topProfiles.map(([pk]) => displayProfileByPubkey(pk))),
|
||||
}
|
||||
}
|
||||
|
|
@ -94,6 +99,7 @@ export class Digest {
|
|||
|
||||
// Load profiles for event authors
|
||||
const pubkeys = new Set(events.map(e => e.pubkey))
|
||||
pubkeys.add(this.sub.pubkey)
|
||||
for (const pk of pubkeys) {
|
||||
try {
|
||||
await loadProfile(pk)
|
||||
|
|
@ -114,6 +120,14 @@ export class Digest {
|
|||
|
||||
const buildLink = (event: TrustedEvent, handler: string) => {
|
||||
const relays = Router.get().Event(event).getUrls()
|
||||
const groupId = getTagValue(["h"], event.tags)
|
||||
|
||||
if (groupId && relays.length > 0) {
|
||||
const relay = encodeRelay(relays[0])
|
||||
const nevent = neventEncode({ id: event.id, relays })
|
||||
return `${handler}spaces/${relay}/${encodeURIComponent(groupId)}?at=${event.created_at}#${nevent}`
|
||||
}
|
||||
|
||||
const nevent = neventEncode({ ...event, relays })
|
||||
|
||||
if (handler.includes('<bech32>')) {
|
||||
|
|
@ -123,6 +137,13 @@ const buildLink = (event: TrustedEvent, handler: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
const encodeRelay = (url: string) =>
|
||||
encodeURIComponent(
|
||||
normalizeRelayUrl(url)
|
||||
.replace(/^wss:\/\//, '')
|
||||
.replace(/\/$/, ''),
|
||||
)
|
||||
|
||||
const displayProfileByPubkey = (pubkey: string) =>
|
||||
displayProfile(profilesByPubkey.get().get(pubkey), displayPubkey(pubkey))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +1,52 @@
|
|||
<mjml>
|
||||
<mj-head>
|
||||
<mj-title>New Activity</mj-title>
|
||||
<mj-title>New activity on Flotilla</mj-title>
|
||||
<mj-font name="Inter" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700" />
|
||||
<mj-style inline="inline">
|
||||
.header { font-size: 24px; font-weight: bold; }
|
||||
.subheader { font-size: 16px; color: #555; }
|
||||
.event-item { margin-bottom: 20px; border-left: 3px solid #F45E43; padding-left: 10px; }
|
||||
.header { font-family: Inter, Helvetica, Arial, sans-serif; font-size: 24px; font-weight: 700; }
|
||||
.subheader { font-family: Inter, Helvetica, Arial, sans-serif; font-size: 15px; color: #64748b; line-height: 1.5; }
|
||||
.event-item { margin-bottom: 20px; border-left: 3px solid #7161FF; padding-left: 12px; }
|
||||
.event-meta { margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.event-meta-left { display: flex; align-items: center; }
|
||||
.event-author { font-weight: bold; margin-right: 4px; }
|
||||
.event-content { white-space: pre-wrap; }
|
||||
.event-timestamp { color: #777; font-size: 12px; }
|
||||
.event-link { font-size: 12px; }
|
||||
.section-header { font-size: 18px; font-weight: bold; margin-top: 15px; }
|
||||
.event-author { font-family: Inter, Helvetica, Arial, sans-serif; font-weight: 600; margin-right: 4px; color: #1e293b; }
|
||||
.event-content { font-family: Inter, Helvetica, Arial, sans-serif; white-space: pre-wrap; color: #334155; font-size: 14px; }
|
||||
.event-timestamp { color: #94a3b8; font-size: 12px; }
|
||||
.event-link { font-family: Inter, Helvetica, Arial, sans-serif; font-size: 13px; font-weight: 600; }
|
||||
.event-link a { color: {{brandAccent}}; text-decoration: none; }
|
||||
.section-header { font-family: Inter, Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 700; color: #1e293b; margin-top: 15px; }
|
||||
.profile-image { width: 20px; height: 20px; border-radius: 50%; margin-right: 3px; vertical-align: middle; }
|
||||
.event-stats { margin-top: 8px; color: #666; font-size: 14px; }
|
||||
.user-photo { width: 32px; height: 32px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
|
||||
.event-stats { margin-top: 8px; color: #64748b; font-size: 13px; }
|
||||
.stat-item { display: inline-flex; align-items: center; margin-right: 12px; }
|
||||
.stat-icon { width: 16px; height: 16px; margin-right: 4px; vertical-align: middle; }
|
||||
.footer { font-family: Inter, Helvetica, Arial, sans-serif; color: #94a3b8; font-size: 12px; line-height: 1.5; }
|
||||
.logo { max-width: 48px; max-height: 48px; }
|
||||
a { text-decoration: none; }
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section background-color="#f0f0f0" padding="20px">
|
||||
<mj-body background-color="#f0f2f5">
|
||||
<mj-section background-color="#ffffff" padding="32px 32px 8px 32px">
|
||||
<mj-column>
|
||||
<mj-text css-class="header">Hello {{name}},</mj-text>
|
||||
<mj-text css-class="subheader">
|
||||
Below is a summary of activity over the last {{Duration}}.
|
||||
We found {{Total}} new posts from {{TopProfiles}}.
|
||||
{{#brandLogo}}
|
||||
<mj-image src="{{brandLogo}}" alt="{{brandName}}" align="center" width="40px" padding="0 0 8px 0" />
|
||||
{{/brandLogo}}
|
||||
<mj-text css-class="header" align="center">
|
||||
<span style="color: {{brandAccent}};">{{brandName}}</span>
|
||||
</mj-text>
|
||||
<mj-divider border-color="#cccccc" />
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section background-color="#ffffff" padding="8px 32px 24px 32px">
|
||||
<mj-column>
|
||||
<mj-text css-class="subheader" align="center">
|
||||
{{UserName}}, here's what's happening in your communities.
|
||||
</mj-text>
|
||||
<mj-divider border-color="#e2e8f0" />
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
{{#HasPopular}}
|
||||
<mj-text css-class="section-header">Most Popular</mj-text>
|
||||
{{#HasPopular}}
|
||||
<mj-section background-color="#ffffff" padding="0 32px 24px 32px">
|
||||
<mj-column>
|
||||
<mj-text css-class="section-header">Latest Activity</mj-text>
|
||||
{{#Popular}}
|
||||
<mj-text>
|
||||
<div class="event-item">
|
||||
|
|
@ -41,32 +57,33 @@
|
|||
<span class="event-timestamp">at {{Timestamp}}</span>
|
||||
</div>
|
||||
<div class="event-link">
|
||||
<a href="{{Link}}">View post</a>
|
||||
<a href="{{Link}}">View in {{brandName}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="event-content">{{{Content}}}</div>
|
||||
<div class="event-stats">
|
||||
<span class="stat-item">
|
||||
<img class="stat-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAASFBMVEUAAAAiIkQdJ04bJkwbKU0dJkscKEwbJ00cKEsdJkwcJ0wbKEscJkwcJ0scJ00bJ00dJk0bJ0wcJkwcKE0cJ0wcJ0wcJ0z///8ZxKH0AAAAF3RSTlMADxovOD1AQlJXW19ld4iWoKizwenq9eQPMKMAAAABYktHRBcL1piPAAAAOElEQVQY02NgoBZg40fls4qw4+WziAnxggA3I1xAmA8EBEU5YEaKsoFpZlEmNBEBHjRruTip4wsADUgCEGuuAxQAAAAASUVORK5CYII=" />
|
||||
{{Replies}}
|
||||
{{Replies}} replies
|
||||
</span>
|
||||
<span class="stat-item">
|
||||
<img class="stat-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAn1BMVEUAAAAAAIAAAFUzMzMrK1UkJEkVK1UkJEkiIkQgIFAeLUscK0cbKFEbJEkaI08aK00ZKUocJUobKU0bKEwaKEsaJ04bJ0sbJ0sdJ0wcJ00cJk0cJkwcJk0bKEwbKEscKEwcJ00cJ0wcJ0wcJ0sbJk0bJ0wcJ0wcJ0sbJ0wdJ0wcJ00cKE0cJ0wbJ0wbJ0scJ0wcJ0wcJ0wcJ0wcJ0z///++FK4PAAAANHRSTlMAAgMFBgcMDg8QERITHB0eHzc4OTo7QVVhY2RleHl6h4iJiouMnq+wsbK+wcLD3+Dh4vHyB17REwAAAAFiS0dENKmx6f0AAACTSURBVBgZBcEJQoJAAADAIUhQkjxKJZdKyzPLXfj/35oBPDZNAYCXy5BSf14CHr7um5KyjfsMfP9MgPq2h9f7BKBOC1w2ALydKIaSLtAFqj43+0MIhID4bBYBIDWKoQJg3OecW9aH1eqwZnvEMtZG79drN1KnOexuNfD0+wGyXWwrxtv0mQEWpz6l/jgHQD6d5sA/VqYMPJkJl3UAAAAASUVORK5CYII=" />
|
||||
{{Reactions}}
|
||||
{{Reactions}} reactions
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</mj-text>
|
||||
{{/Popular}}
|
||||
<mj-divider border-color="#cccccc" />
|
||||
{{/HasPopular}}
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
{{/HasPopular}}
|
||||
|
||||
<mj-text>
|
||||
You're receiving this email because you subscribed to notifications.
|
||||
We'll continue to send you updates based on your subscription preferences.
|
||||
<mj-section background-color="#ffffff" padding="0 32px 24px 32px">
|
||||
<mj-column>
|
||||
<mj-divider border-color="#e2e8f0" />
|
||||
<mj-text css-class="footer">
|
||||
You're receiving this email because you subscribed to {{brandName}} notifications.
|
||||
We'll send you updates based on your subscription preferences.
|
||||
</mj-text>
|
||||
|
||||
<mj-button background-color="#F45E43" color="#ffffff" href="{{unsubscribeUrl}}">
|
||||
<mj-button background-color="{{brandAccent}}" color="#ffffff" href="{{unsubscribeUrl}}" padding-top="16px">
|
||||
Unsubscribe
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ if (!process.env.BASE_URL) throw new Error('BASE_URL is not defined.')
|
|||
export const MAILSHIP_URL = process.env.MAILSHIP_URL
|
||||
export const MAILSHIP_NAME = process.env.MAILSHIP_NAME
|
||||
export const BASE_URL = process.env.BASE_URL
|
||||
export const EVENT_VIEWER_URL = process.env.EVENT_VIEWER_URL || 'https://app.flotilla.social'
|
||||
export const BRAND_ACCENT = process.env.BRAND_ACCENT || '#7161FF'
|
||||
export const BRAND_NAME = process.env.BRAND_NAME || 'Flotilla'
|
||||
export const BRAND_LOGO =
|
||||
process.env.BRAND_LOGO || `${EVENT_VIEWER_URL.replace(/\/$/, '')}/logo.png`
|
||||
export const appSigner = Nip01Signer.fromSecret(process.env.MAILSHIP_SECRET)
|
||||
export const DEFAULT_RELAYS = process.env.DEFAULT_RELAYS.split(',').map(normalizeRelayUrl)
|
||||
export const INDEXER_RELAYS = process.env.INDEXER_RELAYS.split(',').map(normalizeRelayUrl)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import nodemailer from 'nodemailer'
|
||||
import { SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM, MAILSHIP_NAME, BASE_URL } from './env.js'
|
||||
import {
|
||||
SMTP_HOST,
|
||||
SMTP_PORT,
|
||||
SMTP_USER,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_FROM,
|
||||
BASE_URL,
|
||||
EVENT_VIEWER_URL,
|
||||
BRAND_ACCENT,
|
||||
BRAND_NAME,
|
||||
BRAND_LOGO,
|
||||
} from './env.js'
|
||||
import type { Subscription } from './alert.js'
|
||||
import { render } from './templates.js'
|
||||
|
||||
|
|
@ -23,11 +34,32 @@ export const sendConfirm = (sub: Subscription) => {
|
|||
.sendMail({
|
||||
from: SMTP_FROM,
|
||||
to: sub.email,
|
||||
subject: 'Confirm your email digest',
|
||||
subject: `Confirm your ${BRAND_NAME} digest`,
|
||||
html: `
|
||||
<h3>Welcome to ${MAILSHIP_NAME}!</h3>
|
||||
<p>Please confirm that you would like to receive ${sub.frequency} digests by clicking the link below:</p>
|
||||
<p><a href="${href}">Confirm Digest</a></p>
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#f0f2f5;padding:32px 16px;font-family:Inter,Helvetica,Arial,sans-serif;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:480px;background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e2e8f0;">
|
||||
<tr>
|
||||
<td style="padding:28px 32px 8px 32px;text-align:center;">
|
||||
${BRAND_LOGO ? `<img src="${BRAND_LOGO}" alt="${BRAND_NAME}" style="height:40px;width:auto;max-width:200px;object-fit:contain;" />` : `<h1 style="margin:0;font-size:22px;color:#1e293b;font-weight:700;"><span style="color:${BRAND_ACCENT};">${BRAND_NAME}</span></h1>`}
|
||||
<p style="margin:4px 0 0;color:#64748b;font-size:14px;">Digest notifications</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:16px 32px;color:#334155;font-size:15px;line-height:1.6;">
|
||||
<p style="margin:0 0 12px;">Welcome to ${BRAND_NAME}!</p>
|
||||
<p style="margin:0 0 12px;">Please confirm that you would like to receive ${sub.frequency} digests by clicking the button below:</p>
|
||||
<p style="margin:0;text-align:center;">
|
||||
<a href="${href}" style="display:inline-block;background:${BRAND_ACCENT};color:#ffffff;padding:12px 28px;border-radius:8px;font-weight:600;text-decoration:none;">Confirm Digest</a>
|
||||
</p>
|
||||
<p style="margin:16px 0 0;font-size:12px;color:#94a3b8;">Or visit: <a href="${href}" style="color:${BRAND_ACCENT};">${EVENT_VIEWER_URL}</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`,
|
||||
text: `Please confirm that you would like to receive ${sub.frequency} digests by visiting: ${href}`,
|
||||
})
|
||||
|
|
@ -51,6 +83,9 @@ export const sendDigest = async (sub: Subscription, variables: Record<string, an
|
|||
...variables,
|
||||
name: sub.email.split('@')[0],
|
||||
unsubscribeUrl: `${BASE_URL}/unsubscribe?token=${sub.key}`,
|
||||
brandName: BRAND_NAME,
|
||||
brandAccent: BRAND_ACCENT,
|
||||
brandLogo: BRAND_LOGO,
|
||||
}),
|
||||
})
|
||||
.catch(error => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue