diff --git a/data/admin-content-draft.json b/data/admin-content-draft.json
index 90c18ba..315e9ac 100644
--- a/data/admin-content-draft.json
+++ b/data/admin-content-draft.json
@@ -1144,6 +1144,7 @@
]
}
],
+ "episodesSeoIntro": "",
"seo": {
"title": "Verse by Verse with Nate",
"description": "Verse by Verse with Nate explores Scripture one verse at a time with practical Bible teaching.",
@@ -1175,5 +1176,5 @@
]
}
},
- "updatedAt": "2026-06-02T17:54:05.948Z"
+ "updatedAt": "2026-06-03T14:17:48.071Z"
}
\ No newline at end of file
diff --git a/data/admin-content.json b/data/admin-content.json
index 63d5e42..31dba68 100644
--- a/data/admin-content.json
+++ b/data/admin-content.json
@@ -1144,6 +1144,7 @@
]
}
],
+ "episodesSeoIntro": "",
"seo": {
"title": "Verse by Verse with Nate",
"description": "Verse by Verse with Nate explores Scripture one verse at a time with practical Bible teaching.",
@@ -1175,5 +1176,5 @@
]
}
},
- "updatedAt": "2026-06-02T17:54:06.010Z"
+ "updatedAt": "2026-06-03T14:17:48.087Z"
}
\ No newline at end of file
diff --git a/data/questions.json b/data/questions.json
index 1ab7a58..3fc73c5 100644
--- a/data/questions.json
+++ b/data/questions.json
@@ -98,5 +98,5 @@
"topic": "Faith & Life"
}
],
- "updatedAt": "2026-06-02T17:54:06.010Z"
+ "updatedAt": "2026-06-03T14:17:48.087Z"
}
\ No newline at end of file
diff --git a/data/study-users.json b/data/study-users.json
index ef513f8..909170d 100644
--- a/data/study-users.json
+++ b/data/study-users.json
@@ -6,8 +6,11 @@
"passwordHash": "6500cc2e717d1d646f371dba1ec918d2f2f9fddbb8576c792df69a1f2340dcd9",
"displayName": "Student Test",
"subscribeNewsletter": false,
+ "studyRemindersEnabled": false,
+ "enrolledStudySlugs": [],
"avatarUrl": "",
- "lastLoginAt": "2026-05-21T15:41:16.259Z"
+ "lastLoginAt": "2026-05-21T15:41:16.259Z",
+ "pendingEmailChange": null
},
{
"id": "3307bdd7-a770-4fad-a28e-cf2d0c2a6e34",
@@ -15,12 +18,14 @@
"passwordHash": "fc89481d72dbea9af1d4f054faad48813227fa018da39259272dd27b5e493bb5",
"displayName": "Test Student",
"subscribeNewsletter": false,
- "avatarUrl": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2288%22%20height%3D%2288%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20rx%3D%2218%22%20ry%3D%2218%22%20fill%3D%22%2311100d%22%2F%3E%3Ctext%20x%3D%2250%25%22%20y%3D%2250%25%22%20dominant-baseline%3D%22middle%22%20text-anchor%3D%22middle%22%20font-size%3D%2256%22%3E%F0%9F%A6%81%3C%2Ftext%3E%3C%2Fsvg%3E",
- "lastLoginAt": "2026-06-02T17:56:40.769Z",
- "updatedAt": "2026-06-02T17:57:04.582Z",
+ "studyRemindersEnabled": false,
"enrolledStudySlugs": [
"colossians"
- ]
+ ],
+ "avatarUrl": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2288%22%20height%3D%2288%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20rx%3D%2218%22%20ry%3D%2218%22%20fill%3D%22%2311100d%22%2F%3E%3Ctext%20x%3D%2250%25%22%20y%3D%2250%25%22%20dominant-baseline%3D%22middle%22%20text-anchor%3D%22middle%22%20font-size%3D%2256%22%3E%F0%9F%A6%81%3C%2Ftext%3E%3C%2Fsvg%3E",
+ "lastLoginAt": "2026-06-03T14:16:38.510Z",
+ "pendingEmailChange": null,
+ "updatedAt": "2026-06-03T14:16:38.510Z"
},
{
"id": "81c01e32-d94e-4e19-93ea-e5cd7644ea6a",
@@ -28,9 +33,12 @@
"passwordHash": "fc89481d72dbea9af1d4f054faad48813227fa018da39259272dd27b5e493bb5",
"displayName": "Avatar Test",
"subscribeNewsletter": false,
+ "studyRemindersEnabled": false,
+ "enrolledStudySlugs": [],
"avatarUrl": "data:image/svg+xml;utf8,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"88\" height=\"88\"%3E%3Crect width=\"100%\" height=\"100%\" rx=\"18\" ry=\"18\" fill=\"%2311100d\"/%3E%3Ctext x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" font-size=\"56\"%3Eπ±%3C/text%3E%3C/svg%3E",
- "lastLoginAt": "2026-06-02T17:33:13.669Z"
+ "lastLoginAt": "2026-06-02T17:33:13.669Z",
+ "pendingEmailChange": null
}
],
- "updatedAt": "2026-06-02T17:57:04.583Z"
+ "updatedAt": "2026-06-03T14:16:38.513Z"
}
\ No newline at end of file
diff --git a/server.js b/server.js
index bdd3ec7..077af15 100644
--- a/server.js
+++ b/server.js
@@ -3342,7 +3342,7 @@ app.post('/api/study-community/posts', requireStudyAuth, async (req, res) => {
}
const now = new Date().toISOString()
- const authorName = user.displayName?.trim() || user.username
+ const authorName = user.displayName?.trim() || (user.username?.includes('@') ? user.username.split('@')[0] : user.username)
const post = {
id: randomUUID(),
studySlug,
@@ -3386,7 +3386,7 @@ app.post('/api/study-community/posts/:postId/replies', requireStudyAuth, async (
const reply = {
id: randomUUID(),
authorUserId: user.id,
- authorName: user.displayName?.trim() || user.username,
+ authorName: user.displayName?.trim() || (user.username?.includes('@') ? user.username.split('@')[0] : user.username),
authorAvatarUrl: getStudyAvatarUrl(user.username),
message,
createdAt: new Date().toISOString(),
diff --git a/server/helpers.js b/server/helpers.js
index 7f186ba..41388b2 100644
--- a/server/helpers.js
+++ b/server/helpers.js
@@ -29,7 +29,7 @@ export const DEFAULT_SEO = {
ogImage: '/images/podcast-art.jpeg',
canonicalUrl: 'https://versebyversewithnate.us/',
robotsPolicy: 'index,follow',
- sitemapPaths: ['/', '/start-here', '/questions', '/privacy', '/terms'],
+ sitemapPaths: ['/', '/start-here', '/questions', '/privacy', '/terms', '/about', '/contact', '/episodes', '/resources', '/study', '/study/titus', '/study/colossians'],
}
export const DEFAULT_LEGAL = {
@@ -338,6 +338,7 @@ export function sanitizeSiteContent(siteContent) {
colossiansStudySections: sanitizeColossiansStudySections(siteContent.colossiansStudySections),
redirects: sanitizeRedirectRules(siteContent.redirects),
podcastFeaturedLinks: sanitizeFeaturedLinks(siteContent.podcastFeaturedLinks ?? DEFAULT_PODCAST_FEATURED_LINKS),
+ episodesSeoIntro: typeof siteContent.episodesSeoIntro === 'string' && siteContent.episodesSeoIntro.trim() ? siteContent.episodesSeoIntro.trim().slice(0, 600) : '',
seo: {
title: typeof seo.title === 'string' && seo.title.trim() ? seo.title.trim().slice(0, 120) : DEFAULT_SEO.title,
description: typeof seo.description === 'string' && seo.description.trim() ? seo.description.trim().slice(0, 240) : DEFAULT_SEO.description,
diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx
index 1f2c201..940dad8 100644
--- a/src/AdminPage.tsx
+++ b/src/AdminPage.tsx
@@ -491,6 +491,7 @@ const FIELDS: Array<{ key: StringField; label: string; multiline?: boolean; sect
{ key: 'contactPoint2', label: 'Contact β Point 2', section: 'contact' },
{ key: 'contactVerse', label: 'Contact β Scripture Text', section: 'contact' },
{ key: 'contactVerseRef', label: 'Contact β Scripture Reference', section: 'contact' },
+ { key: 'episodesSeoIntro', label: 'Episodes Page β SEO Description (hidden from visitors, indexed by search engines)', multiline: true, section: 'series' },
{ key: 'seriesLabel', label: 'Series Label (e.g. "Now Playing")', section: 'series' },
{ key: 'seriesTitle', label: 'Series Title', section: 'series' },
{ key: 'seriesDescription', label: 'Series Description', multiline: true, section: 'series' },
diff --git a/src/App.css b/src/App.css
index 9fcfe94..4548323 100644
--- a/src/App.css
+++ b/src/App.css
@@ -403,6 +403,62 @@
padding: 5rem 0;
}
+.episode-seo-intro {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ white-space: nowrap;
+}
+
+.latest-episode-card {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2.5rem;
+ align-items: center;
+ background: #111110;
+ border: 1px solid rgba(201, 168, 76, 0.2);
+ border-radius: 12px;
+ padding: 2rem;
+ margin-bottom: 1rem;
+}
+
+.latest-episode-title {
+ font-size: 1.4rem;
+ font-weight: 700;
+ color: #f0e9cc;
+ margin: 0.5rem 0 0.75rem;
+ line-height: 1.3;
+}
+
+.latest-episode-pills {
+ display: flex;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+ margin-bottom: 0.75rem;
+}
+
+.latest-episode-desc {
+ font-size: 0.9rem;
+ color: #8a7f5a;
+ line-height: 1.6;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.latest-episode-embed {
+ width: 100%;
+}
+
+@media (max-width: 700px) {
+ .latest-episode-card {
+ grid-template-columns: 1fr;
+ }
+}
+
.episode-series-subtitle {
text-align: center;
color: var(--brand-gold);
@@ -735,6 +791,71 @@
opacity: 0.9;
}
+/* ββ Homepage Newsletter ββ */
+.section-newsletter {
+ background: #0c0c08;
+ border-top: 1px solid rgba(201, 168, 76, 0.15);
+ border-bottom: 1px solid rgba(201, 168, 76, 0.15);
+ padding: 4rem 0;
+ text-align: center;
+}
+
+.newsletter-inner {
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.newsletter-sub {
+ color: #a89a6a;
+ font-size: 1rem;
+ margin-bottom: 2rem;
+}
+
+.newsletter-form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.newsletter-fields {
+ display: flex;
+ gap: 0.75rem;
+ width: 100%;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.newsletter-input {
+ flex: 1 1 180px;
+ padding: 0.7rem 1rem;
+ border-radius: 6px;
+ border: 1px solid rgba(201, 168, 76, 0.3);
+ background: #1a1a12;
+ color: #f0e9cc;
+ font-size: 0.95rem;
+ outline: none;
+}
+
+.newsletter-input::placeholder {
+ color: #5a5440;
+}
+
+.newsletter-input:focus {
+ border-color: rgba(201, 168, 76, 0.7);
+}
+
+.newsletter-thanks {
+ color: #c9a84c;
+ font-size: 1.05rem;
+ font-weight: 600;
+}
+
+.newsletter-error {
+ color: #e05c5c;
+ font-size: 0.9rem;
+}
+
/* ββ Series ββ */
.section-series {
background: #080808;
@@ -935,6 +1056,61 @@
font-size: 0.8rem;
}
+/* ββ Homepage Q&A Callout ββ */
+.section-qa-callout {
+ background: #080808;
+ border-top: 1px solid rgba(201, 168, 76, 0.18);
+ border-bottom: 1px solid rgba(201, 168, 76, 0.18);
+ padding: 5rem 0;
+ text-align: center;
+}
+
+.qa-callout-sub {
+ color: #8a7f5a;
+ font-size: 1rem;
+ margin: -0.5rem 0 2rem;
+}
+
+.qa-callout-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+ gap: 1.25rem;
+ max-width: 960px;
+ margin: 0 auto;
+ text-align: left;
+}
+
+.qa-callout-card {
+ background: #111110;
+ border: 1px solid rgba(201, 168, 76, 0.15);
+ border-radius: 10px;
+ padding: 1.25rem 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.qa-callout-question {
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: #c9a84c;
+ margin: 0;
+ line-height: 1.5;
+}
+
+.qa-callout-answer {
+ font-size: 0.875rem;
+ color: #a89a6a;
+ line-height: 1.65;
+ margin: 0;
+}
+
+.qa-callout-from {
+ font-size: 0.8rem;
+ color: #5a5440;
+ margin: 0;
+}
+
/* ββ Home Jump Section ββ */
.section-share {
background: #080808;
diff --git a/src/App.tsx b/src/App.tsx
index 6592ab1..d3efccb 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -978,6 +978,107 @@ function ExternalSitesSection({ content }: { content: SiteContent }) {
)
}
+function HomepageNewsletterSection() {
+ const [email, setEmail] = useState('')
+ const [firstName, setFirstName] = useState('')
+ const [honey, setHoney] = useState('')
+ const [status, setStatus] = useState<'idle' | 'submitting' | 'done' | 'error'>('idle')
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ if (honey) return
+ setStatus('submitting')
+ try {
+ const res = await fetch('/api/contact', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ firstName, email, messageType: 'general', message: 'Newsletter signup from homepage', subscribe: true, _honey: honey }),
+ })
+ setStatus(res.ok ? 'done' : 'error')
+ } catch {
+ setStatus('error')
+ }
+ }
+
+ return (
+ Stay in the Word New episodes, study resources, and ministry updates β delivered to your inbox. You're subscribed! Thank you for signing up. Q&A Real questions. Scripture-grounded answers. "{q.question}" {q.answer.length > 220 ? q.answer.slice(0, 220).trimEnd() + 'β¦' : q.answer} β {q.firstName} Latest Episode {latestEpisode.description} {enrollMessage} Want episode updates and study announcements by email? Have a question from the Word?
+ Submit a Bible or theology question and it may be answered in a future episode or the Q&A archive.
+ Get episode updates by email
+ β¦ Questions from Listeners β¦
+
@@ -1125,6 +1228,8 @@ function LandingPage({ content }: { content: SiteContent }) {
+
+ {latestEpisode && (
+ {latestEpisode.title}
+
β¦ {content.seriesLabel || 'Now Playing'}{' '}
β¦
diff --git a/src/colossiansStudy.tsx b/src/colossiansStudy.tsx
index 6981237..80d0541 100644
--- a/src/colossiansStudy.tsx
+++ b/src/colossiansStudy.tsx
@@ -463,6 +463,8 @@ export function StudyLandingPage({ content }: Props) {
const [studyModal, setStudyModal] = useState<'none' | 'enrollments' | 'browseTracks'>('none')
const [enrollBusySlug, setEnrollBusySlug] = useState('')
const [enrollMessage, setEnrollMessage] = useState('')
+ const [showNewsletterNudge, setShowNewsletterNudge] = useState(false)
+ const [newsletterNudgeDone, setNewsletterNudgeDone] = useState(false)
const studies = getStudies(content)
const activeStudies = studies.filter(study => study.status !== 'planned')
@@ -495,6 +497,7 @@ export function StudyLandingPage({ content }: Props) {
const slugs = normalizeEnrolledStudySlugs(data.enrolledStudySlugs)
setAuth(prev => ({ ...prev, enrolledStudySlugs: slugs }))
setEnrollMessage(enrolled ? `Unenrolled from ${data.studyTitle ?? studySlug}.` : `Enrolled in ${data.studyTitle ?? studySlug}.`)
+ if (!enrolled && !auth.subscribeNewsletter) setShowNewsletterNudge(true)
await refreshOverview()
} catch (err) {
setEnrollMessage(err instanceof Error ? err.message : 'Unable to update enrollment.')
@@ -678,6 +681,22 @@ export function StudyLandingPage({ content }: Props) {
Ask Nate a Question
+
Total notes saved
{new Date(overview.stats.memberSince).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
+{overview.stats.memberSince && !isNaN(new Date(overview.stats.memberSince).getTime()) ? new Date(overview.stats.memberSince).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'β'}
Member since