Add Episode Highlights player with Spotify embed auto-resolution and direct embed URL support
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
"siteContent": {
|
"siteContent": {
|
||||||
"eyebrow": "A Journey Through Scripture",
|
"eyebrow": "A Journey Through Scripture",
|
||||||
"heroTagline": "Exploring God's Word one verse at a time",
|
"heroTagline": "Exploring God's Word one verse at a time",
|
||||||
|
"heroBtnSpotify": "Listen on Spotify",
|
||||||
|
"heroBtnEpisodes": "Explore Episodes ↓",
|
||||||
|
"heroBtnStartHere": "New here? Start here →",
|
||||||
"startHereHeading": "New Here? Start Here",
|
"startHereHeading": "New Here? Start Here",
|
||||||
"startHereIntro": "If this is your first visit, this path helps you get grounded quickly and make the most of the site.",
|
"startHereIntro": "If this is your first visit, this path helps you get grounded quickly and make the most of the site.",
|
||||||
"startHereStep1Title": "Step 1: Listen to 3 Starter Episodes",
|
"startHereStep1Title": "Step 1: Listen to 3 Starter Episodes",
|
||||||
@@ -13,6 +16,9 @@
|
|||||||
"startHereStep3Title": "Step 3: Ask Nate Directly",
|
"startHereStep3Title": "Step 3: Ask Nate Directly",
|
||||||
"startHereStep3Body": "Use the contact form to submit your Bible question for future Q&A or an upcoming episode.",
|
"startHereStep3Body": "Use the contact form to submit your Bible question for future Q&A or an upcoming episode.",
|
||||||
"startHereStep3Cta": "Submit a Question",
|
"startHereStep3Cta": "Submit a Question",
|
||||||
|
"aboutEyebrow": "About Nate",
|
||||||
|
"aboutListenBtnLabel": "Listen Now ↓",
|
||||||
|
"aboutShowEyebrow": "About the Show",
|
||||||
"aboutShowHeading": "Depth. Clarity. Application.",
|
"aboutShowHeading": "Depth. Clarity. Application.",
|
||||||
"aboutShowP1": "Verse by Verse with Nate walks through Scripture passage by passage — unpacking the original context, drawing out the meaning, and connecting each verse to how we live today.",
|
"aboutShowP1": "Verse by Verse with Nate walks through Scripture passage by passage — unpacking the original context, drawing out the meaning, and connecting each verse to how we live today.",
|
||||||
"aboutShowP2": "Whether you're in the car, at the gym, or just looking for something to anchor your day, each episode is designed to feed your faith with solid, practical teaching.",
|
"aboutShowP2": "Whether you're in the car, at the gym, or just looking for something to anchor your day, each episode is designed to feed your faith with solid, practical teaching.",
|
||||||
@@ -20,17 +26,95 @@
|
|||||||
"aboutPhotoUrl": "/images/nate-photo.jpeg",
|
"aboutPhotoUrl": "/images/nate-photo.jpeg",
|
||||||
"aboutVerseArtUrl": "/images/hebrews-4-12-verse-art.png",
|
"aboutVerseArtUrl": "/images/hebrews-4-12-verse-art.png",
|
||||||
"contactPhotoUrl": "/images/nate-contact-photo.png",
|
"contactPhotoUrl": "/images/nate-contact-photo.png",
|
||||||
|
"contactEyebrow": "Get in Touch",
|
||||||
|
"contactHeading": "Contact Nate",
|
||||||
|
"contactName": "Nate",
|
||||||
|
"contactRole": "Bible teacher · Lynchburg, VA",
|
||||||
|
"contactQuote": "I read every message - nothing blesses me more than hearing how God's Word is at work in your life.",
|
||||||
|
"contactIntro": "Ask a Bible question, share a testimony, or request a topic for a future episode.",
|
||||||
|
"contactPoint1": "I read every message personally",
|
||||||
|
"contactPoint2": "Response within 48 hours",
|
||||||
|
"contactVerse": "Your word is a lamp to my feet and a light to my path.",
|
||||||
|
"contactVerseRef": "Psalm 119:105",
|
||||||
"seriesLabel": "Now Playing",
|
"seriesLabel": "Now Playing",
|
||||||
"seriesTitle": "Study of Titus: Sound Doctrine",
|
"seriesTitle": "Study of Titus: Sound Doctrine",
|
||||||
"seriesDescription": "A deep-dive into Paul's letter to Titus — unpacking what it means to build a church and a life on sound doctrine.",
|
"seriesDescription": "A deep-dive into Paul's letter to Titus — unpacking what it means to build a church and a life on sound doctrine.",
|
||||||
"seriesImageUrl": "/uploads/titus_study_cover-1777916521873.png",
|
"seriesImageUrl": "/images/titus-cover.png",
|
||||||
"seriesListenUrl": "https://open.spotify.com/show/0Gq1TzoJOdReSZ1gYQi8Xl",
|
"seriesListenUrl": "/spotify",
|
||||||
|
"seriesListenBtnLabel": "Listen to Series",
|
||||||
|
"seriesDownloadsBtnLabel": "View Downloads",
|
||||||
"studyGuideTitle": "Companion Study Guide",
|
"studyGuideTitle": "Companion Study Guide",
|
||||||
"studyGuideDescription": "Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.",
|
"studyGuideDescription": "Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.",
|
||||||
"studyGuideDownloadUrl": "",
|
"studyGuideDownloadUrl": "",
|
||||||
"studyGuideUrl": "https://a.co/d/01sG2tOJ",
|
"studyGuideUrl": "https://a.co/d/01sG2tOJ",
|
||||||
|
"studyGuideAmazonButtonLabel": "Get it on Amazon",
|
||||||
"shareHeading": "Help one more person hear the Word this week.",
|
"shareHeading": "Help one more person hear the Word this week.",
|
||||||
"shareP": "Scan the QR code or text the show link to a friend who needs encouragement today.",
|
"shareP": "Scan the QR code or text the show link to a friend who needs encouragement today.",
|
||||||
|
"whereToNextEyebrow": "Where to Next",
|
||||||
|
"whereToNextHeading": "Choose the page you need.",
|
||||||
|
"whereToNextCards": [
|
||||||
|
{
|
||||||
|
"id": "episodes",
|
||||||
|
"title": "Episodes",
|
||||||
|
"description": "Listen to latest episodes and platform links.",
|
||||||
|
"path": "/episodes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "downloads",
|
||||||
|
"title": "Downloads",
|
||||||
|
"description": "Main guide, extra downloads, and past study files.",
|
||||||
|
"path": "/resources"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qa",
|
||||||
|
"title": "Q&A",
|
||||||
|
"description": "Browse Bible questions and approved answers.",
|
||||||
|
"path": "/questions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "about",
|
||||||
|
"title": "About",
|
||||||
|
"description": "Learn about Nate and the mission of the show.",
|
||||||
|
"path": "/about"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "contact",
|
||||||
|
"title": "Contact",
|
||||||
|
"description": "Submit a Bible question, testimony, or topic.",
|
||||||
|
"path": "/contact"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footerTitle": "Verse by Verse with Nate",
|
||||||
|
"footerSubtitle": "A Journey Through Scripture",
|
||||||
|
"footerEmail": "hello@versebyversewithnate.us",
|
||||||
|
"footerCopyright": "© 2026 Nate Emmert · Made with faith.",
|
||||||
|
"footerPrivacyNote": "Privacy: with consent, analytics may store masked IP-based location data (country/state/county/city) and returning visitor activity.",
|
||||||
|
"platformSpotifyUrl": "/spotify",
|
||||||
|
"platformAppleUrl": "/apple",
|
||||||
|
"platformYoutubeUrl": "https://www.youtube.com/@blackzebraem5558",
|
||||||
|
"platformAmazonUrl": "/amazon",
|
||||||
|
"platformFacebookUrl": "https://facebook.com/versebyversewithnate",
|
||||||
|
"platformCreatorUrl": "https://creators.spotify.com/pod/profile/nmemmert/",
|
||||||
|
"welcomeEmailSubject": "Welcome to Verse by Verse with Nate",
|
||||||
|
"welcomeEmailGreetingPrefix": "Glad you're here",
|
||||||
|
"welcomeEmailIntro": "Thanks for subscribing to Verse by Verse with Nate - a Bible teaching podcast where we slow down, dig into the text, and pull out the nuggets God has for us word by word.",
|
||||||
|
"welcomeEmailCurrentSeries": "Right now we're working through the book of Titus - a short letter packed with practical wisdom about grace, godliness, and what the Christian life looks like when it's rooted in sound doctrine.",
|
||||||
|
"welcomeEmailStartHereTitle": "Episode 1 - Introduction to Titus",
|
||||||
|
"welcomeEmailStartHereSummary": "Who wrote it, who received it, and why it still matters.",
|
||||||
|
"welcomeEmailStartHereUrl": "/start-here",
|
||||||
|
"welcomeEmailSpotifyUrl": "/spotify",
|
||||||
|
"welcomeEmailAppleUrl": "/apple",
|
||||||
|
"welcomeEmailAmazonUrl": "/amazon",
|
||||||
|
"welcomeEmailWebsiteUrl": "https://versebyversewithnate.us",
|
||||||
|
"welcomeEmailImageUrl": "/images/podcast-art.jpeg",
|
||||||
|
"welcomeEmailWhatToExpect1": "Verse-by-verse teaching - we go slow and let the text speak for itself.",
|
||||||
|
"welcomeEmailWhatToExpect2": "Greek word studies - the kind that open up meaning without being a lecture.",
|
||||||
|
"welcomeEmailWhatToExpect3": "New episodes + study notes delivered right to your inbox.",
|
||||||
|
"welcomeEmailScripture": "For the grace of God has appeared, bringing salvation to all people.",
|
||||||
|
"welcomeEmailScriptureRef": "Titus 2:11 - BSB",
|
||||||
|
"welcomeEmailSignoff": "Grace and peace,\nNate",
|
||||||
|
"headerFollowLabel": "Follow on Spotify",
|
||||||
|
"cookieBannerText": "We use optional analytics cookies to measure visits and location trends for site improvement.",
|
||||||
"customLinks": [],
|
"customLinks": [],
|
||||||
"customBlocks": [],
|
"customBlocks": [],
|
||||||
"archivedSeries": [],
|
"archivedSeries": [],
|
||||||
@@ -56,17 +140,15 @@
|
|||||||
],
|
],
|
||||||
"podcastFeaturedLinks": [
|
"podcastFeaturedLinks": [
|
||||||
{
|
{
|
||||||
"id": "sample-highlight-1",
|
"id": "movn6tk3",
|
||||||
"title": "Introduction to Titus",
|
"title": "news",
|
||||||
"episodeNumber": "2",
|
"episodeNumber": "1",
|
||||||
"summary": "A practical walkthrough of context, structure, and application for this study.",
|
"summary": "news",
|
||||||
"url": "https://open.spotify.com/episode/3mpFfJQP6avAhzxu4BTKvR?si=cl_Dm_2vQraiboouaoINaw",
|
"url": "",
|
||||||
"embedUrl": "",
|
"embedUrl": "https://open.spotify.com/embed/episode/4fDzD2q9XtVtOsOq0xlgLn?utm_source=generator",
|
||||||
"showNotes": "",
|
"showNotes": "test",
|
||||||
"discussionQuestions": [
|
"discussionQuestions": [
|
||||||
"1. What do you already know about Paul, Titus, or the island of Crete? And what — if anything — surprised you about the background of this letter?",
|
"tes"
|
||||||
"2. The letter’s core theme is that sound doctrine leads to godly living. Where have you seen that connection — or its breakdown — in your own experience?",
|
|
||||||
"3. Paul trusted Titus with a hard assignment. Who has trusted you with something difficult — and what did that trust mean to you?"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -101,5 +183,5 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updatedAt": "2026-05-05T20:02:33.053Z"
|
"updatedAt": "2026-05-07T15:35:23.888Z"
|
||||||
}
|
}
|
||||||
+89
-29
@@ -2,6 +2,9 @@
|
|||||||
"siteContent": {
|
"siteContent": {
|
||||||
"eyebrow": "A Journey Through Scripture",
|
"eyebrow": "A Journey Through Scripture",
|
||||||
"heroTagline": "Exploring God's Word one verse at a time",
|
"heroTagline": "Exploring God's Word one verse at a time",
|
||||||
|
"heroBtnSpotify": "Listen on Spotify",
|
||||||
|
"heroBtnEpisodes": "Explore Episodes ↓",
|
||||||
|
"heroBtnStartHere": "New here? Start here →",
|
||||||
"startHereHeading": "New Here? Start Here",
|
"startHereHeading": "New Here? Start Here",
|
||||||
"startHereIntro": "If this is your first visit, this path helps you get grounded quickly and make the most of the site.",
|
"startHereIntro": "If this is your first visit, this path helps you get grounded quickly and make the most of the site.",
|
||||||
"startHereStep1Title": "Step 1: Listen to 3 Starter Episodes",
|
"startHereStep1Title": "Step 1: Listen to 3 Starter Episodes",
|
||||||
@@ -13,6 +16,9 @@
|
|||||||
"startHereStep3Title": "Step 3: Ask Nate Directly",
|
"startHereStep3Title": "Step 3: Ask Nate Directly",
|
||||||
"startHereStep3Body": "Use the contact form to submit your Bible question for future Q&A or an upcoming episode.",
|
"startHereStep3Body": "Use the contact form to submit your Bible question for future Q&A or an upcoming episode.",
|
||||||
"startHereStep3Cta": "Submit a Question",
|
"startHereStep3Cta": "Submit a Question",
|
||||||
|
"aboutEyebrow": "About Nate",
|
||||||
|
"aboutListenBtnLabel": "Listen Now ↓",
|
||||||
|
"aboutShowEyebrow": "About the Show",
|
||||||
"aboutShowHeading": "Depth. Clarity. Application.",
|
"aboutShowHeading": "Depth. Clarity. Application.",
|
||||||
"aboutShowP1": "Verse by Verse with Nate walks through Scripture passage by passage — unpacking the original context, drawing out the meaning, and connecting each verse to how we live today.",
|
"aboutShowP1": "Verse by Verse with Nate walks through Scripture passage by passage — unpacking the original context, drawing out the meaning, and connecting each verse to how we live today.",
|
||||||
"aboutShowP2": "Whether you're in the car, at the gym, or just looking for something to anchor your day, each episode is designed to feed your faith with solid, practical teaching.",
|
"aboutShowP2": "Whether you're in the car, at the gym, or just looking for something to anchor your day, each episode is designed to feed your faith with solid, practical teaching.",
|
||||||
@@ -20,40 +26,96 @@
|
|||||||
"aboutPhotoUrl": "/images/nate-photo.jpeg",
|
"aboutPhotoUrl": "/images/nate-photo.jpeg",
|
||||||
"aboutVerseArtUrl": "/images/hebrews-4-12-verse-art.png",
|
"aboutVerseArtUrl": "/images/hebrews-4-12-verse-art.png",
|
||||||
"contactPhotoUrl": "/images/nate-contact-photo.png",
|
"contactPhotoUrl": "/images/nate-contact-photo.png",
|
||||||
|
"contactEyebrow": "Get in Touch",
|
||||||
|
"contactHeading": "Contact Nate",
|
||||||
|
"contactName": "Nate",
|
||||||
|
"contactRole": "Bible teacher · Lynchburg, VA",
|
||||||
|
"contactQuote": "I read every message - nothing blesses me more than hearing how God's Word is at work in your life.",
|
||||||
|
"contactIntro": "Ask a Bible question, share a testimony, or request a topic for a future episode.",
|
||||||
|
"contactPoint1": "I read every message personally",
|
||||||
|
"contactPoint2": "Response within 48 hours",
|
||||||
|
"contactVerse": "Your word is a lamp to my feet and a light to my path.",
|
||||||
|
"contactVerseRef": "Psalm 119:105",
|
||||||
"seriesLabel": "Now Playing",
|
"seriesLabel": "Now Playing",
|
||||||
"seriesTitle": "Study of Titus: Sound Doctrine",
|
"seriesTitle": "Study of Titus: Sound Doctrine",
|
||||||
"seriesDescription": "A deep-dive into Paul's letter to Titus — unpacking what it means to build a church and a life on sound doctrine.",
|
"seriesDescription": "A deep-dive into Paul's letter to Titus — unpacking what it means to build a church and a life on sound doctrine.",
|
||||||
"seriesImageUrl": "/uploads/titus_study_cover-1777916521873.png",
|
"seriesImageUrl": "/images/titus-cover.png",
|
||||||
"seriesListenUrl": "https://open.spotify.com/show/0Gq1TzoJOdReSZ1gYQi8Xl",
|
"seriesListenUrl": "/spotify",
|
||||||
|
"seriesListenBtnLabel": "Listen to Series",
|
||||||
|
"seriesDownloadsBtnLabel": "View Downloads",
|
||||||
"studyGuideTitle": "Companion Study Guide",
|
"studyGuideTitle": "Companion Study Guide",
|
||||||
"studyGuideDescription": "Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.",
|
"studyGuideDescription": "Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.",
|
||||||
"studyGuideDownloadUrl": "",
|
"studyGuideDownloadUrl": "",
|
||||||
"studyGuideUrl": "https://a.co/d/01sG2tOJ",
|
"studyGuideUrl": "https://a.co/d/01sG2tOJ",
|
||||||
|
"studyGuideAmazonButtonLabel": "Get it on Amazon",
|
||||||
"shareHeading": "Help one more person hear the Word this week.",
|
"shareHeading": "Help one more person hear the Word this week.",
|
||||||
"shareP": "Scan the QR code or text the show link to a friend who needs encouragement today.",
|
"shareP": "Scan the QR code or text the show link to a friend who needs encouragement today.",
|
||||||
"customLinks": [
|
"whereToNextEyebrow": "Where to Next",
|
||||||
|
"whereToNextHeading": "Choose the page you need.",
|
||||||
|
"whereToNextCards": [
|
||||||
{
|
{
|
||||||
"id": "pocket-casts",
|
"id": "episodes",
|
||||||
"label": "Pocket Casts",
|
"title": "Episodes",
|
||||||
"url": "https://pca.st/76jisz3d",
|
"description": "Listen to latest episodes and platform links.",
|
||||||
"placement": "platforms",
|
"path": "/episodes"
|
||||||
"imageUrl": "/images/listen-on-pocket-casts.svg"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "castbox",
|
"id": "downloads",
|
||||||
"label": "Castbox",
|
"title": "Downloads",
|
||||||
"url": "https://castbox.fm/channel/id7140701?country=us",
|
"description": "Main guide, extra downloads, and past study files.",
|
||||||
"placement": "platforms",
|
"path": "/resources"
|
||||||
"imageUrl": "/images/listen-on-castbox.svg"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "goodpods",
|
"id": "qa",
|
||||||
"label": "Goodpods",
|
"title": "Q&A",
|
||||||
"url": "https://goodpods.com/podcasts/verse-by-verse-with-nate-732127",
|
"description": "Browse Bible questions and approved answers.",
|
||||||
"placement": "platforms",
|
"path": "/questions"
|
||||||
"imageUrl": "https://storage.googleapis.com/goodpods-images-bucket/badges/generic-badge-3.svg"
|
},
|
||||||
|
{
|
||||||
|
"id": "about",
|
||||||
|
"title": "About",
|
||||||
|
"description": "Learn about Nate and the mission of the show.",
|
||||||
|
"path": "/about"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "contact",
|
||||||
|
"title": "Contact",
|
||||||
|
"description": "Submit a Bible question, testimony, or topic.",
|
||||||
|
"path": "/contact"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"footerTitle": "Verse by Verse with Nate",
|
||||||
|
"footerSubtitle": "A Journey Through Scripture",
|
||||||
|
"footerEmail": "hello@versebyversewithnate.us",
|
||||||
|
"footerCopyright": "© 2026 Nate Emmert · Made with faith.",
|
||||||
|
"footerPrivacyNote": "Privacy: with consent, analytics may store masked IP-based location data (country/state/county/city) and returning visitor activity.",
|
||||||
|
"platformSpotifyUrl": "/spotify",
|
||||||
|
"platformAppleUrl": "/apple",
|
||||||
|
"platformYoutubeUrl": "https://www.youtube.com/@blackzebraem5558",
|
||||||
|
"platformAmazonUrl": "/amazon",
|
||||||
|
"platformFacebookUrl": "https://facebook.com/versebyversewithnate",
|
||||||
|
"platformCreatorUrl": "https://creators.spotify.com/pod/profile/nmemmert/",
|
||||||
|
"welcomeEmailSubject": "Welcome to Verse by Verse with Nate",
|
||||||
|
"welcomeEmailGreetingPrefix": "Glad you're here",
|
||||||
|
"welcomeEmailIntro": "Thanks for subscribing to Verse by Verse with Nate - a Bible teaching podcast where we slow down, dig into the text, and pull out the nuggets God has for us word by word.",
|
||||||
|
"welcomeEmailCurrentSeries": "Right now we're working through the book of Titus - a short letter packed with practical wisdom about grace, godliness, and what the Christian life looks like when it's rooted in sound doctrine.",
|
||||||
|
"welcomeEmailStartHereTitle": "Episode 1 - Introduction to Titus",
|
||||||
|
"welcomeEmailStartHereSummary": "Who wrote it, who received it, and why it still matters.",
|
||||||
|
"welcomeEmailStartHereUrl": "/start-here",
|
||||||
|
"welcomeEmailSpotifyUrl": "/spotify",
|
||||||
|
"welcomeEmailAppleUrl": "/apple",
|
||||||
|
"welcomeEmailAmazonUrl": "/amazon",
|
||||||
|
"welcomeEmailWebsiteUrl": "https://versebyversewithnate.us",
|
||||||
|
"welcomeEmailImageUrl": "/images/podcast-art.jpeg",
|
||||||
|
"welcomeEmailWhatToExpect1": "Verse-by-verse teaching - we go slow and let the text speak for itself.",
|
||||||
|
"welcomeEmailWhatToExpect2": "Greek word studies - the kind that open up meaning without being a lecture.",
|
||||||
|
"welcomeEmailWhatToExpect3": "New episodes + study notes delivered right to your inbox.",
|
||||||
|
"welcomeEmailScripture": "For the grace of God has appeared, bringing salvation to all people.",
|
||||||
|
"welcomeEmailScriptureRef": "Titus 2:11 - BSB",
|
||||||
|
"welcomeEmailSignoff": "Grace and peace,\nNate",
|
||||||
|
"headerFollowLabel": "Follow on Spotify",
|
||||||
|
"cookieBannerText": "We use optional analytics cookies to measure visits and location trends for site improvement.",
|
||||||
|
"customLinks": [],
|
||||||
"customBlocks": [],
|
"customBlocks": [],
|
||||||
"archivedSeries": [],
|
"archivedSeries": [],
|
||||||
"redirects": [
|
"redirects": [
|
||||||
@@ -78,17 +140,15 @@
|
|||||||
],
|
],
|
||||||
"podcastFeaturedLinks": [
|
"podcastFeaturedLinks": [
|
||||||
{
|
{
|
||||||
"id": "sample-highlight-1",
|
"id": "movn6tk3",
|
||||||
"title": "Introduction to Titus",
|
"title": "news",
|
||||||
"episodeNumber": "2",
|
"episodeNumber": "1",
|
||||||
"summary": "A practical walkthrough of context, structure, and application for this study.",
|
"summary": "news",
|
||||||
"url": "https://open.spotify.com/episode/3mpFfJQP6avAhzxu4BTKvR?si=cl_Dm_2vQraiboouaoINaw",
|
"url": "",
|
||||||
"embedUrl": "",
|
"embedUrl": "https://open.spotify.com/embed/episode/4fDzD2q9XtVtOsOq0xlgLn?utm_source=generator",
|
||||||
"showNotes": "",
|
"showNotes": "test",
|
||||||
"discussionQuestions": [
|
"discussionQuestions": [
|
||||||
"1. What do you already know about Paul, Titus, or the island of Crete? And what — if anything — surprised you about the background of this letter?",
|
"tes"
|
||||||
"2. The letter’s core theme is that sound doctrine leads to godly living. Where have you seen that connection — or its breakdown — in your own experience?",
|
|
||||||
"3. Paul trusted Titus with a hard assignment. Who has trusted you with something difficult — and what did that trust mean to you?"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -123,5 +183,5 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updatedAt": "2026-05-04T17:42:41.949Z"
|
"updatedAt": "2026-05-07T15:35:23.896Z"
|
||||||
}
|
}
|
||||||
+7
-3
@@ -1,11 +1,13 @@
|
|||||||
[
|
{
|
||||||
|
"questions": [
|
||||||
{
|
{
|
||||||
"id": "cb_001",
|
"id": "cb_001",
|
||||||
"firstName": "Nate",
|
"firstName": "Nate",
|
||||||
"question": "What Bible translation do you use?",
|
"question": "What Bible translation do you use?",
|
||||||
"answer": "In the podcast, I primarily teach from the BSB (Berean Standard Bible). I may compare other translations at times, but BSB is my main teaching text in the Verse by Verse episodes.",
|
"answer": "In the podcast, I primarily teach from the BSB (Berean Standard Bible). I may compare other translations at times, but BSB is my main teaching text in the Verse by Verse episodes.",
|
||||||
"isApproved": true,
|
"isApproved": true,
|
||||||
"topic": "Bible Study"
|
"topic": "Bible Study",
|
||||||
|
"answeredAt": "2026-05-07T13:36:16.859Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cb_002",
|
"id": "cb_002",
|
||||||
@@ -95,4 +97,6 @@
|
|||||||
"isApproved": true,
|
"isApproved": true,
|
||||||
"topic": "Faith & Life"
|
"topic": "Faith & Life"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"updatedAt": "2026-05-07T15:35:23.896Z"
|
||||||
|
}
|
||||||
@@ -2702,6 +2702,130 @@ async function fetchAllEpisodes() {
|
|||||||
return episodes
|
return episodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toSpotifyEpisodeEmbedUrl(urlValue) {
|
||||||
|
if (!urlValue) return ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(urlValue)
|
||||||
|
if (parsed.protocol !== 'https:') return ''
|
||||||
|
|
||||||
|
const host = parsed.hostname.toLowerCase()
|
||||||
|
const parts = parsed.pathname.split('/').filter(Boolean)
|
||||||
|
|
||||||
|
if (host === 'open.spotify.com') {
|
||||||
|
if (parts[0] === 'embed' && parts[1] === 'episode' && parts[2]) {
|
||||||
|
return `https://open.spotify.com/embed/episode/${parts[2]}?utm_source=generator`
|
||||||
|
}
|
||||||
|
if (parts[0] === 'episode' && parts[1]) {
|
||||||
|
return `https://open.spotify.com/embed/episode/${parts[1]}?utm_source=generator`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractSpotifyEpisodeIdFromCreatorHtml(html, sourceUrl) {
|
||||||
|
const input = String(html || '')
|
||||||
|
if (!input) return ''
|
||||||
|
|
||||||
|
const sourceEpisodeSlug = /-([A-Za-z0-9]+)(?:\/|$)/.exec(sourceUrl)?.[1] ?? ''
|
||||||
|
|
||||||
|
const blockRegex = /"episodeId":"([^"]+)"[\s\S]*?"spotifyUrl":"([^"]+)"/g
|
||||||
|
let match
|
||||||
|
let firstEpisodeId = ''
|
||||||
|
while ((match = blockRegex.exec(input)) !== null) {
|
||||||
|
const episodeSlug = match[1]
|
||||||
|
const spotifyUrl = decodeEscapedJsonUrl(match[2])
|
||||||
|
const episodeId = /\/episode\/([A-Za-z0-9]+)/.exec(spotifyUrl)?.[1]
|
||||||
|
|
||||||
|
if (!firstEpisodeId && episodeId) firstEpisodeId = episodeId
|
||||||
|
if (sourceEpisodeSlug && episodeSlug === sourceEpisodeSlug && episodeId) {
|
||||||
|
return episodeId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstEpisodeId) return firstEpisodeId
|
||||||
|
|
||||||
|
const urlMatch = /"spotifyUrl":"(https:\\u002F\\u002Fopen\.spotify\.com\\u002Fepisode\\u002F([A-Za-z0-9]+))/.exec(input)
|
||||||
|
return urlMatch ? (urlMatch[2] || '') : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedSpotifyResolverHost(hostname) {
|
||||||
|
const host = String(hostname || '').toLowerCase()
|
||||||
|
return host === 'open.spotify.com'
|
||||||
|
|| host === 'creators.spotify.com'
|
||||||
|
|| host === 'anchor.fm'
|
||||||
|
|| host === 'podcasters.spotify.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeEscapedJsonUrl(value) {
|
||||||
|
return String(value || '').replace(/\\u002F/g, '/').replace(/\\\//g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/api/spotify/embed-url', async (req, res) => {
|
||||||
|
const incoming = typeof req.query.url === 'string' ? req.query.url.trim() : ''
|
||||||
|
const safeInput = sanitizeUrl(incoming)
|
||||||
|
|
||||||
|
if (!safeInput || safeInput.startsWith('/')) {
|
||||||
|
res.status(400).json({ message: 'A valid episode URL is required.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed
|
||||||
|
try {
|
||||||
|
parsed = new URL(safeInput)
|
||||||
|
} catch {
|
||||||
|
res.status(400).json({ message: 'Malformed URL.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.protocol !== 'https:' || !isAllowedSpotifyResolverHost(parsed.hostname)) {
|
||||||
|
res.status(400).json({ message: 'Unsupported episode URL host.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const directEmbed = toSpotifyEpisodeEmbedUrl(safeInput)
|
||||||
|
if (directEmbed) {
|
||||||
|
res.json({ embedUrl: directEmbed, resolvedFrom: 'direct' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 8000)
|
||||||
|
const response = await fetch(safeInput, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Siteforge/1.0 (+https://versebyversewithnate.us)',
|
||||||
|
Accept: 'text/html',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
res.status(404).json({ message: 'Could not fetch episode page.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await response.text()
|
||||||
|
const spotifyEpisodeId = extractSpotifyEpisodeIdFromCreatorHtml(html, safeInput)
|
||||||
|
|
||||||
|
if (!spotifyEpisodeId) {
|
||||||
|
res.status(404).json({ message: 'Could not resolve Spotify episode ID from page.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const embedUrl = `https://open.spotify.com/embed/episode/${spotifyEpisodeId}?utm_source=generator`
|
||||||
|
res.json({ embedUrl, resolvedFrom: 'page-fetch' })
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[spotify/embed-url] resolve error:', err.message)
|
||||||
|
res.status(500).json({ message: 'Could not resolve Spotify embed URL right now.' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
app.get('/api/episodes', async (_req, res) => {
|
app.get('/api/episodes', async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
const episodes = await fetchAllEpisodes()
|
const episodes = await fetchAllEpisodes()
|
||||||
|
|||||||
+117
-3
@@ -13,6 +13,48 @@ const SPOTIFY_EMBED_URL =
|
|||||||
const CONSENT_KEY = 'vbn_analytics_consent_choice'
|
const CONSENT_KEY = 'vbn_analytics_consent_choice'
|
||||||
const HEADLINER_WIDGET_ID = 'WI_cmou3b4q7000701p0o9qmmcfj'
|
const HEADLINER_WIDGET_ID = 'WI_cmou3b4q7000701p0o9qmmcfj'
|
||||||
|
|
||||||
|
function toSpotifyEpisodeEmbedUrl(url: string | undefined): string {
|
||||||
|
if (!url) return ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url)
|
||||||
|
const host = parsed.hostname.toLowerCase()
|
||||||
|
const parts = parsed.pathname.split('/').filter(Boolean)
|
||||||
|
|
||||||
|
if (host === 'open.spotify.com') {
|
||||||
|
if (parts[0] === 'embed' && parts[1] === 'episode' && parts[2]) {
|
||||||
|
return `https://open.spotify.com/embed/episode/${parts[2]}?utm_source=generator`
|
||||||
|
}
|
||||||
|
if (parts[0] === 'episode' && parts[1]) {
|
||||||
|
return `https://open.spotify.com/embed/episode/${parts[1]}?utm_source=generator`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikelySpotifyEpisodeUrl(url: string | undefined): boolean {
|
||||||
|
if (!url) return false
|
||||||
|
if (toSpotifyEpisodeEmbedUrl(url)) return true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url)
|
||||||
|
const host = parsed.hostname.toLowerCase()
|
||||||
|
const path = parsed.pathname.toLowerCase()
|
||||||
|
|
||||||
|
if (host === 'creators.spotify.com' && path.includes('/episodes/')) return true
|
||||||
|
if (host === 'anchor.fm' && path.includes('/episodes/')) return true
|
||||||
|
if (host === 'podcasters.spotify.com' && path.includes('/episodes/')) return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
function HeadlinerWidget() {
|
function HeadlinerWidget() {
|
||||||
const [iframeSrc, setIframeSrc] = useState('')
|
const [iframeSrc, setIframeSrc] = useState('')
|
||||||
const [status, setStatus] = useState<'loading' | 'ready' | 'empty'>('loading')
|
const [status, setStatus] = useState<'loading' | 'ready' | 'empty'>('loading')
|
||||||
@@ -683,7 +725,12 @@ function PodcastHighlightsSection({ content }: { content: SiteContent }) {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="resources-list">
|
<div className="resources-list">
|
||||||
{links.map(link => {
|
{links.map(link => {
|
||||||
const hasRichContent = !!(link.embedUrl || (link.discussionQuestions ?? []).length > 0 || link.showNotes)
|
const hasRichContent = !!(
|
||||||
|
link.embedUrl
|
||||||
|
|| isLikelySpotifyEpisodeUrl(link.url)
|
||||||
|
|| (link.discussionQuestions ?? []).length > 0
|
||||||
|
|| link.showNotes
|
||||||
|
)
|
||||||
const label = <>{link.episodeNumber ? `Ep. ${link.episodeNumber}: ` : ''}{link.title || link.url}</>
|
const label = <>{link.episodeNumber ? `Ep. ${link.episodeNumber}: ` : ''}{link.title || link.url}</>
|
||||||
// Use external link only when there's a url AND no rich detail-page content
|
// Use external link only when there's a url AND no rich detail-page content
|
||||||
if (!hasRichContent && link.url) {
|
if (!hasRichContent && link.url) {
|
||||||
@@ -1004,6 +1051,73 @@ function LandingPage({ content }: { content: SiteContent }) {
|
|||||||
function EpisodeDetailPage({ content }: { content: SiteContent }) {
|
function EpisodeDetailPage({ content }: { content: SiteContent }) {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
const episode = (content.podcastFeaturedLinks ?? []).find(e => e.id === id)
|
const episode = (content.podcastFeaturedLinks ?? []).find(e => e.id === id)
|
||||||
|
const [resolvedEmbedUrl, setResolvedEmbedUrl] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
if (!episode) {
|
||||||
|
setResolvedEmbedUrl('')
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const manualEmbed = episode.embedUrl?.trim() ?? ''
|
||||||
|
if (manualEmbed) {
|
||||||
|
setResolvedEmbedUrl(manualEmbed)
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!episode.url) {
|
||||||
|
setResolvedEmbedUrl('')
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(episode.url)
|
||||||
|
const host = urlObj.hostname.toLowerCase()
|
||||||
|
|
||||||
|
if (host === 'open.spotify.com') {
|
||||||
|
const fromUrl = toSpotifyEpisodeEmbedUrl(episode.url)
|
||||||
|
if (fromUrl) {
|
||||||
|
setResolvedEmbedUrl(fromUrl)
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLikelySpotifyEpisodeUrl(episode.url)) {
|
||||||
|
setResolvedEmbedUrl('')
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setResolvedEmbedUrl('')
|
||||||
|
fetch(`/api/spotify/embed-url?url=${encodeURIComponent(episode.url)}`)
|
||||||
|
.then(r => (r.ok ? r.json() : Promise.reject(new Error('embed resolve failed'))))
|
||||||
|
.then((data: { embedUrl?: string }) => {
|
||||||
|
if (cancelled) return
|
||||||
|
setResolvedEmbedUrl(data.embedUrl?.trim() ?? '')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (cancelled) return
|
||||||
|
setResolvedEmbedUrl('')
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [episode])
|
||||||
|
|
||||||
if (!episode) {
|
if (!episode) {
|
||||||
return (
|
return (
|
||||||
@@ -1037,10 +1151,10 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
|
|||||||
<h1 className="episode-detail-title">{episode.title}</h1>
|
<h1 className="episode-detail-title">{episode.title}</h1>
|
||||||
{episode.summary && <p className="episode-detail-summary">{episode.summary}</p>}
|
{episode.summary && <p className="episode-detail-summary">{episode.summary}</p>}
|
||||||
|
|
||||||
{episode.embedUrl && (
|
{resolvedEmbedUrl && (
|
||||||
<div className="episode-detail-embed">
|
<div className="episode-detail-embed">
|
||||||
<iframe
|
<iframe
|
||||||
src={episode.embedUrl}
|
src={resolvedEmbedUrl}
|
||||||
title={episode.title}
|
title={episode.title}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="152"
|
height="152"
|
||||||
|
|||||||
Reference in New Issue
Block a user