Add a newsletter archive so past issues can be shared and found; v2.3.0
Publish Container / build (push) Successful in 20m51s
Publish Container / build (push) Successful in 20m51s
Web-hosted copies of episode newsletters, published from Resend broadcasts tagged with the "New Episode" topic. Gives Facebook (and anyone else) a proper link to share instead of raw HTML email, and makes issues findable via site search and Google. - Admin "Newsletter Archive" tab lists sent Resend broadcasts, flags ones tagged "New Episode", and publishes them with one click (pulls the HTML straight from Resend's API) - Public /newsletter archive index and /newsletter/:slug issue pages, server-rendered with per-issue Open Graph tags for social previews - Unsubscribe merge-tag stripped from the web copy; archive + subscribe links appended - Issues added to sitemap.xml and the on-site search index Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60b23b0fba
commit
fe4c09f8a3
11 files changed
+516
-4
No files matched your search
+68
@@ -3022,6 +3022,73 @@ function QuestionsPage({ content }: { content: SiteContent }) {
|
||||
)
|
||||
}
|
||||
|
||||
interface NewsletterIssueSummary {
|
||||
id: string
|
||||
slug: string
|
||||
title: string
|
||||
subject: string
|
||||
description: string
|
||||
sentAt: string | null
|
||||
publishedAt: string
|
||||
}
|
||||
|
||||
function NewsletterArchivePage({ content }: { content: SiteContent }) {
|
||||
const siteTitle = content.seo?.title || DEFAULTS.seo.title
|
||||
usePageMeta(
|
||||
`Newsletter | ${siteTitle}`,
|
||||
'Read past newsletter issues from Verse by Verse with Nate — episode notes, study excerpts, and Greek word studies.',
|
||||
)
|
||||
const [issues, setIssues] = useState<NewsletterIssueSummary[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/newsletter/issues')
|
||||
.then(r => r.json())
|
||||
.then(d => { if (Array.isArray(d.issues)) setIssues(d.issues) })
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="site">
|
||||
<SiteHeader content={content} />
|
||||
<SiteSearchBar content={content} />
|
||||
<PageBanner eyebrow="Newsletter" title="Newsletter Archive" desc="Past issues of the Verse by Verse with Nate newsletter — episode notes, study excerpts, and Greek word studies." />
|
||||
<section className="section-resources" aria-label="Newsletter archive">
|
||||
<div className="section-inner">
|
||||
{loading ? (
|
||||
<p className="episode-seo-intro">Loading issues…</p>
|
||||
) : issues.length === 0 ? (
|
||||
<p className="episode-seo-intro">No newsletter issues published yet.</p>
|
||||
) : (
|
||||
<div className="resources-list">
|
||||
{issues.map(issue => (
|
||||
<a key={issue.id} href={`/newsletter/${issue.slug}`} className="resource-download-card">
|
||||
<div className="resource-download-header">
|
||||
<div className="resource-download-meta">
|
||||
<span className="resource-link-label">{issue.title}</span>
|
||||
{issue.description && <p className="resource-link-description">{issue.description}</p>}
|
||||
{issue.sentAt && (
|
||||
<span className="resource-link-tags">
|
||||
{new Date(issue.sentAt).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
|
||||
</span>
|
||||
)}
|
||||
<span className="resource-link-action">Read issue →</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<CustomBlocksSection content={content} page="newsletter" />
|
||||
<SiteFooter content={content} />
|
||||
<AnalyticsConsentBanner content={content} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PreviewPage() {
|
||||
const [content, setContent] = useState<SiteContent | null>(null)
|
||||
const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading')
|
||||
@@ -3239,6 +3306,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="/newsletter" element={<NewsletterArchivePage content={content} />} />
|
||||
<Route path="/scripture" element={<ScriptureIndexPage content={content} />} />
|
||||
<Route path="/thanks" element={<ThankYouPage />} />
|
||||
<Route path="/subscribe" element={<SubscribePage />} />
|
||||
|
||||
Reference in new issue
Block a user