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

+50
View File
@@ -1925,6 +1925,55 @@ function ResourcesPage({ content }: { content: SiteContent }) {
)
}
function ScriptureIndexPage({ content }: { content: SiteContent }) {
const siteTitle = content.seo?.title || DEFAULTS.seo.title
usePageMeta(
`Scripture Index | ${siteTitle}`,
'Every passage referenced across all Bible studies — find the lesson that covers a particular verse.',
)
interface ScriptureEntry { sectionId: string; title: string; reference: string }
interface StudyGroup { slug: string; title: string; entries: ScriptureEntry[] }
const [studies, setStudies] = useState<StudyGroup[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/public/scripture-index')
.then(r => r.json())
.then(d => { if (Array.isArray(d.studies)) setStudies(d.studies) })
.catch(() => {})
.finally(() => setLoading(false))
}, [])
return (
<div className="site">
<SiteHeader content={content} />
<SiteSearchBar content={content} />
<PageBanner eyebrow="Reference" title="Scripture Index" desc="Every passage covered in our studies, organized by reference. Find the lesson for any verse." />
<main className="scripture-index-main page-width">
{loading ? (
<p className="scripture-index-note">Loading…</p>
) : studies.length === 0 ? (
<p className="scripture-index-note">No study lessons are published yet.</p>
) : studies.map(study => (
<section key={study.slug} className="scripture-study-group">
<h2 className="scripture-study-title">{study.title}</h2>
<ul className="scripture-entry-list">
{study.entries.map(entry => (
<li key={entry.sectionId} className="scripture-entry">
<Link to={`/study/${study.slug}/${entry.sectionId}`} className="scripture-entry-link">
<span className="scripture-entry-ref">{entry.reference}</span>
{entry.title && <span className="scripture-entry-title">{entry.title}</span>}
</Link>
</li>
))}
</ul>
</section>
))}
</main>
<SiteFooter content={content} />
<AnalyticsConsentBanner content={content} />
</div>
)
}
function AboutPage({ content }: { content: SiteContent }) {
const siteTitle = content.seo?.title || DEFAULTS.seo.title
usePageMeta(
@@ -2711,6 +2760,7 @@ export default function App() {
<Route path="/about" element={<AboutPage content={content} />} />
<Route path="/contact" element={<ContactPage content={content} />} />
<Route path="/questions" element={<QuestionsPage content={content} />} />
<Route path="/scripture" element={<ScriptureIndexPage content={content} />} />
<Route path="/thanks" element={<ThankYouPage />} />
<Route path="/subscribe" element={<SubscribePage />} />
<Route path="/subscribe/thanks" element={<SubscribeThankYouPage />} />