v1.2.0: Broadcast emails, webhook admin UI, dynamic sitemap, scripture index
Publish Container / docker (push) Successful in 14m11s
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:
1 parent
0bf2d92b1f
commit
63d4f9e316
7 files changed
+348
-3
No files matched your search
+50
@@ -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 />} />
|
||||
|
||||
Reference in new issue
Block a user