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

+

Get episode updates by email

+

New episodes, study resources, and ministry updates β€” delivered to your inbox.

+ {status === 'done' ? ( +

You're subscribed! Thank you for signing up.

+ ) : ( +
+ setHoney(e.target.value)} /> +
+ setFirstName(e.target.value)} + className="newsletter-input" + /> + setEmail(e.target.value)} + className="newsletter-input" + /> + +
+ {status === 'error' &&

Something went wrong. Please try again.

} +
+ )} +
+
+ ) +} + +function HomepageQACallout() { + const [questions, setQuestions] = useState<{ id: string; firstName: string; question: string; answer: string }[]>([]) + + useEffect(() => { + fetch('/api/questions') + .then(r => (r.ok ? r.json() : Promise.reject())) + .then((data: { questions: { id: string; firstName: string; question: string; answer: string }[] }) => { + setQuestions((data.questions ?? []).slice(0, 3)) + }) + .catch(() => {}) + }, []) + + if (questions.length === 0) return null + + return ( +
+
+

Q&A

+

✦ Questions from Listeners ✦

+

Real questions. Scripture-grounded answers.

+
+ {questions.map(q => ( +
+

"{q.question}"

+

{q.answer.length > 220 ? q.answer.slice(0, 220).trimEnd() + '…' : q.answer}

+ {q.firstName &&

β€” {q.firstName}

} +
+ ))} +
+
+ Browse All Q&A β†’ +
+
+
+ ) +} + function LandingPage({ content }: { content: SiteContent }) { const featuredStudy = getHomepageFeaturedStudy(content) @@ -1063,6 +1164,8 @@ function LandingPage({ content }: { content: SiteContent }) { + +

@@ -1125,6 +1228,8 @@ function LandingPage({ content }: { content: SiteContent }) {

+ +
@@ -1386,12 +1491,51 @@ function EpisodesPage({ content }: { content: SiteContent }) {
) + const latestEpisode = !loading && !failed ? (currentEpisodes[0] ?? allEpisodes[0]) : null + const latestEmbedUrl = latestEpisode ? toSpotifyEpisodeEmbedUrl(latestEpisode.link) : '' + return (
+
-

+ + {latestEpisode && ( +
+
+

Latest Episode

+

{latestEpisode.title}

+
+ {latestEpisode.episode && Ep. {latestEpisode.episode}} + {latestEpisode.pubDate && {formatPubDate(latestEpisode.pubDate)}} + {latestEpisode.duration && {latestEpisode.duration}} +
+ {latestEpisode.description &&

{latestEpisode.description}

} + + Listen on Spotify + +
+ {latestEmbedUrl && ( +
+