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

+36
View File
@@ -63,6 +63,23 @@ export function register(app) {
.filter(Boolean)
.map(item => `${root}${item}`)
// Dynamic study lesson URLs
const studies = Array.isArray(state.cachedSiteContent?.studies) ? state.cachedSiteContent.studies : []
const now = new Date()
for (const study of studies) {
if (!study.slug || !Array.isArray(study.sections)) continue
for (const section of study.sections) {
if (section.id && section.releasedAt && new Date(section.releasedAt) <= now) {
urls.push(`${root}/study/${encodeURIComponent(study.slug)}/${encodeURIComponent(section.id)}`)
}
}
}
// Dynamic episode highlight URLs
const highlights = Array.isArray(state.cachedSiteContent?.podcastFeaturedLinks) ? state.cachedSiteContent.podcastFeaturedLinks : []
for (const ep of highlights) {
if (ep.id) urls.push(`${root}/episodes/${encodeURIComponent(ep.id)}`)
}
const xml = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
@@ -142,6 +159,25 @@ export function register(app) {
</html>`)
})
// Scripture index — all released study lessons with their passage references
app.get('/api/public/scripture-index', (_req, res) => {
const content = state.cachedSiteContent
const allStudies = Array.isArray(content?.studies) ? content.studies : []
const cutoff = new Date()
const studies = allStudies
.filter(s => s.slug && Array.isArray(s.sections))
.map(s => ({
slug: s.slug,
title: s.title || s.slug,
entries: s.sections
.filter(sec => sec.id && sec.reference && sec.releasedAt && new Date(sec.releasedAt) <= cutoff)
.map(sec => ({ sectionId: sec.id, title: sec.title || '', reference: sec.reference }))
.sort((a, b) => a.reference.localeCompare(b.reference, undefined, { numeric: true })),
}))
.filter(s => s.entries.length > 0)
res.json({ studies })
})
// SPA static files and fallback
app.use(express.static(DIST_DIR))