v1.2.0: Broadcast emails, webhook admin UI, dynamic sitemap, scripture index
Publish Container / docker (push) Successful in 14m11s

- Admin > Audience > Broadcast Email: compose and send one-off HTML emails
  to all subscribers via Resend batch API; {{name}} personalization; confirm step
- Admin > Configure > Webhooks: CRUD UI for managing POST event webhooks;
  backend GET/POST/DELETE /api/admin-webhooks routes wired to existing state.webhooks
- Sitemap: dynamically appends released study lesson URLs (/study/:slug/:id)
  and episode highlight URLs (/episodes/:id) to /sitemap.xml
- Scripture Index: public /scripture page listing every released lesson grouped
  by study with passage reference links; served by /api/public/scripture-index

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-09-03 09:54:43 -04:00
1 parent 0bf2d92b1f
commit 63d4f9e316
7 files changed
+348 -3

No files matched your search

+46
View File
@@ -386,4 +386,50 @@ export function register(app) {
res.setHeader('Content-Disposition', `attachment; filename="subscribers-${new Date().toISOString().slice(0, 10)}.csv"`)
res.send(csv)
})
app.post('/api/admin-broadcast', requireAdminAuth, async (req, res) => {
const { subject, html } = req.body ?? {}
if (!subject || typeof subject !== 'string' || !subject.trim()) {
res.status(400).json({ message: 'Subject is required.' }); return
}
if (!html || typeof html !== 'string' || !html.trim()) {
res.status(400).json({ message: 'Email body is required.' }); return
}
if (!process.env.RESEND_API_KEY) {
res.status(500).json({ message: 'RESEND_API_KEY is not configured.' }); return
}
const seen = new Set()
const recipients = state.contactSubmissions
.filter(e => e.subscribe === true && e.email && !seen.has(e.email) && seen.add(e.email))
.map(e => ({ name: e.name ?? '', email: e.email }))
if (recipients.length === 0) {
res.status(400).json({ message: 'No subscribers to send to.' }); return
}
const resend = new Resend(process.env.RESEND_API_KEY)
const from = getResendFromAddress()
const replyTo = getResendReplyToAddress()
const BATCH = 50
let sent = 0
let failed = 0
for (let i = 0; i < recipients.length; i += BATCH) {
const batch = recipients.slice(i, i + BATCH)
const emails = batch.map(r => ({
from,
to: r.email,
reply_to: replyTo,
subject: subject.trim(),
html: html.replace(/\{\{name\}\}/g, escapeHtml(r.name || 'Friend')),
}))
try {
const { error } = await resend.batch.send(emails)
if (error) { failed += batch.length } else { sent += batch.length }
} catch { failed += batch.length }
}
res.json({ ok: true, sent, failed, total: recipients.length })
})
}