Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 287fa72462 | |||
| 50459b7234 | |||
| 36594d5e16 | |||
| 0e9d112c44 | |||
| 063f5d2a0e | |||
| 6e8b360398 | |||
| 770d2357ac | |||
| f07d8fecfb | |||
| dcfce9dc24 | |||
| 738cc2a412 | |||
| bac22950ba | |||
| 85af1a29ad | |||
| 85e8a298fd | |||
| a252167052 | |||
| 53408ad124 | |||
| d5f15486d8 | |||
| afb80aaf2a | |||
| 3a9d11cf40 | |||
| 2653d95605 | |||
| 7772bfd6a6 | |||
| 74650a39b3 | |||
| c46d765759 | |||
| d95ed9cd8d | |||
| 4b01d01133 | |||
| e93840c25f | |||
| 5b401671db | |||
| f156257573 |
@@ -0,0 +1,23 @@
|
|||||||
|
# Copy this file to .env and fill in your real values.
|
||||||
|
# Never commit .env to version control.
|
||||||
|
|
||||||
|
# Resend API key for the contact form
|
||||||
|
# 1. Sign up free at https://resend.com
|
||||||
|
# 2. Go to API Keys and create a new key
|
||||||
|
# 3. Paste it below
|
||||||
|
RESEND_API_KEY=re_your_api_key_here
|
||||||
|
|
||||||
|
# Optional: use a separate key for Contacts/Segments if your main key is send-only
|
||||||
|
RESEND_CONTACTS_API_KEY=
|
||||||
|
|
||||||
|
# Email address that receives contact form submissions
|
||||||
|
RESEND_TO=hello@versebyversewithnate.us
|
||||||
|
|
||||||
|
# Optional: automatically add contact form submitters to a Resend Segment
|
||||||
|
# Create a Segment in Resend and paste its ID here if you want new contacts grouped.
|
||||||
|
RESEND_SEGMENT_ID=
|
||||||
|
|
||||||
|
# "From" address shown on received emails.
|
||||||
|
# During testing you can leave this as-is (uses Resend's shared domain).
|
||||||
|
# For production: verify your own domain at resend.com/domains and change this.
|
||||||
|
RESEND_FROM=Verse by Verse with Nate <hello@versebyversewithnate.us>
|
||||||
@@ -17,6 +17,7 @@ dist-ssr
|
|||||||
# the image default and is preserved here for the Docker build context only.
|
# the image default and is preserved here for the Docker build context only.
|
||||||
# Uncomment the line below if you do NOT want to track live data in git.
|
# Uncomment the line below if you do NOT want to track live data in git.
|
||||||
# data/admin-content.json
|
# data/admin-content.json
|
||||||
|
data/backups/
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
@@ -50,13 +50,21 @@ docker compose pull
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Admin auth:
|
||||||
|
|
||||||
|
- Set `ADMIN_PASSWORD` on the server/container to protect `/admin` and admin stats/maintenance endpoints.
|
||||||
|
- Without `ADMIN_PASSWORD`, admin login is disabled until configured.
|
||||||
|
|
||||||
The app will be available at `http://localhost:4173`.
|
The app will be available at `http://localhost:4173`.
|
||||||
|
|
||||||
Persistent admin saves:
|
Persistent admin saves:
|
||||||
|
|
||||||
- Admin updates are written to `data/admin-content.json`.
|
- Admin updates are written to `data/admin-content.json`.
|
||||||
|
- Built-in page hit stats are written to `data/hit-stats.json`.
|
||||||
|
- Detailed visitor analytics are written to `data/visitor-stats.json`.
|
||||||
|
- Backup snapshots are written to `data/backups/`.
|
||||||
- `docker-compose.yml` mounts `./data` into the container at `/app/data`.
|
- `docker-compose.yml` mounts `./data` into the container at `/app/data`.
|
||||||
- This keeps your edits after container restarts/rebuilds.
|
- This keeps all admin-managed data after container restarts/rebuilds/updates.
|
||||||
|
|
||||||
## Where to edit content
|
## Where to edit content
|
||||||
|
|
||||||
@@ -80,6 +88,12 @@ Persistent admin saves:
|
|||||||
- Enter a URL and click **Scan URL metadata** to auto-fill title, summary, domain, category, and icon when available.
|
- Enter a URL and click **Scan URL metadata** to auto-fill title, summary, domain, category, and icon when available.
|
||||||
- Click **Save project** to apply updates instantly.
|
- Click **Save project** to apply updates instantly.
|
||||||
- Saved edits are written to `data/admin-content.json` through the API server.
|
- Saved edits are written to `data/admin-content.json` through the API server.
|
||||||
|
- Built-in stats in `/admin` include page hits plus visitor details (IP, country/state/county/city, returning visitors, and recent visitor log).
|
||||||
|
- Site Stats in `/admin` includes a Bible Questions inbox sourced from contact form submissions marked as Bible Question.
|
||||||
|
- Analytics cookies are consent-based. Visitors can accept or decline tracking from the site banner.
|
||||||
|
- Admin now includes maintenance actions: **Export JSON**, **Backup Now**, **Prune Old Data**, and **Clear Analytics**.
|
||||||
|
- Admin also supports restoring from a backup snapshot from `/admin`.
|
||||||
|
- The server creates startup + daily backup snapshots and retains recent backups automatically.
|
||||||
- Use the **Theme** dropdown to switch between Sandstone, Ocean, Midnight, Forest, and Sunset.
|
- Use the **Theme** dropdown to switch between Sandstone, Ocean, Midnight, Forest, and Sunset.
|
||||||
- Use **Remove project** to delete the selected project from your local Admin data.
|
- Use **Remove project** to delete the selected project from your local Admin data.
|
||||||
- Use **Reset project** or **Reset all** to restore defaults from `src/data/projects.ts`.
|
- Use **Reset project** or **Reset all** to restore defaults from `src/data/projects.ts`.
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
+8
-86
@@ -5,98 +5,20 @@
|
|||||||
"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.",
|
||||||
"aboutNate": "Nate Emmert is a husband, dad, and lifelong student of the Bible from Nashville, TN. He created Verse by Verse to share the joy of deep Scripture study in a format anyone can follow along with — no seminary required.",
|
"aboutNate": "Nate Emmert is a husband, dad, and lifelong student of the Bible from Lynchburg, Va. He created Verse by Verse to share the joy of deep Scripture study in a format anyone can follow along with — no seminary required.",
|
||||||
"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": "/images/titus-cover.png",
|
||||||
|
"seriesListenUrl": "https://open.spotify.com/show/0Gq1TzoJOdReSZ1gYQi8Xl",
|
||||||
"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.",
|
||||||
"studyGuideUrl": "https://a.co/d/01sG2tOJ",
|
"studyGuideUrl": "https://a.co/d/01sG2tOJ",
|
||||||
"shareHeading": "Know someone who'd love this?",
|
"shareHeading": "Help one more person hear the Word this week.",
|
||||||
"shareP": "Scan the QR code or share the link to introduce a friend to Verse by Verse with Nate.",
|
"shareP": "Scan the QR code or text the show link to a friend who needs encouragement today.",
|
||||||
"customLinks": [],
|
"customLinks": [],
|
||||||
"customBlocks": []
|
"customBlocks": [],
|
||||||
|
"archivedSeries": []
|
||||||
},
|
},
|
||||||
"updatedAt": "2026-04-09T00:00:00.000Z",
|
"updatedAt": "2026-04-16T12:13:53.095Z"
|
||||||
"_legacy_projects": [
|
|
||||||
{
|
|
||||||
"slug": "verse-by-verse",
|
|
||||||
"title": "Verse by Verse with Nate",
|
|
||||||
"domain": "spotify.com",
|
|
||||||
"url": "https://creators.spotify.com/pod/profile/nmemmert/",
|
|
||||||
"category": "Podcast",
|
|
||||||
"access": "Public",
|
|
||||||
"ownership": "hosted",
|
|
||||||
"status": "Live",
|
|
||||||
"summary": "A verse-by-verse podcast from Nate Emmert focused on Scripture, story, and practical faith application.",
|
|
||||||
"details": "Weekly episodes explore Scripture one verse at a time, mixing theological insight, devotional reflection, and real-life application.",
|
|
||||||
"features": [
|
|
||||||
"Verse-by-verse teaching",
|
|
||||||
"Weekly podcast episodes",
|
|
||||||
"Practical reflection"
|
|
||||||
],
|
|
||||||
"scanSource": "manual",
|
|
||||||
"featured": {
|
|
||||||
"headline": "Podcast teaching shaped around verse-by-verse discovery.",
|
|
||||||
"problem": "Bible study and podcast listening often feel disconnected from each other and from everyday life.",
|
|
||||||
"solution": "Verse by Verse with Nate brings Scripture study and podcast conversation together in a format that is easy to follow and apply.",
|
|
||||||
"stack": [
|
|
||||||
"Podcast hosting",
|
|
||||||
"Spotify",
|
|
||||||
"Verse-by-verse teaching"
|
|
||||||
],
|
|
||||||
"highlights": [
|
|
||||||
"Episode guides",
|
|
||||||
"Scripture focus",
|
|
||||||
"Practical application"
|
|
||||||
],
|
|
||||||
"nextSteps": [
|
|
||||||
"Add episode notes",
|
|
||||||
"Add guest interviews",
|
|
||||||
"Link transcripts and study resources"
|
|
||||||
],
|
|
||||||
"updateNote": "Edit this content in data/admin-content.json for the Verse by Verse podcast project."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"siteContent": {
|
|
||||||
"theme": "sand",
|
|
||||||
"homeEyebrow": "Verse by Verse with Nate",
|
|
||||||
"homeTitle": "Podcast episodes, Scripture study, and weekly reflections",
|
|
||||||
"homeIntro": "A podcast experience centered on verse-by-verse conversation, prayerful application, and practical faith rhythms. Explore episodes, show notes, and the latest reflections from Nate.",
|
|
||||||
"projectEyebrow": "Featured Episode",
|
|
||||||
"quickTipsTitle": "Why listeners stay engaged",
|
|
||||||
"quickTipsBody": "Each episode focuses on clear Scripture teaching, practical application, and encouragement you can carry into the week.",
|
|
||||||
"downloadsTitle": "Listen Everywhere",
|
|
||||||
"downloadsIntro": "Use these links to follow the show, share episodes, and stay connected with Verse by Verse with Nate.",
|
|
||||||
"downloads": [
|
|
||||||
{
|
|
||||||
"title": "Spotify Creator Profile",
|
|
||||||
"url": "https://creators.spotify.com/pod/profile/nmemmert/",
|
|
||||||
"description": "Open the official show profile and listen to the latest episodes."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"showSidebar": false,
|
|
||||||
"showWeatherWidget": false,
|
|
||||||
"showMusicBar": false,
|
|
||||||
"homeSections": [
|
|
||||||
{
|
|
||||||
"id": "summary",
|
|
||||||
"placement": "main"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "projects",
|
|
||||||
"placement": "main"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "quickTips",
|
|
||||||
"placement": "main"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "downloads",
|
|
||||||
"placement": "main"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"updatedAt": "2026-04-09T00:00:00.000Z"
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,542 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cb_001",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "What Bible translation do you use?",
|
||||||
|
"content": "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.",
|
||||||
|
"keywords": [
|
||||||
|
"bible",
|
||||||
|
"translation",
|
||||||
|
"bsb",
|
||||||
|
"berean standard bible",
|
||||||
|
"version",
|
||||||
|
"podcast",
|
||||||
|
"teaching"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.272Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_002",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "How do you approach a difficult or confusing Bible passage?",
|
||||||
|
"content": "I always start by reading the surrounding context — a confusing verse often makes perfect sense once you zoom out a chapter or two. Then I look at who the author was, who they were writing to, and what was happening historically. A good study Bible or a commentary like Matthew Henry's can be a huge help. Most importantly, pray for understanding before you dive in. The Holy Spirit is your best guide.",
|
||||||
|
"keywords": [
|
||||||
|
"difficult",
|
||||||
|
"confusing",
|
||||||
|
"passage",
|
||||||
|
"context",
|
||||||
|
"commentary",
|
||||||
|
"study",
|
||||||
|
"interpretation",
|
||||||
|
"hard"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.272Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_003",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "How do I stay consistent with daily Bible reading?",
|
||||||
|
"content": "Start small and protect that time like an appointment. Even 10 minutes in the morning before checking your phone can be transformative over a year. A reading plan helps a lot — there are great ones on apps like YouVersion that break the whole Bible into daily chunks so you never have to decide what to read. And give yourself grace on the days you miss; guilt is not a good motivator. Just pick back up where you left off.",
|
||||||
|
"keywords": [
|
||||||
|
"daily",
|
||||||
|
"reading",
|
||||||
|
"consistent",
|
||||||
|
"habit",
|
||||||
|
"bible",
|
||||||
|
"plan",
|
||||||
|
"youversion",
|
||||||
|
"devotional",
|
||||||
|
"routine",
|
||||||
|
"morning"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.272Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_005",
|
||||||
|
"type": "topic",
|
||||||
|
"title": "About the Verse by Verse with Nate Podcast",
|
||||||
|
"content": "Verse by Verse with Nate is a podcast dedicated to walking through books of the Bible chapter by chapter and verse by verse. The goal is to help everyday listeners develop a deep, contextual understanding of Scripture — not just surface-level summaries, but the meaning behind the words. New episodes are released regularly. You can listen on Spotify, Apple Podcasts, and other major platforms.",
|
||||||
|
"keywords": [
|
||||||
|
"podcast",
|
||||||
|
"about",
|
||||||
|
"verse by verse",
|
||||||
|
"nate",
|
||||||
|
"listen",
|
||||||
|
"show",
|
||||||
|
"episodes",
|
||||||
|
"what is"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.272Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_006",
|
||||||
|
"type": "topic",
|
||||||
|
"title": "How to listen to the podcast",
|
||||||
|
"content": "You can listen to Verse by Verse with Nate on Spotify or Apple Podcasts. Just search 'Verse by Verse with Nate' in your favorite podcast app. New episodes drop regularly — subscribing will make sure you never miss one. If you'd like to leave a review, that helps more people find the show!",
|
||||||
|
"keywords": [
|
||||||
|
"listen",
|
||||||
|
"subscribe",
|
||||||
|
"spotify",
|
||||||
|
"apple podcasts",
|
||||||
|
"find",
|
||||||
|
"where",
|
||||||
|
"app",
|
||||||
|
"review"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.272Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_007",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "How do I submit a question to Nate?",
|
||||||
|
"content": "You can submit a question using the contact form at the bottom of this page. Select 'Bible Question' as the message type. Nate reads every question personally and answers the best ones on the site's Q&A section or in a future episode. There's no guarantee every question gets a public response, but all are read and appreciated!",
|
||||||
|
"keywords": [
|
||||||
|
"submit",
|
||||||
|
"question",
|
||||||
|
"contact",
|
||||||
|
"ask",
|
||||||
|
"send",
|
||||||
|
"form",
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.272Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_008",
|
||||||
|
"type": "topic",
|
||||||
|
"title": "Prayer and quiet time",
|
||||||
|
"content": "Prayer is simply talking to God — it doesn't have to be formal or long. A quiet time usually involves reading a passage of Scripture, reflecting on what it means for your life today, and spending a few minutes in prayer. You must make sure that you stay in context of what Scripture says and not put your own thoughts or words into what the Bible is saying. Many people find journaling helpful. The key is consistency, not perfection. Even five minutes a day done faithfully is far better than an hour once a week.",
|
||||||
|
"keywords": [
|
||||||
|
"prayer",
|
||||||
|
"quiet time",
|
||||||
|
"devotional",
|
||||||
|
"pray",
|
||||||
|
"journal",
|
||||||
|
"meditate",
|
||||||
|
"reflect",
|
||||||
|
"morning",
|
||||||
|
"time with god"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.272Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "167fa083-5438-44a5-ac72-6dd379e937f5",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 2 — Introduction to Titus (Background & Overview)",
|
||||||
|
"content": "WHO WAS PAUL? Let’s start at the beginning. The letter opens in verse 1 with the words: Paul, a servant of God and an apostle of Jesus Christ for the faith of God’s elect and their knowledge of the truth that leads to godliness, Now, if you’ve spent any time in the New Testament, you’ve heard of Paul. But let me give you the short version, because I think it matters here. Paul — originally named Saul — was a man who, by his own description, was a Hebrew of Hebrews. He was a Pharisee. He was educated, zealous, and deeply committed to the law of Moses. And for a season of his life, that zeal led him to do something he would carry with him for the rest of his days: he persecuted the early church. He approved of the stoning of Stephen. He dragged men and women out of their homes and had them thrown into prison. And then — on a road to Damascus — everything changed. He met the risen Jesus. And the man who was once the church’s greatest enemy became its most tireless ambassador. Paul went on to plant churches across the Roman world. He traveled through modern-day Turkey, Greece, Macedonia. He wrote letters that would become a third of our New Testament. He was beaten, shipwrecked, imprisoned — and through all of it, he kept going. Why does this matter for Titus? Because the man writing this letter is not writing from a place of comfort or theory. Paul understood, in the most personal way possible, what it meant to be completely transformed by grace. That’s going to come through on every page. WHO WAS TITUS? Now, who is Titus? Titus doesn’t get as much attention as, say, Timothy — but he should. He’s one of Paul’s closest and most trusted co-workers, and the more you look at him, the more impressive he becomes. Here’s what we know: Titus was a Gentile. He wasn’t Jewish. He came to faith through Paul’s ministry — which is why Paul, in verse 4 of this very letter, calls him: To Titus, my true child in our common faith: Grace and peace from God the Father and Christ Jesus our Savior. That phrase is loaded. Paul is saying: you and I share the same gospel. You are my son in the faith. If you want to understand Titus better, look at a few other places in Paul’s letters. In 2 Corinthians chapter 7, Paul describes being comforted by the arrival of Titus during a particularly difficult season. In chapter 8, he sends Titus to help organize a relief offering for struggling believers in Jerusalem. And in Galatians chapter 2, Titus shows up as something of a test case — proof, in human form, that the gospel is for everyone. This is a man Paul trusted with hard things. And that’s important — because what he’s being asked to do in Crete is very hard. WHERE IS CRETE? Which brings us to the island. Crete. If you look at a map of the Mediterranean, you’ll find it in the southern Aegean Sea — a large island stretching about 160 miles from east to west. It was a well-known place in the ancient world. It had a long history, a busy coastline, and a reputation. And not necessarily a good one. Paul actually quotes a Cretan poet in this very letter — we’ll get to that in chapter 1. Crete had a reputation for dishonesty, for moral looseness, for people who were, frankly, difficult to work with. The cultural environment was a challenging one for a young church trying to take root. There were also false teachers mixing Jewish influences with the faith in unhealthy ways. Paul is going to address that head-on. So picture Titus: a Gentile pastor, on a difficult island, in a church still finding its footing, surrounded by cultural pressures and false teachers. This letter is his assignment briefing. WHY WAS THE LETTER WRITTEN? In verse 5, Paul gets right to the point: The reason I left you in Cr",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 2",
|
||||||
|
"titus 2:11",
|
||||||
|
"introduction",
|
||||||
|
"background",
|
||||||
|
"overview",
|
||||||
|
"crete",
|
||||||
|
"letter"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.315Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.272Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "881279c7-3ca4-436d-a584-19cc0a5a9bf3",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 3 — Who Is Paul, and Why Does This Letter Exist? (Titus 1:1– 4)",
|
||||||
|
"content": "SERVANT AND APOSTLE Let’s start by reading Titus Chapter 1 Verses 1-4 I am reading from The Berean Standard Bible ( BSB) . 1 Paul, a servant of God and an apostle of Jesus Christ for the faith of God’s elect and their knowledge of the truth that leads to godliness, 2 in the hope of eternal life, which God, who cannot lie, promised before time began. 3 In His own time He has made His word evident in the proclamation entrusted to me by the command of God our Savior. 4 To Titus, my true child in our common faith: Grace and peace from God the Father and Christ Jesus our Savior. Paul opens with two words to describe himself: servant and apostle. The order matters, and the pairing is intentional. Apostle is a word of authority — a commissioned representative sent with the full weight of the sender’s backing behind them. Paul is saying: I speak on behalf of Jesus Christ. What I write carries weight. But before he says apostle, he says servant. In the Greek, the word is doulos (doo'- los ) — bondservant – a person who chooses to be a servant one who belongs entirely to another. Paul is saying: before anything else, I am owned. Everything I do flows from submission to the God I serve. This combination — servant and apostle — is a picture of what healthy spiritual authority looks like. It is never raw power. It is authority that comes from below — from a posture of submission — exercised in service, not dominance. Paul is modeling for Titus, right here in the opening line, what it looks like to lead. You don’t choose between humility and authority. You hold both. At the same time. TRUTH THAT LEADS TO GODLINESS In verse 1, Paul describes his mission as being for “the faith of God’s elect and their knowledge of the truth that leads to godliness.” That last phrase is the key: truth that leads to godliness. Paul is not interested in truth for its own sake — collecting correct theological propositions the way some people collect rare books, impressive on the shelf but never actually read. For Paul, truth has a direction. Truth moves. Truth produces something. What does it produce? Godliness. A life shaped by what it believes. This is the big theme of the entire letter — sound doctrine and transformed living are not separate categories. They are inseparable. The truth you know should be shaping the way you live. If it isn’t — if your theology is just mental furniture — something has gone wrong. BEFORE TIME BEGAN Now we get to one of the most stunning phrases in the entire letter. Verse 2: …in the hope of eternal life, which God, who cannot lie, promised before time began. Before time began. Let that land for a second. Paul is saying that the plan of salvation was not an afterthought. It was not Plan B. It was not God improvising a rescue after the fall. It was a promise made before the universe existed. Before the first atom, the first star, the first breath — God had already determined to save a people for Himself. And notice what Paul says about the God who made that promise: He cannot lie. Not He does not lie. He cannot lie. It is outside His nature. It is impossible. That means the promise of eternal life is not hanging on your faithfulness, or your feelings, or whether you had a good week. It is hanging on the character of a God who is constitutionally incapable of breaking His word. For Titus, laboring on a difficult island with a struggling church and real opposition — this is the ground he stands on. Not his own strength. The unchangeable promise of an undeceivable God. ENTRUSTED BY COMMAND Verse 3 adds one more layer to Paul’s self-understanding: …In His own time He has made His word evident in the proclamation entrusted to me by the",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 3",
|
||||||
|
"titus 1:1– 4",
|
||||||
|
"titus 1:1",
|
||||||
|
"paul",
|
||||||
|
"does",
|
||||||
|
"this",
|
||||||
|
"letter",
|
||||||
|
"exist"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.347Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e0ad268f-2826-41a5-b078-90f90aeda21b",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 4 — What Does a Faithful Leader Look Like? (Titus 1:5–9)",
|
||||||
|
"content": "WHY ELDERS FIRST? Remember where we are in the letter. Paul has just finished his opening and the very first practical instruction he gives Titus is this: The reason I left you in Crete was that you would set in order what was unfinished and appoint elders in every town, as I directed you. Appoint elders. That’s the first thing. Because a church without mature, qualified leadership is vulnerable. Think of it this way: Paul had planted the gospel in Crete, and people had come to faith. But a collection of new believers without shepherds is like a flock without a shepherd — it can survive for a season, but it will eventually scatter or get picked off. Leadership is not bureaucracy. In Paul’s vision, it’s protection. It’s structure that allows the gospel to take root across generations. Titus’s first assignment is to build the infrastructure that makes everything else possible. VERSE 6: THE HOUSEHOLD QUALIFICATION Before Paul moves to the broader character portrait in verses 7 and 8, he pauses in verse 6 to say something about the elder’s household. It’s easy to read past it. Don’t. An elder must be blameless, the husband of but one wife, having children who are believers and who are not open to accusation of indiscretion or insubordination. The elder’s home is exhibit A. His marriage, his children, the atmosphere of his household — these are not private matters separate from his fitness to lead. They are the primary evidence of it. Think about what Paul is saying. Before Titus looks at a man’s preaching or his public presence, he should look at his home. Is his marriage marked by faithfulness? Are his children — the people who know him best, who cannot be impressed or deceived — living in a way that reflects what he believes? Paul’s logic is plain: if a man cannot lead his own household well, there is no reason to believe he will lead God’s household well. The church is not a stage where a person performs. It is a family — and family life requires the same qualities at home and at the pulpit. Integrity cannot be compart - mentalized. This isn’t a demand for perfection — Paul is not saying an elder must have flawless children. He says they must not be open to accusation of wild living or insubordination. The standard is a household genuinely ordered by the same faith the elder proclaims. ELDER AND OVERSEER: TWO WORDS, ONE ROLE In verses 5 through 7, Paul uses two words interchangeably: elder and overseer. Elder — presbyteros — carries the idea of maturity and respect. It speaks to someone the community looks to for wisdom. Overseer — episkopos — is more functional. It means one who watches over, who takes responsibility for the welfare of others. It emphasizes the work of the role. Together they give us the full picture. The person leading a local church is someone who is mature in faith AND active in oversight. Not just respected but responsible. Not just experienced but engaged. THE FIVE ‘NOTS’ AND THE POSITIVES In verses 7 and 8, Paul gives Titus a remarkably specific list. Let’s read them: As God’s steward, an overseer must be above reproach—not self-absorbed, not quick-tempered, not given to drunkenness, not violent, not greedy for money. Five things an elder must not be: not self-absorbed, not quick-tempered, not given to drunkenness, not violent, not greedy for money. Then in verse 8, six things he must be: Hospitable — someone whose home and life are genuinely open to others. A lover of good — someone drawn toward what is right and true and beautiful. Self-controlled — someone who governs their own desires and impulses. Upright — someone whose dealings with others are fair and straight. Holy — someon",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 4",
|
||||||
|
"titus 1:5–9",
|
||||||
|
"titus 1:5",
|
||||||
|
"what",
|
||||||
|
"does",
|
||||||
|
"faithful",
|
||||||
|
"leader",
|
||||||
|
"look",
|
||||||
|
"like"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.369Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "56626549-7e1e-4564-8c79-2d21b49eb911",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 5 — The Danger of Empty Words (Titus 1:10–13a)",
|
||||||
|
"content": "WHO WERE THE CIRCUMCISION GROUP? Paul identifies the primary troublemakers in verse 10: For many are rebellious and full of empty talk and deception, especially those of the circumcision, “Those of the circumcision” refers to some of the Jewish Christians — or people who were influenced by Jewish tradition — they were insisting that Gentile believers needed to follow the Mosaic law in addition to having faith in Christ. The specific issue was circumcision — but it stood for a whole system. The argument was essentially: Jesus is necessary, but not sufficient. You also need the law, Jewish customs, certain food regulations, certain rituals. For Paul, this was not a secondary debate. It was a direct attack on the gospel itself. The moment you add requirements to grace, you have changed what grace is. Paul’s entire ministry was grounded in one truth: salvation comes through Christ alone. Jesus’ life, death, and resurrection are fully sufficient. Nothing needs to be added. Faith in Jesus is all that is required to be reconciled to God. THREE GREEK WORDS In verse 10, Paul uses three Greek words to describe these teachers, and each one is precise. ἀνυπότακτος ( any - potak - tos ) — rebellious. Literally not submitted, not under authority. These people have placed themselves outside accountability. ματαιολόγος ( mata - io - logos ) — full of empty talk. Combines mata - ios (empty, vain) with logos (word). Religious noise without substance. φρεναπάτης ( phren - a - patēs ) — deception. Literally one who deceives the mind. Not just wrong content — distorting the way a person thinks from the inside. Rebellious. Empty talkers. Mind-deceivers. These aren’t accidental mistakes . These are people actively leading others in the wrong direction. Paul wants Titus to see them clearly. UNDERMINING ENTIRE HOUSEHOLDS Verse 11 tells us the scale of the damage: …who must be silenced. For the sake of dishonorable gain, they undermine entire households and teach things they should not. Whole families ! In the first-century world, the household was the basic unit of society. Churches met in homes. Faith was practiced in families. When a false teacher reshaped what a household believed, the damage spread through every relationship in that home. And notice the motive Paul names: dishonorable gain. These teachers weren’t doing this out of sincere conviction. They were doing it for money. For influence. For the advantages that came from building a following. That’s the pastoral warning. Not everyone who teaches with confidence is teaching the truth. In the Garden of Eden, Satan spoke convincingly while distorting what God had said. And not everyone who sounds sincere is free from self-interest. WHY PAUL QUOTES EPIMENIDES In verse 12, Paul quotes a Cretan poet — Epimenides — who said: “Cretans are always liars, evil beasts, lazy gluttons.” And then in verse 13 Paul adds: This testimony is true. Therefore rebuke them sternly, so that they will be sound in the faith This has confused readers for centuries. Is Paul endorsing ethnic stereotyping? No. He is engaged in ministry that takes context into account. He’s saying to Titus: the culture you’re working in has certain well-known tendencies. O ne of their own poets — said so. This isn’t about ethnicity; it’s about a specific cultural pattern of dishonesty that the false teachers were both shaped by and exploiting. Paul isn’t writing off Crete. He’s telling Titus to go in with eyes open. Know where you are. Know what you’re working against. REBUKE AS AN ACT OF LOVE Verse 13 also contains an instruction that modern ears often s",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 5",
|
||||||
|
"titus 1:10",
|
||||||
|
"danger",
|
||||||
|
"empty",
|
||||||
|
"words",
|
||||||
|
"false",
|
||||||
|
"teacher",
|
||||||
|
"titus 1:13"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.396Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dd6b3af4-44a5-43f1-9d69-ab12c63dd46d",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 6 — Words That Deny What We Claim to Believe (Titus 1:13b–16)",
|
||||||
|
"content": "A DEFILED CONSCIENCE AND A DISTORTED WORLD Verse 15 contains one of the most psychologically precise statements in all of Paul’s letters: To the pure, all things are pure; but to the defiled and unbelieving, nothing is pure. Indeed, both their minds and their consciences are defiled. A person with a clean conscience — someone whose heart has been washed by the gospel — can engage with the world from a place of trust and clarity. But a person whose conscience is defiled — corrupted by sin, self-deception, or bad doctrine — can’t see anything clearly. Everything gets filtered through that corruption. Even what is pure looks suspect to them. This is why false teaching is so damaging at the root level. It doesn’t just change what people believe — it changes how they see. Think about what that looks like practically. A person shaped by bad doctrine doesn’t just hold wrong opinions — they become suspicious of grace, hostile toward correction, and unable to receive what is genuinely good. They see generosity and call it ingenuousness . They see freedom and call it license. The very gifts of the gospel become things they can’t trust, because the lens they’re looking through has been cracked. That’s the devastation Paul is describing — not just theological error, but a person who has lost the ability to see clearly. JEWISH MYTHS AND COMMANDS OF MEN Verse 14 tells us specifically what these false teachers were promoting — Jewish myths and the commands of people who have rejected the truth. Jewish myths likely refers to speculative expansions of Old Testament stories. Commands of men refers to human-made religious rules elevated to the level of divine requirement. And here’s why Paul sees this as a gospel issue: when you add human requirements to the gospel, you shift the basis of acceptance before God from what Christ has done to what you do. The result is a Christianity more anxious, more performance-driven, more focused on the maintenance of rules than on the freedom of grace. We should be honest that this isn’t only a first-century problem. The “commands of men” show up in modern dress all the time — in churches that quietly teach that real Christians hold certain political views, in communities where your worth is measured by how often you volunteer, how much you give, or whether you use the right vocabulary. None of those things are bad in themselves. But the moment they become the measure of your standing before God, the gospel has been displaced. Paul’s warning is just as sharp now as it was in Crete. DETESTABLE, DISOBEDIENT, UNFIT Verse 16 ends with a devastating summary: They profess to know God, but by their actions they deny Him. They are detestable, disobedient, and unfit for any good deed. Three words. Detestable. Disobedient. Unfit for any good deed. The first word Paul uses - Detestable - describes something morally repulsive — something unacceptable before God . It’s the same kind of word the Old Testament used for things that were considered abominations. In other words, these teachers may look religious on the outside, but in God’s sight their teaching and conduct are deeply offensive. The second word - Disobedient - describes someone who is not persuadable — someone resistant to truth . Not merely confused, but unwilling to submit when confronted with what is right. They have heard the truth, but they refuse to bend their lives to it. The third word – is a phrase in English but is one word in Greek - Unfit for any good deed - This comes from the world of metalworking . It refers to metal that has been tested , examined and discovered to be counterfeit — rejected because it lacks real substance. That’s the picture Paul is painting: religious appearance on the surface, but wh",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 6",
|
||||||
|
"titus 1:13",
|
||||||
|
"deny",
|
||||||
|
"claim",
|
||||||
|
"believe",
|
||||||
|
"titus 1:16"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.417Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1559c4af-3913-4a0d-9ac3-a59172adabc5",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 7 — Sound Doctrine for Every Stage of Life (Titus 2:1–10)",
|
||||||
|
"content": "THE PIVOT FROM WARNING TO VISION Chapter 1 was about threats — the things that were wrong, the people causing damage, the patterns that needed to be stopped. It was a necessary chapter. You can’t build well if you haven’t cleared the ground. Chapter 2 is about vision. Not what are we against, but what are we for. Not just what does sound doctrine forbid, but what does it produce? And what it produces is a community that looks different. Every generation investing in the next. Older believers modeling what long faithfulness looks like. Younger believers learning to bring their appetites and ambitions under the direction of the gospel. Workers whose ordinary faithfulness makes the gospel attractive. This is not a vision of individual spiritual achievement. It is a vision of intergenerational community shaped by grace. OLDER MEN Verse 2 describes what Paul wants Titus to cultivate in older men: Older men are to be temperate, dignified, self-controlled, and sound in faith, love, and perseverance. Four qualities of character, and three qualities of soul. Temperate. Dignified. Self-controlled. These describe a man who has learned — through years of walking with God and being humbled by life — to govern himself. And then: sound in faith, love, and perseverance. Faith that hasn’t grown cynical. Love that hasn’t grown cold. Perseverance that hasn’t given up. The older man who embodies these qualities is not just personally virtuous — he is a witness. He shows the younger generation that it is possible to follow Jesus for decades and still be full of faith, still warm in love, still standing. That’s a gift no sermon can replace. OLDER WOMEN AND YOUNGER WOMEN Verses 3 through 5 describe older women and their specific role in the life of younger women: Older women, likewise, are to be reverent in their behavior, not slanderers or addicted to much wine, but teachers of good. In this way they can train the young women to love their husbands and children, to be self-controlled, pure, managers of their households, kind, and submissive to their own husbands, so that the word of God will not be discredited. The older women Paul describes are not passive. They are reverent — a word used elsewhere for temple service, suggesting a life of sacred intentionality. They are not slanderers. They are not addicted to much wine. They are self-governing. And their primary ministry is to teach younger women. The Greek word for train here — sophronizō — is about forming wisdom and good judgment in someone else. This is not advice-giving. This is mentorship. Investment. One life poured into another. And the reason Paul gives for all of it is stunning: so that the word of God will not be discredited. The way the women in this congregation live their ordinary lives either adorns the gospel or brings it into disrepute. Their faithfulness is a form of witness. YOUNGER MEN AND TITUS HIMSELF The section on younger men is the shortest in the passage. Verse 6: “In the same way, urge the younger men to be self-controlled.” That’s it. One charge. And then Paul immediately turns to Titus in verse 7: In everything, show yourself to be an example by doing good works. In your teaching show integrity, dignity, The connection is important. The best thing Titus can do for the younger men is not give them a list. It’s show them a life. Paul uses the word typos — a pattern, a mold. Titus himself is to be the shape that younger men are formed around. Self-control is not a naturally occurring quality in young men. It has to be learned. And it is learned most effectively not from a lecture but from a model. ADORNING THE GOSPEL [This is the capstone i",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 7",
|
||||||
|
"titus 2:1–10",
|
||||||
|
"titus 2:1",
|
||||||
|
"sound",
|
||||||
|
"doctrine",
|
||||||
|
"every",
|
||||||
|
"stage",
|
||||||
|
"life"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.441Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6be7a644-9276-4797-a7bb-1422b170846c",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 8 — Grace Is Not Just What Saves You — It’s What Trains You (Titus 2:11–12)",
|
||||||
|
"content": "GRACE AS TEACHER: THE GRAMMAR MATTERS Let’s read verses 11 and 12 carefully: For the grace of God has appeared, bringing salvation to everyone. It instructs us to renounce ungodliness and worldly passions, and to live sensible, upright, and godly lives in the present age, Now — look at the grammar of verse 12. The subject of the sentence is ‘it.’ And ‘it’ refers back to grace. Grace instructs us. Grace is the subject. Grace is doing the teaching. The Greek word Paul uses is paideuō — the word for the education of a child. Not classroom instruction. Formation. The kind of learning that shapes not just what you know but who you are. Grace is the tutor. And the curriculum it’s teaching has two parts: what to say no to, and what to say yes to. WHAT GRACE TEACHES US TO SAY NO TO Grace teaches us to renounce two things: ungodliness and worldly passions. Ungodliness — asebeia — is the opposite of reverence. A life where God is not actually at the center. Where decisions are made without reference to Him. Worldly passions — desires shaped by the age we live in rather than the kingdom we’ve been brought into. And here’s what I want you to notice: Paul doesn’t say that moral effort teaches us to renounce these things. He says grace teaches us. The pathway to saying no to ungodliness is going deeper into grace — understanding more fully what you have been given. Grace, properly understood, reorients your desires. It doesn’t just change the rules. It changes what you want. WHAT GRACE TEACHES US TO SAY YES TO On the other side, grace teaches three orientations: sensible, upright, and godly. Sensible — sōphrōn — a sound, well-ordered mind. Not repressed. Not uptight. But governed. A person whose interior life is ordered enough that they can engage with the world without being swept away by it. Upright — dikaios — right in relationship with others. Fair. Honest. The gospel changes how we treat people because it has changed how we understand our own standing before God. Godly — eusebōs — living with genuine reverence. A life where God is actually at the center, not just in stated belief, but in the practical orientation of each day. Inward (sensible), outward (upright), and upward (godly). Grace reshapes the whole person. THE KEY WORD: CHARIS The Greek word for grace is charis — χάρις. It appears in the opening greeting (1:4) and in the closing benediction (3:15), bookending the entire letter. Charis means unmerited favor. A gift freely given with no precondition of worthiness. But Paul uses it here as a teacher — it initiates AND sustains. The grace that started the work in you is the same grace that is continuing it. This is why trying harder without grace fails. Moral effort without a gospel foundation is white-knuckle religion. And this is also why grace without obedience is incomplete. Grace is so powerful that it actually produces change. If it’s not producing anything in your life, Paul would want to ask whether you’ve actually encountered it. DISCUSSION QUESTIONS Three questions to carry this week: 1. In what ways is grace a teacher? How does understanding what God has freely given change your motivation for how you live? 2. What’s the difference between obeying out of fear or duty versus obeying as a response to grace? What does that difference feel like from the inside? 3. Where do you find it hardest to say no to ungodliness — and what role does grace actually play there, in your honest experience? ✦ CLOSING Grace is not a hall pass for how you live. It is a school you attend every day. The same grace that declared you forgiven is act",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 8",
|
||||||
|
"titus 2:11–12",
|
||||||
|
"titus 2:11",
|
||||||
|
"grace",
|
||||||
|
"just",
|
||||||
|
"what",
|
||||||
|
"saves",
|
||||||
|
"trains"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.461Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4e6ec41f-b083-45b9-9581-0dc91f092f66",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 9 — Living Between Two Appearings (Titus 2:13–15)",
|
||||||
|
"content": "THE THREE TIMEFRAMES Verse 11: “For the grace of God has appeared.” Past tense. The first appearing. The foundation. Verse 12: “…to live sensible, upright, and godly lives in the present age.” Present tense. The current moment. Verse 13: …as we await the blessed hope and glorious appearance of our great God and Savior Jesus Christ. Future tense. We are waiting actively, with anticipation, with our behavior shaped by what we know is coming. Past event. Present calling. Future hope. You can’t live the present well without both the foundation behind you and the horizon ahead of you. THE BLESSED HOPE Verse 13 uses a phrase that has become so familiar it has almost lost its force: the blessed hope. The Greek is makaria elpis. Makaria is the same word used in the Beatitudes — blessed, in a divinely favored condition. And elpis — hope — in the New Testament is not the way we use the word in everyday conversation. We say ‘I hope it doesn’t rain’ — meaning we’re uncertain, we’re wishing. Biblical hope is a confident expectation of something certain. Not a wish. An anchor. The blessed hope is: Jesus is coming back. Not maybe. Not eventually, probably. He is coming back. And the person who really believes that is in a blessed condition even now, living toward a certainty that changes how everything feels today. THE GOSPEL IN ONE SENTENCE Verse 14 does something remarkable. In a single sentence, Paul gives us the full scope of what Christ’s death accomplished: He gave Himself for us to redeem us from all lawlessness and to purify for Himself a people for His own possession, zealous for good deeds. Two verbs. Redeem and purify. Redeem — lytroō — a word from the slave market and from Exodus. To pay a price to set someone free. What were we freed from? All lawlessness. Purify — katharizō — from the temple. To cleanse, to make acceptable. We weren’t just released from prison — we were made clean. Belonging. And look at the end: ‘a people for His own possession, zealous for good deeds.’ The word zelotes — eager, passionate, on fire. Not reluctant. Not dragging their feet. People who are genuinely excited about doing good because they understand what they’ve been rescued for. SPEAK WITH AUTHORITY Paul closes the chapter with a charge to Titus: Speak these things as you encourage and rebuke with all authority. Let no one despise you. Speak these things. All of it — the grace, the training, the blessed hope, the gospel in one sentence. Don’t soften it. Don’t apologize for it. With all authority. Not Titus’s own authority — the authority of the message itself. The gospel doesn’t need Titus’s permission to be true. His job is to represent it faithfully and let it carry its own weight. DISCUSSION QUESTIONS Three questions for this week: 1. How does actively anticipating the return of Christ affect the way you make decisions and treat people today? Does the blessed hope feel like a real anchor in your life? 2. What does it mean to you personally that Christ ‘gave Himself’ — that it was intentional, not accidental? 3. Do you experience your Christian life as something you belong to — a people claimed by God — or mostly as something you have to maintain on your own? ✦ CLOSING We are living between two great appearings of grace. The first established the foundation. The second will complete what was begun. The in-between space is not a waiting room. It’s a mission field. We don’t pace and wait. We live — sensible, upright, godly, zealous for good deeds — shaped by what we know is coming. Next ep",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 9",
|
||||||
|
"titus 2:13–15",
|
||||||
|
"titus 2:13",
|
||||||
|
"living",
|
||||||
|
"between",
|
||||||
|
"appearings"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.484Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "41233c63-d2b4-4fd3-9054-1afb11c37287",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 10 — How the Gospel Forms a Community (Titus 3:1–2)",
|
||||||
|
"content": "WHY PAUL OPENS CHAPTER 3 WITH CIVIC SUBMISSION Paul is not saying: agree with every government decision. He is saying: your engagement with public life should be marked by something recognizable as different. Your submission, your readiness to do good, your peaceable presence — these are not just civic virtues. They are missionary strategies. The way the community in Crete navigates public life is either going to open doors for the gospel or close them. THREE ACTIVE ORIENTATIONS Verse 1: Remind the believers to submit to rulers and authorities, to be obedient and ready for every good work, Submit to rulers and authorities — a posture of respect toward legitimate governing structures. The believer’s default is cooperation rather than defiance. Be obedient — follow through. Actually do what is asked. People whose lives don’t create unnecessary friction with the society around them. Be ready for every good work — active, not passive. Readiness implies preparation and eagerness. The believer’s posture is not just ‘don’t cause problems.’ It’s ‘be looking for opportunities to do good.’ THE POSTURE OF BELIEVERS IN A CONFLICT-SATURATED CULTURE Verse 2 adds four more qualities: …to malign no one, and to be peaceable and gentle, showing full consideration to everyone. Malign no one. In a culture built on callouts, political attack cycles, and the steady background noise of contempt — this is genuinely countercultural. Paul doesn’t say: malign no one except your political opponents. He says no one. Peaceable — amachous — literally non-combative. Alpha (not) + machē (battle). People who are not itching for a fight, not framing every interaction as a conflict to be won. Gentle — epieikēs — gracious, not insisting on the full weight of one’s rights. Showing full consideration to everyone — not just the likable, the agreeable, the theologically correct. For all people. This is not weakness. This is the confidence of people who know how the story ends and don’t need to win every argument to prove it. DISCUSSION QUESTIONS Three questions for this week: 1. How should the gospel shape the way Christians engage with politics, authority, and public life? Where do you see the church getting this right — and where do you see it getting it wrong? 2. What does it look like to be gentle and considerate to everyone in the age of social media? What would need to change in your online presence to reflect the posture Paul describes here? 3. Where do you find it hardest to reflect the peaceable posture Paul describes? What situation or kind of person most tests your gentleness? ✦ CLOSING The gospel sends us into the world not as culture warriors, but as witnesses. The posture Paul calls for — gentle, peaceable, considerate, ready for every good work — is the confidence of people who know who they belong to and what the future holds. The confidence of people who know the King doesn’t look like aggression. It looks like peace. Next episode, before Paul gives us the gospel again in its fullest form, he gives us the diagnosis. Verse 3 is one of the most honest passages in Titus — and Paul includes himself in it. I’m Nate, and this is Verse by Verse with Nate: A Journey Through Scripture. Until next time — keep going. — End of Episode 10 —",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 10",
|
||||||
|
"titus 3:1–2",
|
||||||
|
"titus 3:1",
|
||||||
|
"gospel",
|
||||||
|
"forms",
|
||||||
|
"community"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.506Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6d0def21-46cb-497e-ac20-8e09c5368a08",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 11 — Remember What You Were (Titus 3:3)",
|
||||||
|
"content": "THE ‘WE TOO WERE’ Let’s read the verse: For at one time we too were foolish, disobedient, misled, and enslaved to all sorts of desires and pleasures—living in malice and envy, being hated and hating one another. We ourselves. This is not rhetoric. This is autobiography. Paul is giving instructions on how to correct others — how to rebuke false teachers, how to call the congregation to a higher standard. And right here he stops and says: before you do any of that, remember what you were. This is the ground of Christian humility. Every time a believer looks at someone who is not yet a Christian — someone whose life is being destroyed by these very patterns — the right response is not contempt. It is recognition. I was there. I was enslaved to those same kinds of things. I didn’t save myself. I was saved. SEVEN DESCRIPTORS Paul gives us seven words for what we were before grace. Each one is precise. Foolish — anoētos — without understanding. Not unintelligent. Without the wisdom that comes from knowing God. Disobedient — apeitheis — not persuadable, resistant to truth. Misled — planōmenoi — wandering, deceived, off course. Confidently heading in the wrong direction. Enslaved to all sorts of desires and pleasures — douleuontes — serving as slaves. Not choosing. Owned by desires. Living in malice and envy — ordinary corrosive stuff — wanting what others have, being glad when they fail. Being hated and hating one another — the natural end state of a life shaped by malice and envy. Seven words for what we were. And this is not a description of the worst people in the world. This is a description of all of us, before grace. HONEST MEMORY AS A PASTORAL GIFT Why does Paul make Titus — and us — look at this? Because the people Titus is ministering to include people who are still in this condition. And it would be easy — especially for a young pastor who has been well-formed by the gospel — to look at them with spiritual impatience. To treat their condition as a problem to be solved rather than a darkness to be illuminated with compassion. Paul is cutting that off at the root. You were there. You did not extricate yourself. You were found. There is a kind of humility that only grows from honest memory. The person who genuinely remembers what they were before grace is not capable of looking down on someone who doesn’t yet have it. They are too aware of the distance grace had to travel to reach them. DISCUSSION QUESTIONS Three questions to sit with this week: 1. Why is Paul’s autobiographical ‘we’ so important here? How does remembering your own ‘before’ picture shape your attitude toward people who don’t yet know the gospel? 2. Which of the seven descriptors — foolish, disobedient, misled, enslaved, malicious, envious, hated and hating — do you most need to keep in view to stay humble? 3. How does honest self-awareness about what we were become a gift for the way we love others? Where have you seen that in practice? ✦ CLOSING Humility toward others grows from memory. We don’t look down on where people are — because we remember where we were. And we remember that we didn’t save ourselves. Grace came to us. Grace found us in the dark and brought us out. That memory is not a wound. It’s a gift. Next episode, we get the gospel in its fullest form — Titus 3:4 through 7 is one of the most concentrated and beautiful gospel summaries anywhere in the New Testament. I’m Nate, and this is Verse by Verse with Nate: A Journey Through Scripture. Until next time — keep going. — End of Episode 11 —",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 11",
|
||||||
|
"titus 3:3",
|
||||||
|
"titus 3:4",
|
||||||
|
"remember",
|
||||||
|
"what",
|
||||||
|
"were"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.532Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b68c5659-cc7e-42d2-ab24-a623b7404058",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 12 — The Gospel in One Paragraph (Titus 3:4–7)",
|
||||||
|
"content": "SALVATION IS TRINITARIAN All three persons of the Trinity are present and active in our salvation. This is not accidental — Paul is showing us that the gospel is not a one-person operation. God the Father — verse 4 speaks of ‘the kindness of God our Savior and His love for mankind.’ The initiating motivation for salvation is the character of the Father. He is kind. He is full of love toward humanity. Our rescue begins in the heart of God. The Holy Spirit — verse 5 speaks of ‘the washing of new birth and renewal by the Holy Spirit.’ The Spirit is the agent of the new birth — the one who applies the work of Christ to the individual soul. Jesus Christ — verse 6 names Him explicitly as the one through whom the Spirit is poured out, and verse 7 calls Him ‘our Savior.’ Father, Son, Spirit — each person fully present, each playing a distinct role, all working together in the single act of saving a person. This is not just theology. It is the shape of your own story. NOT BY OUR DEEDS: MERCY, NOT MERIT Verse 5 contains one of the most explicit denials of works-based salvation in the New Testament: But when the kindness of God our Savior and His love for mankind appeared, He saved us, not by the righteous deeds we had done, but according to His mercy, through the washing of new birth and renewal by the Holy Spirit. Not by the righteous deeds we had done. Not partially. Not mostly. The basis of salvation is not our righteousness. It is His mercy. The theological term for this is monergism — salvation as a single-agent work. God does the saving. We receive it. There was nothing in the raw material that deserved rescue. The rescue happened anyway, because of mercy. And why does this matter so much? Because if salvation is even one percent based on our merit, it is unstable. But if salvation is entirely based on God’s mercy — on the character of a God who cannot lie and who loved humanity enough to appear as a human and die — then it rests on something that cannot change. NEW BIRTH AND RENEWAL Verse 5 gives us two words for what the Spirit does in us: the washing of new birth and renewal. Washing of new birth — palingenesia — literally ‘again-genesis.’ A new beginning. A second creation. This is the moment the Spirit brings a spiritually dead person to life. A definitive event. A before-and-after. Renewal by the Holy Spirit — anakainōsis — ongoing work. Continuous present tense in the Greek. The Spirit who brought you to life at regeneration is also continually renewing you. Both are needed. New birth without renewal produces a static faith. Renewal without new birth produces endless effort without a genuine starting point. Both are the Spirit’s work. Your part is to cooperate with what He is already doing. JUSTIFIED AND HEIRS Verse 7 adds two more categories: This is the Spirit He poured out on us abundantly through Jesus Christ our Savior, so that, having been justified by His grace, we would become heirs with the hope of eternal life. Justified — dikaiōthentes — a courtroom term. To be declared righteous. Not to be made righteous in our behavior — but declared righteous in our standing. The verdict rendered over you in the courtroom of God is: not guilty. Accepted. Righteous in Christ. Heirs — klēronómoi — relational. An heir belongs to a family. Has a place at the table. Has a guaranteed share in the inheritance. You are not just legally acquitted. You are a member of the family. The eternal life promised before time began is not a prize you might win. It is an inheritance secured for you. DISCUSSION QUESTIONS Three questions for this week: 1. Why is it so hard for people — including long-time Christians — to fully accept that salvation is en",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 12",
|
||||||
|
"titus 3:4–7",
|
||||||
|
"titus 3:4",
|
||||||
|
"gospel",
|
||||||
|
"paragraph"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.554Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c8db1a86-346f-4bad-ba4f-30c29d95feb3",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 13 — What Happens After the Gospel Takes Root (Titus 3:8–11)",
|
||||||
|
"content": "THE FAITHFUL SAYING Verse 8: This saying is trustworthy. And I want you to emphasize these things, so that those who have believed God will take care to devote themselves to good deeds. These things are excellent and profitable for the people. Pistos ho logos — this saying is trustworthy. Paul uses this formula in his letters to Timothy and Titus to mark something as having the highest doctrinal weight. What he just said in verses 4 through 7 is the trustworthy, reliable, foundational word. Bank on it. Build on it. And then immediately: the expected outcome. Those who have believed God will take care to devote themselves to good deeds. The test of the gospel having taken root is not doctrinal sophistication. It is practical devotion to good works. PROFITABLE VS. POINTLESS AND WORTHLESS Verse 9: But avoid foolish controversies, genealogies, arguments, and quarrels about the law, because these things are pointless and worthless. Profitable — ōphélima. Pointless and worthless — anōphelēs and mataioi. The contrast is total. Good works produce something. They are excellent — kalá — beautiful, genuinely valuable. They benefit people. They demonstrate the gospel in tangible ways. Foolish controversies, on the other hand, produce nothing. They generate heat but no light. They create factions but no fruit. The test Paul applies is not: is this interesting? Is this theologically sophisticated? The test is: does engaging with this produce anything? Does it make people more devoted to good works? SOUND DOCTRINE PRODUCES GOOD WORKS Let me name the thread Paul has been pulling through this entire letter, because we’re near the end and it is clearer here than anywhere else. From chapter 1, verse 1: truth that leads to godliness. Elders who hold firmly to sound doctrine. False teachers whose lives deny what they claim to believe. Chapter 2: grace that instructs us to live sensible, upright, godly lives. Ordinary faithfulness that adorns the gospel. Chapter 3: people who have believed God devoting themselves to good deeds. The thread is this: sound doctrine and transformed living are not separate categories. Doctrine is the seed. Good works are the fruit. You cannot have genuine doctrine without eventual fruit, and you cannot sustain genuine good works without doctrinal roots. THE DIVISIVE PERSON: TWO WARNINGS, THEN REJECTION Verses 10 and 11: Reject a divisive man after a first and second admonition, knowing that such a man is corrupt and sinful; he is self-condemned. Two warnings. Not immediate removal. Not endless patience. Two warnings. One warning is not enough — it doesn’t give the person a genuine chance to respond. The second warning is confirmation that the first was heard and not responded to. After two warnings with no change — have nothing more to do with him. And Paul’s assessment: self-condemned. The verdict is not coming from the outside. The person who persists in division after two clear warnings has essentially condemned themselves by their own choices. The community is simply recognizing what is already true. This is not cruelty. It is honesty. And it protects the people who are being divided. DISCUSSION QUESTIONS Three questions for this week: 1. Why is good theology supposed to produce good deeds — and what happens in a person or a church when doctrine and life become disconnected from each other? 2. What kinds of theological discussions do you think fall into Paul’s ‘pointless and worthless’ category today? How do you tell the difference between a discussion worth having and one that just generates heat? 3. Why does Paul prescribe two warnings rather than immediate removal or endless patience for divisive people? What does that process protect? ✦ CLOSING [Grounde",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 13",
|
||||||
|
"titus 3:8–11",
|
||||||
|
"titus 3:8",
|
||||||
|
"what",
|
||||||
|
"happens",
|
||||||
|
"after",
|
||||||
|
"gospel",
|
||||||
|
"takes",
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.582Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "14ced988-1382-4c8d-a29e-b0f531580af1",
|
||||||
|
"type": "episode",
|
||||||
|
"title": "Episode 14 — Grace: Where It Starts and Where It Ends (Titus 3:12–15)",
|
||||||
|
"content": "REAL PEOPLE IN A REAL COMMUNITY Verses 12 through 14 name four people: Artemas, Tychicus, Zenas the lawyer, and Apollos. Tychicus appears in several of Paul’s letters as a trusted messenger. Apollos is mentioned in Acts and 1 Corinthians as a gifted teacher. Zenas is only mentioned here. Artemas we know nothing about beyond this verse. Why does Paul name them? Because the gospel travels through people. Not as an abstract idea floating through the air, but carried by specific men and women who have been changed by it and who are willing to take it somewhere difficult at personal cost. Titus is not alone on Crete. He is part of a network. The church in the first century was a community of people in motion — bound together by a shared gospel and a shared Lord. That is still how it works. DEVOTED TO GOOD WORKS TO MEET PRESSING NEEDS Verse 14: And our people must also learn to devote themselves to good works in order to meet the pressing needs of others, so that they will not be unfruitful. Two things worth noticing. First: let our people learn. Devotion to good works has to be cultivated, practiced, formed over time. It doesn’t just happen because you believe the right things. Second: to meet the pressing needs of others. The word is anagkaias — necessary, pressing, urgent. Paul is not describing optional generosity. He is describing a community that has made itself available for the real needs that arise around it. And the consequence of not doing this: be unfruitful. The gospel that has been received and believed and proclaimed — if it doesn’t produce this kind of practical, neighbor-serving fruit — something is missing. GRACE AS BOOKEND Let’s look at the two times grace appears at the frame of this letter. Titus 1:4 — the opening: “Grace and peace from God the Father and Christ Jesus our Savior.” In the opening, grace is the origin. It comes from God and from Christ. It is something received, given from outside, descending as pure gift. Titus 3:15 — the closing: “Grace be with all of you.” In the closing, grace is the blessing. It is what Paul wants them to be with — not wealth, not safety, not resolved problems. Grace. Everything that happens between those two bookends flows from the first grace and points toward the second. The elders who lead well are stewards of grace. The community formed generation by generation is shaped by grace. The gospel paragraph is the concentrated statement of grace. The good works that follow are the fruit of grace. Grace is not a footnote in Titus. It is the frame. It is the air the letter breathes. THE LETTER’S ONE COHERENT VISION Let me take you through the structure of the letter one final time. Chapter 1 — Leadership. Establish qualified elders. The elder who holds firmly to sound doctrine is the church’s first line of protection. Character is not the entry point for leadership — it is the definition of it. Chapter 2 — Community Formation. Every generation — older men, older women, younger women, younger men, enslaved people — is addressed. Grace instructs. Grace trains. Grace produces a particular kind of life in every stage and every situation. Chapter 3 — Civic and Social Life. Engage with submission, with gentleness, with readiness to do good. Remember what you were. The same grace that found you is the grace you are carrying to others. Three chapters. One vision. Sound doctrine and godly living are inseparable. What you believe should shape how you live. And the whole thing is held together by the grace that started it and the grace that will complete it. DISCUSSION QUESTIONS Three final questions — take your tim",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"episode 14",
|
||||||
|
"titus 3:12",
|
||||||
|
"titus 1:4",
|
||||||
|
"titus 3:15",
|
||||||
|
"grace",
|
||||||
|
"starts",
|
||||||
|
"ends",
|
||||||
|
"review"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T13:11:07.605Z",
|
||||||
|
"updatedAt": "2026-04-13T14:06:04.273Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_faith_family_001",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "How can I share my faith with skeptical family?",
|
||||||
|
"content": "Lead with relationship, not argument. Skeptical family members usually don't need more information — they need to see the faith lived out genuinely. Consistency, love, and patience over time speaks louder than any debate win.\n\nPray specifically and persistently. This is the most underrated strategy. God moves hearts in ways arguments never can.",
|
||||||
|
"keywords": [
|
||||||
|
"faith",
|
||||||
|
"family",
|
||||||
|
"skeptical",
|
||||||
|
"share",
|
||||||
|
"relationship",
|
||||||
|
"evangelism",
|
||||||
|
"prayer",
|
||||||
|
"witness",
|
||||||
|
"unbeliever",
|
||||||
|
"argument",
|
||||||
|
"patience",
|
||||||
|
"love"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_blessed_hope_001",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "What is the blessed hope?",
|
||||||
|
"content": "The blessed hope is a phrase from Titus 2:13 — \"as we await the blessed hope and glorious appearance of our great God and Savior Jesus Christ.\" In Episode 9, Nate breaks down the Greek: makaria elpis. Makaria is the same word used in the Beatitudes — a divinely favored condition. And elpis in the New Testament is not wishful thinking. Biblical hope is a confident expectation of something certain. Not a wish. An anchor.\n\nThe blessed hope is simply this: Jesus is coming back. Not maybe. He is coming back. And the person who really believes that is in a blessed condition even now, living toward a certainty that changes how everything feels today.",
|
||||||
|
"keywords": [
|
||||||
|
"blessed hope",
|
||||||
|
"titus 2:13",
|
||||||
|
"second coming",
|
||||||
|
"return of Christ",
|
||||||
|
"hope",
|
||||||
|
"appearing",
|
||||||
|
"makaria",
|
||||||
|
"elpis",
|
||||||
|
"anchor",
|
||||||
|
"future",
|
||||||
|
"episode 9"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_who_is_titus_001",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "Who was Titus?",
|
||||||
|
"content": "Titus was a Gentile — not Jewish — who came to faith through Paul's ministry. Paul calls him \"my true child in our common faith\" (Titus 1:4). He was one of Paul's closest and most trusted co-workers. In 2 Corinthians 7, Paul describes being comforted by Titus arriving during a difficult season. In chapter 8, Titus helped organize a relief offering for believers in Jerusalem. In Galatians 2, he appears as proof that the gospel is for everyone.\n\nPaul left Titus on the island of Crete with a difficult assignment: appoint elders, correct false teaching, and help a young church find its footing in a challenging culture. The letter of Titus is essentially his briefing.",
|
||||||
|
"keywords": [
|
||||||
|
"titus",
|
||||||
|
"who is titus",
|
||||||
|
"gentile",
|
||||||
|
"paul",
|
||||||
|
"co-worker",
|
||||||
|
"crete",
|
||||||
|
"new testament",
|
||||||
|
"biography",
|
||||||
|
"history",
|
||||||
|
"faith"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_deny_works_001",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "What does it mean to deny God by your works?",
|
||||||
|
"content": "Titus 1:16 says: \"They profess to know God, but by their actions they deny Him. They are detestable, disobedient, and unfit for any good deed.\"\n\nIn Episode 6, Nate explains this using three precise words Paul chose. Detestable — morally repulsive before God, regardless of religious appearance. Disobedient — not persuadable, resistant to truth even when confronted with it. Unfit — a Greek metalworking term for metal tested and found counterfeit. Religious on the surface, but no real substance underneath.\n\nThe warning cuts both ways: a person can know the right words, attend the right services, hold the right opinions — and still live in a way that functionally says God doesn't matter. Theology and life are inseparable. What you actually believe will show itself in how you actually live.",
|
||||||
|
"keywords": [
|
||||||
|
"deny god",
|
||||||
|
"works",
|
||||||
|
"titus 1:16",
|
||||||
|
"detestable",
|
||||||
|
"disobedient",
|
||||||
|
"unfit",
|
||||||
|
"profess",
|
||||||
|
"actions",
|
||||||
|
"hypocrisy",
|
||||||
|
"false teaching",
|
||||||
|
"episode 6"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_grace_trains_001",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "What does grace actually train us to do?",
|
||||||
|
"content": "Titus 2:11-12 says grace \"instructs us to renounce ungodliness and worldly passions, and to live sensible, upright, and godly lives.\"\n\nIn Episode 8, Nate points out that the Greek word for instructs is paideuō — the education of a child. Not classroom instruction but formation. Grace is the subject doing the teaching.\n\nGrace teaches two things: what to say no to (ungodliness and worldly passions) and what to say yes to — sensible (an ordered mind), upright (right in relationship with others), and godly (God genuinely at the center). Inward, outward, and upward.\n\nThe key insight: the pathway to saying no to sin is going deeper into grace, not trying harder. Grace properly understood reorients your desires — it doesn't just change the rules, it changes what you want. Grace is not a hall pass. It's a school you attend every day.",
|
||||||
|
"keywords": [
|
||||||
|
"grace",
|
||||||
|
"trains",
|
||||||
|
"titus 2:11",
|
||||||
|
"titus 2:12",
|
||||||
|
"paideuō",
|
||||||
|
"ungodliness",
|
||||||
|
"sensible",
|
||||||
|
"upright",
|
||||||
|
"godly",
|
||||||
|
"obedience",
|
||||||
|
"formation",
|
||||||
|
"episode 8"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_leadership_titus1_001",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "What does Titus 1 teach about church leadership?",
|
||||||
|
"content": "Paul's first instruction in Titus was to appoint elders — because a church without mature, qualified leadership is vulnerable. From Episode 4 (Titus 1:5-9):\n\nThe elder's home is exhibit A. Before looking at preaching or public presence, look at the household. If a man cannot lead his own family well, there's no reason to believe he'll lead God's family well. Integrity can't be compartmentalized.\n\nPaul gives five things an elder must NOT be: not self-absorbed, not quick-tempered, not given to drunkenness, not violent, not greedy. And six things he must BE: hospitable, a lover of good, self-controlled, upright, holy, and disciplined in sound doctrine.\n\nThis is not a job description for a CEO. Nate frames it as a picture of Christlike character — authority that flows from below, from a posture of submission and service, not dominance.",
|
||||||
|
"keywords": [
|
||||||
|
"leadership",
|
||||||
|
"elder",
|
||||||
|
"overseer",
|
||||||
|
"titus 1:5",
|
||||||
|
"titus 1:6",
|
||||||
|
"titus 1:7",
|
||||||
|
"titus 1:9",
|
||||||
|
"church",
|
||||||
|
"pastor",
|
||||||
|
"qualify",
|
||||||
|
"character",
|
||||||
|
"household",
|
||||||
|
"episode 4"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_titus347_summary_001",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "Give me a summary of Titus 3:4-7",
|
||||||
|
"content": "Titus 3:4-7 is what Nate calls 'the gospel in one paragraph' — one of the most concentrated summaries of salvation anywhere in Paul's letters. Episode 12 breaks it down:\n\nAll three persons of the Trinity are present: God the Father is the initiating motivation — He is kind and full of love toward humanity. The Holy Spirit is the agent of new birth and ongoing renewal. Jesus Christ is the one through whom the Spirit is poured out, and verse 7 calls him 'our Savior.'\n\nThe hinge of the passage is verse 5: He saved us not by the righteous deeds we had done, but according to His mercy. Not partially. Not mostly. The basis is His mercy, not our merit — which means salvation rests on something that cannot change.\n\nVerse 7 gives us two results: justified (declared righteous in God's courtroom) and heirs (members of the family with a guaranteed inheritance). You are not just legally acquitted — you belong.",
|
||||||
|
"keywords": [
|
||||||
|
"titus 3:4",
|
||||||
|
"titus 3:4-7",
|
||||||
|
"titus 3:7",
|
||||||
|
"summary",
|
||||||
|
"gospel",
|
||||||
|
"mercy",
|
||||||
|
"salvation",
|
||||||
|
"trinity",
|
||||||
|
"justified",
|
||||||
|
"heirs",
|
||||||
|
"new birth",
|
||||||
|
"renewal",
|
||||||
|
"holy spirit",
|
||||||
|
"episode 12"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_christians_politics_001",
|
||||||
|
"type": "qa",
|
||||||
|
"title": "How should Christians engage with politics and public life?",
|
||||||
|
"content": "Titus 3:1-2 is Paul's answer, covered in Episode 10. He tells Titus to remind believers to submit to rulers and authorities, be obedient, be ready for every good work, malign no one, and be peaceable, gentle, and considerate to everyone.\n\nNate's key insight: Paul is not saying agree with every government decision. He's saying your engagement with public life should be marked by something recognizable as different. Submission, readiness to do good, and a peaceable presence are not just civic virtues — they are missionary strategies. The way you navigate public life either opens doors for the gospel or closes them.\n\nThe word for peaceable in Greek is amachous — literally non-combative. Paul doesn't say malign no one except your political opponents. He says no one. This is not weakness. It is the confidence of people who know how the story ends and don't need to win every argument to prove it.",
|
||||||
|
"keywords": [
|
||||||
|
"politics",
|
||||||
|
"government",
|
||||||
|
"Christians",
|
||||||
|
"public life",
|
||||||
|
"civic",
|
||||||
|
"titus 3:1",
|
||||||
|
"titus 3:2",
|
||||||
|
"submit",
|
||||||
|
"peaceable",
|
||||||
|
"gentle",
|
||||||
|
"social media",
|
||||||
|
"culture",
|
||||||
|
"episode 10"
|
||||||
|
],
|
||||||
|
"createdAt": "2026-04-13T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-04-13T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
[
|
||||||
|
{ "query": "who was titus", "expectedEntryId": "cb_who_is_titus_001", "expectedTopK": 3 },
|
||||||
|
{ "query": "who is paul", "expectedEntryId": "881279c7-3ca4-436d-a584-19cc0a5a9bf3", "expectedTopK": 3 },
|
||||||
|
{ "query": "tell me about paul in titus", "expectedEntryId": "881279c7-3ca4-436d-a584-19cc0a5a9bf3", "expectedTopK": 4 },
|
||||||
|
{ "query": "who was saul before his conversion", "expectedEntryId": "167fa083-5438-44a5-ac72-6dd379e937f5", "expectedTopK": 4 },
|
||||||
|
{ "query": "who was titus and why was he in crete", "expectedEntryId": "cb_who_is_titus_001", "expectedTopK": 12 },
|
||||||
|
{ "query": "what bible translation does nate use", "expectedEntryId": "cb_001", "expectedTopK": 1 },
|
||||||
|
{ "query": "does the podcast use bsb", "expectedEntryId": "cb_001", "expectedTopK": 2 },
|
||||||
|
{ "query": "which bible version do you teach from", "expectedEntryId": "cb_001", "expectedTopK": 2 },
|
||||||
|
{ "query": "how can i stay consistent with daily bible reading", "expectedEntryId": "cb_003", "expectedTopK": 2 },
|
||||||
|
{ "query": "help me build a daily reading habit", "expectedEntryId": "cb_003", "expectedTopK": 3 },
|
||||||
|
{ "query": "i keep missing devotion time what should i do", "expectedEntryId": "cb_003", "expectedTopK": 4 },
|
||||||
|
{ "query": "how should christians engage with politics", "expectedEntryId": "cb_christians_politics_001", "expectedTopK": 2 },
|
||||||
|
{ "query": "what does titus say about public life", "expectedEntryId": "cb_christians_politics_001", "expectedTopK": 3 },
|
||||||
|
{ "query": "how can i be peaceable online", "expectedEntryId": "cb_christians_politics_001", "expectedTopK": 4 },
|
||||||
|
{ "query": "give me a summary of titus 3:4-7", "expectedEntryId": "cb_titus347_summary_001", "expectedTopK": 1 },
|
||||||
|
{ "query": "summarize titus 3 4 through 7", "expectedEntryId": "cb_titus347_summary_001", "expectedTopK": 12 },
|
||||||
|
{ "query": "what does titus 3:4-7 teach about salvation", "expectedEntryId": "b68c5659-cc7e-42d2-ab24-a623b7404058", "expectedTopK": 2 },
|
||||||
|
{ "query": "what is the blessed hope", "expectedEntryId": "cb_blessed_hope_001", "expectedTopK": 2 },
|
||||||
|
{ "query": "define blessed hope from titus 2:13", "expectedEntryId": "cb_blessed_hope_001", "expectedTopK": 2 },
|
||||||
|
{ "query": "what does makaria elpis mean", "expectedEntryId": "cb_blessed_hope_001", "expectedTopK": 3 },
|
||||||
|
{ "query": "what does grace train us to do", "expectedEntryId": "cb_grace_trains_001", "expectedTopK": 2 },
|
||||||
|
{ "query": "how does grace teach us to say no to ungodliness", "expectedEntryId": "cb_grace_trains_001", "expectedTopK": 3 },
|
||||||
|
{ "query": "what is paideuo in titus 2", "expectedEntryId": "6be7a644-9276-4797-a7bb-1422b170846c", "expectedTopK": 12 },
|
||||||
|
{ "query": "how do i submit a question to nate", "expectedEntryId": "cb_007", "expectedTopK": 1 },
|
||||||
|
{ "query": "where can i send bible questions", "expectedEntryId": "cb_007", "expectedTopK": 12 },
|
||||||
|
{ "query": "how do i contact nate", "expectedEntryId": "cb_007", "expectedTopK": 2 },
|
||||||
|
{ "query": "what does titus 1 teach about church leadership", "expectedEntryId": "cb_leadership_titus1_001", "expectedTopK": 2 },
|
||||||
|
{ "query": "elder qualifications in titus 1", "expectedEntryId": "cb_leadership_titus1_001", "expectedTopK": 12 },
|
||||||
|
{ "query": "what should an overseer be like", "expectedEntryId": "cb_leadership_titus1_001", "expectedTopK": 3 },
|
||||||
|
{ "query": "what did paul say about elders", "expectedEntryId": "e0ad268f-2826-41a5-b078-90f90aeda21b", "expectedTopK": 4 },
|
||||||
|
{ "query": "episode on faithful leaders", "expectedEntryId": "e0ad268f-2826-41a5-b078-90f90aeda21b", "expectedTopK": 3 },
|
||||||
|
{ "query": "titus 1:5-9 overview", "expectedEntryId": "e0ad268f-2826-41a5-b078-90f90aeda21b", "expectedTopK": 12 },
|
||||||
|
{ "query": "what are false teachers doing in titus", "expectedEntryId": "56626549-7e1e-4564-8c79-2d21b49eb911", "expectedTopK": 3 },
|
||||||
|
{ "query": "what is the circumcision group in titus", "expectedEntryId": "56626549-7e1e-4564-8c79-2d21b49eb911", "expectedTopK": 3 },
|
||||||
|
{ "query": "empty talk and deception in titus 1", "expectedEntryId": "56626549-7e1e-4564-8c79-2d21b49eb911", "expectedTopK": 4 },
|
||||||
|
{ "query": "what does it mean to deny god by your works", "expectedEntryId": "cb_deny_works_001", "expectedTopK": 2 },
|
||||||
|
{ "query": "they profess to know god but deny him by actions", "expectedEntryId": "cb_deny_works_001", "expectedTopK": 3 },
|
||||||
|
{ "query": "detestable disobedient unfit meaning", "expectedEntryId": "cb_deny_works_001", "expectedTopK": 4 },
|
||||||
|
{ "query": "episode about grace training us", "expectedEntryId": "6be7a644-9276-4797-a7bb-1422b170846c", "expectedTopK": 2 },
|
||||||
|
{ "query": "titus 2:11-12 episode", "expectedEntryId": "6be7a644-9276-4797-a7bb-1422b170846c", "expectedTopK": 12 },
|
||||||
|
{ "query": "how should i share faith with skeptical family", "expectedEntryId": "cb_faith_family_001", "expectedTopK": 2 },
|
||||||
|
{ "query": "evangelize skeptical relatives", "expectedEntryId": "cb_faith_family_001", "expectedTopK": 4 },
|
||||||
|
{ "query": "where can i listen to the podcast", "expectedEntryId": "cb_006", "expectedTopK": 2 },
|
||||||
|
{ "query": "how do i find verse by verse with nate on apple podcasts", "expectedEntryId": "cb_006", "expectedTopK": 3 },
|
||||||
|
{ "query": "what is verse by verse with nate podcast", "expectedEntryId": "cb_005", "expectedTopK": 2 },
|
||||||
|
{ "query": "tell me about the podcast", "expectedEntryId": "cb_005", "expectedTopK": 2 },
|
||||||
|
{ "query": "how do i approach a difficult passage", "expectedEntryId": "cb_002", "expectedTopK": 2 },
|
||||||
|
{ "query": "what should i do with confusing bible verses", "expectedEntryId": "cb_002", "expectedTopK": 3 },
|
||||||
|
{ "query": "prayer and quiet time advice", "expectedEntryId": "cb_008", "expectedTopK": 2 },
|
||||||
|
{ "query": "how to start a quiet time", "expectedEntryId": "cb_008", "expectedTopK": 3 },
|
||||||
|
{ "query": "what is episode 9 about", "expectedEntryId": "4e6ec41f-b083-45b9-9581-0dc91f092f66", "expectedTopK": 12 },
|
||||||
|
{ "query": "living between two appearings", "expectedEntryId": "4e6ec41f-b083-45b9-9581-0dc91f092f66", "expectedTopK": 2 },
|
||||||
|
{ "query": "what is episode 12", "expectedEntryId": "b68c5659-cc7e-42d2-ab24-a623b7404058", "expectedTopK": 2 },
|
||||||
|
{ "query": "the gospel in one paragraph", "expectedEntryId": "b68c5659-cc7e-42d2-ab24-a623b7404058", "expectedTopK": 2 },
|
||||||
|
{ "query": "what is episode 14 about", "expectedEntryId": "14ced988-1382-4c8d-a29e-b0f531580af1", "expectedTopK": 3 },
|
||||||
|
{ "query": "grace where it starts and where it ends", "expectedEntryId": "14ced988-1382-4c8d-a29e-b0f531580af1", "expectedTopK": 3 }
|
||||||
|
]
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cb_001",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"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.",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Bible Study"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_002",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"question": "How do you approach a difficult or confusing Bible passage?",
|
||||||
|
"answer": "I always start by reading the surrounding context — a confusing verse often makes perfect sense once you zoom out a chapter or two. Then I look at who the author was, who they were writing to, and what was happening historically. A good study Bible or a commentary like Matthew Henry's can be a huge help. Most importantly, pray for understanding before you dive in. The Holy Spirit is your best guide.",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Bible Study"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_003",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"question": "How do I stay consistent with daily Bible reading?",
|
||||||
|
"answer": "Start small and protect that time like an appointment. Even 10 minutes in the morning before checking your phone can be transformative over a year. A reading plan helps a lot — there are great ones on apps like YouVersion that break the whole Bible into daily chunks so you never have to decide what to read. And give yourself grace on the days you miss; guilt is not a good motivator. Just pick back up where you left off.",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Bible Study"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_007",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"question": "How do I submit a question to Nate?",
|
||||||
|
"answer": "You can submit a question using the contact form at the bottom of this page. Select 'Bible Question' as the message type. Nate reads every question personally and answers the best ones on the site's Q&A section or in a future episode. There's no guarantee every question gets a public response, but all are read and appreciated!",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Podcast"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_faith_family_001",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"question": "How can I share my faith with skeptical family?",
|
||||||
|
"answer": "Lead with relationship, not argument. Skeptical family members usually don't need more information — they need to see the faith lived out genuinely. Consistency, love, and patience over time speaks louder than any debate win.\n\nPray specifically and persistently. This is the most underrated strategy. God moves hearts in ways arguments never can.",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Faith & Life"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_blessed_hope_001",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"question": "What is the blessed hope?",
|
||||||
|
"answer": "The blessed hope is a phrase from Titus 2:13 — \"as we await the blessed hope and glorious appearance of our great God and Savior Jesus Christ.\" In Episode 9, Nate breaks down the Greek: makaria elpis. Makaria is the same word used in the Beatitudes — a divinely favored condition. And elpis in the New Testament is not wishful thinking. Biblical hope is a confident expectation of something certain. Not a wish. An anchor.\n\nThe blessed hope is simply this: Jesus is coming back. Not maybe. He is coming back. And the person who really believes that is in a blessed condition even now, living toward a certainty that changes how everything feels today.",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Titus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_who_is_titus_001",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"question": "Who was Titus?",
|
||||||
|
"answer": "Titus was a Gentile — not Jewish — who came to faith through Paul's ministry. Paul calls him \"my true child in our common faith\" (Titus 1:4). He was one of Paul's closest and most trusted co-workers. In 2 Corinthians 7, Paul describes being comforted by Titus arriving during a difficult season. In chapter 8, Titus helped organize a relief offering for believers in Jerusalem. In Galatians 2, he appears as proof that the gospel is for everyone.\n\nPaul left Titus on the island of Crete with a difficult assignment: appoint elders, correct false teaching, and help a young church find its footing in a challenging culture. The letter of Titus is essentially his briefing.",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Titus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_deny_works_001",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"question": "What does it mean to deny God by your works?",
|
||||||
|
"answer": "Titus 1:16 says: \"They profess to know God, but by their actions they deny Him. They are detestable, disobedient, and unfit for any good deed.\"\n\nIn Episode 6, Nate explains this using three precise words Paul chose. Detestable — morally repulsive before God, regardless of religious appearance. Disobedient — not persuadable, resistant to truth even when confronted with it. Unfit — a Greek metalworking term for metal tested and found counterfeit. Religious on the surface, but no real substance underneath.\n\nThe warning cuts both ways: a person can know the right words, attend the right services, hold the right opinions — and still live in a way that functionally says God doesn't matter. Theology and life are inseparable. What you actually believe will show itself in how you actually live.",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Titus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_grace_trains_001",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"question": "What does grace actually train us to do?",
|
||||||
|
"answer": "Titus 2:11-12 says grace \"instructs us to renounce ungodliness and worldly passions, and to live sensible, upright, and godly lives.\"\n\nIn Episode 8, Nate points out that the Greek word for instructs is paideuō — the education of a child. Not classroom instruction but formation. Grace is the subject doing the teaching.\n\nGrace teaches two things: what to say no to (ungodliness and worldly passions) and what to say yes to — sensible (an ordered mind), upright (right in relationship with others), and godly (God genuinely at the center). Inward, outward, and upward.\n\nThe key insight: the pathway to saying no to sin is going deeper into grace, not trying harder. Grace properly understood reorients your desires — it doesn't just change the rules, it changes what you want. Grace is not a hall pass. It's a school you attend every day.",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Titus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_leadership_titus1_001",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"question": "What does Titus 1 teach about church leadership?",
|
||||||
|
"answer": "Paul's first instruction in Titus was to appoint elders — because a church without mature, qualified leadership is vulnerable. From Episode 4 (Titus 1:5-9):\n\nThe elder's home is exhibit A. Before looking at preaching or public presence, look at the household. If a man cannot lead his own family well, there's no reason to believe he'll lead God's family well. Integrity can't be compartmentalized.\n\nPaul gives five things an elder must NOT be: not self-absorbed, not quick-tempered, not given to drunkenness, not violent, not greedy. And six things he must BE: hospitable, a lover of good, self-controlled, upright, holy, and disciplined in sound doctrine.\n\nThis is not a job description for a CEO. Nate frames it as a picture of Christlike character — authority that flows from below, from a posture of submission and service, not dominance.",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Titus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_titus347_summary_001",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"question": "Give me a summary of Titus 3:4-7",
|
||||||
|
"answer": "Titus 3:4-7 is what Nate calls 'the gospel in one paragraph' — one of the most concentrated summaries of salvation anywhere in Paul's letters. Episode 12 breaks it down:\n\nAll three persons of the Trinity are present: God the Father is the initiating motivation — He is kind and full of love toward humanity. The Holy Spirit is the agent of new birth and ongoing renewal. Jesus Christ is the one through whom the Spirit is poured out, and verse 7 calls him 'our Savior.'\n\nThe hinge of the passage is verse 5: He saved us not by the righteous deeds we had done, but according to His mercy. Not partially. Not mostly. The basis is His mercy, not our merit — which means salvation rests on something that cannot change.\n\nVerse 7 gives us two results: justified (declared righteous in God's courtroom) and heirs (members of the family with a guaranteed inheritance). You are not just legally acquitted — you belong.",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Titus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cb_christians_politics_001",
|
||||||
|
"firstName": "Nate",
|
||||||
|
"question": "How should Christians engage with politics and public life?",
|
||||||
|
"answer": "Titus 3:1-2 is Paul's answer, covered in Episode 10. He tells Titus to remind believers to submit to rulers and authorities, be obedient, be ready for every good work, malign no one, and be peaceable, gentle, and considerate to everyone.\n\nNate's key insight: Paul is not saying agree with every government decision. He's saying your engagement with public life should be marked by something recognizable as different. Submission, readiness to do good, and a peaceable presence are not just civic virtues — they are missionary strategies. The way you navigate public life either opens doors for the gospel or closes them.\n\nThe word for peaceable in Greek is amachous — literally non-combative. Paul doesn't say malign no one except your political opponents. He says no one. This is not weakness. It is the confidence of people who know how the story ends and don't need to win every argument to prove it.",
|
||||||
|
"isApproved": true,
|
||||||
|
"topic": "Faith & Life"
|
||||||
|
}
|
||||||
|
]
|
||||||
+3
-1
@@ -7,6 +7,8 @@ services:
|
|||||||
- "4173:4173"
|
- "4173:4173"
|
||||||
environment:
|
environment:
|
||||||
- PORT=4173
|
- PORT=4173
|
||||||
|
- ADMIN_PASSWORD=`generate a random password and set it here`
|
||||||
|
- RESEND_API_KEY=`set your Resend API key here`
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- /media/ZimaOS-HD/AppData/siteforge/data:/app/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
+16
-7
@@ -1,17 +1,26 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
DATA_FILE=/app/data/admin-content.json
|
DATA_DIR=/app/data
|
||||||
SEED_FILE=/app/data-seed/admin-content.json
|
SEED_DIR=/app/data-seed
|
||||||
|
|
||||||
# Ensure the data directory exists (in case the volume was not mounted)
|
# Ensure the data directory exists (in case the volume was not mounted)
|
||||||
mkdir -p /app/data
|
mkdir -p "$DATA_DIR"
|
||||||
|
|
||||||
# Only seed from image defaults when no live data file exists yet.
|
# Seed each default JSON file only when missing.
|
||||||
# This runs on first boot or a fresh volume, but never overwrites existing data.
|
# This runs on first boot or a fresh volume, but never overwrites existing data.
|
||||||
if [ ! -f "$DATA_FILE" ] && [ -f "$SEED_FILE" ]; then
|
if [ -d "$SEED_DIR" ]; then
|
||||||
echo "[siteforge] No admin-content.json found. Seeding from image defaults..."
|
for seed_file in "$SEED_DIR"/*.json; do
|
||||||
cp "$SEED_FILE" "$DATA_FILE"
|
[ -f "$seed_file" ] || continue
|
||||||
|
|
||||||
|
base_name=$(basename "$seed_file")
|
||||||
|
target_file="$DATA_DIR/$base_name"
|
||||||
|
|
||||||
|
if [ ! -f "$target_file" ]; then
|
||||||
|
echo "[siteforge] No $base_name found. Seeding from image defaults..."
|
||||||
|
cp "$seed_file" "$target_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec node server.js
|
exec node server.js
|
||||||
|
|||||||
Generated
+74
-1
@@ -13,7 +13,8 @@
|
|||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.13.1",
|
"react-router-dom": "^7.13.1",
|
||||||
"remark-gfm": "^4.0.1"
|
"remark-gfm": "^4.0.1",
|
||||||
|
"resend": "^6.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
@@ -863,6 +864,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@stablelib/base64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
@@ -2239,6 +2246,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-sha256": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
@@ -4309,6 +4322,12 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postal-mime": {
|
||||||
|
"version": "2.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz",
|
||||||
|
"integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==",
|
||||||
|
"license": "MIT-0"
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.8",
|
"version": "8.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||||
@@ -4582,6 +4601,27 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resend": {
|
||||||
|
"version": "6.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resend/-/resend-6.10.0.tgz",
|
||||||
|
"integrity": "sha512-i7CwZpYj4Oho1RxsTpLcCUkO08+HiL4NXrm6jLJ2WzJ89UGI8eROSieLONJA3hnUrf1OYnCyfq5F6POnHUMv1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postal-mime": "2.7.4",
|
||||||
|
"svix": "1.88.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@react-email/render": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@react-email/render": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@@ -4866,6 +4906,16 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/standardwebhooks": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@stablelib/base64": "^1.0.0",
|
||||||
|
"fast-sha256": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
@@ -4961,6 +5011,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svix": {
|
||||||
|
"version": "1.88.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/svix/-/svix-1.88.0.tgz",
|
||||||
|
"integrity": "sha512-vm/JrrUd3bVyBE+3L33TIyVSs8gS5fYx7lrISvKlDJXTYX1ACH4REX8P1tHxsSKoZi/rvifM1t0XRc5Vc45THw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"standardwebhooks": "1.0.0",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
@@ -5246,6 +5306,19 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
+6
-4
@@ -5,12 +5,13 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"api": "node server.js",
|
"api": "node --env-file=.env server.js",
|
||||||
"dev:full": "concurrently \"npm:api\" \"npm:dev\"",
|
"dev:full": "concurrently \"npm:api\" \"npm:dev\"",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"start": "node server.js",
|
"start": "node --env-file=.env server.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"chatbot:eval": "node scripts/evaluate-chatbot.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
@@ -18,7 +19,8 @@
|
|||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.13.1",
|
"react-router-dom": "^7.13.1",
|
||||||
"remark-gfm": "^4.0.1"
|
"remark-gfm": "^4.0.1",
|
||||||
|
"resend": "^6.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
|
|||||||
+19
-1
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,92 @@
|
|||||||
|
import { execSync } from 'child_process'
|
||||||
|
import { readFileSync, writeFileSync } from 'fs'
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
|
||||||
|
const BASE = "/Users/nate.emmert/Documents/github/Siteforge/Verse by Verse with Nate Complete Series"
|
||||||
|
const CHATBOT_FILE = "/Users/nate.emmert/Documents/github/Siteforge/data/chatbot-content.json"
|
||||||
|
|
||||||
|
const FILES = [
|
||||||
|
{ file: `${BASE}/Done/Verse_by_Verse_with_Nate_Episode02.docx`, ep: 2 },
|
||||||
|
{ file: `${BASE}/Done/Verse_by_Verse_with_Nate_Episode03.docx`, ep: 3 },
|
||||||
|
{ file: `${BASE}/Done/Verse_by_Verse_with_Nate_Episode04_Updated.docx`, ep: 4 },
|
||||||
|
{ file: `${BASE}/Verse_by_Verse_with_Nate_Episode05.docx`, ep: 5 },
|
||||||
|
{ file: `${BASE}/Verse_by_Verse_with_Nate_Episode06_expanded.docx`, ep: 6 },
|
||||||
|
{ file: `${BASE}/Verse_by_Verse_with_Nate_Episode07.docx`, ep: 7 },
|
||||||
|
{ file: `${BASE}/Verse_by_Verse_with_Nate_Episode08.docx`, ep: 8 },
|
||||||
|
{ file: `${BASE}/Verse_by_Verse_with_Nate_Episode09.docx`, ep: 9 },
|
||||||
|
{ file: `${BASE}/Verse_by_Verse_with_Nate_Episode10.docx`, ep: 10 },
|
||||||
|
{ file: `${BASE}/Verse_by_Verse_with_Nate_Episode11.docx`, ep: 11 },
|
||||||
|
{ file: `${BASE}/Verse_by_Verse_with_Nate_Episode12.docx`, ep: 12 },
|
||||||
|
{ file: `${BASE}/Verse_by_Verse_with_Nate_Episode13.docx`, ep: 13 },
|
||||||
|
{ file: `${BASE}/Verse_by_Verse_with_Nate_Episode14.docx`, ep: 14 },
|
||||||
|
]
|
||||||
|
|
||||||
|
function extractText(filePath) {
|
||||||
|
const xml = execSync(`unzip -p "${filePath}" word/document.xml 2>/dev/null`, { encoding: 'utf8' })
|
||||||
|
return xml
|
||||||
|
.replace(/<[^>]+>/g, ' ')
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'")
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEpisode(raw, epNum) {
|
||||||
|
// Extract episode subtitle and passage reference from header
|
||||||
|
const headerMatch = raw.match(/EPISODE\s+\d+\s*[—\-\u2013\u2014]+\s*(.+?)\s+(Titus\s+[\d:]+(?:\s*[\-\u2013\u2014]+\s*[\d:]+)?)\s*·/i)
|
||||||
|
const subtitle = headerMatch ? headerMatch[1].trim().replace(/\s+/g, ' ') : ''
|
||||||
|
const passageRef = headerMatch ? headerMatch[2].trim() : 'Titus'
|
||||||
|
const episodeTitle = `Episode ${epNum} — ${subtitle || 'Verse by Verse with Nate'}`
|
||||||
|
|
||||||
|
// Find where the actual teaching content starts
|
||||||
|
let contentStart = raw.indexOf('SEGMENT 1')
|
||||||
|
if (contentStart === -1) contentStart = raw.indexOf('WHO WAS PAUL')
|
||||||
|
if (contentStart === -1) contentStart = raw.indexOf('COLD OPEN')
|
||||||
|
if (contentStart === -1) contentStart = 400
|
||||||
|
|
||||||
|
const rawContent = raw.slice(contentStart, contentStart + 4000)
|
||||||
|
const content = rawContent
|
||||||
|
.replace(/\[[^\]]{0,100}\]/g, '') // remove [stage directions]
|
||||||
|
.replace(/[✝🎙️📖💬🧠💡🔑✅◀▶]/gu, '') // remove emoji
|
||||||
|
.replace(/SEGMENT\s+\d+\s*[—\-\u2013]+\s*/g, '\n\n') // turn SEGMENT headers into breaks
|
||||||
|
.replace(/\s{2,}/g, ' ')
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
// Build keyword list
|
||||||
|
const verseRefs = [...new Set((raw.match(/Titus\s+\d+:\d+/g) || []))].slice(0, 5).map(k => k.toLowerCase())
|
||||||
|
const titleWords = subtitle.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 3)
|
||||||
|
const keywords = [...new Set([
|
||||||
|
'titus', `episode ${epNum}`, passageRef.toLowerCase(),
|
||||||
|
...verseRefs, ...titleWords
|
||||||
|
])].slice(0, 20)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: randomUUID(),
|
||||||
|
type: 'episode',
|
||||||
|
title: `${episodeTitle} (${passageRef})`,
|
||||||
|
content: content.slice(0, 3900),
|
||||||
|
keywords,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing entries (keep the 8 hand-written ones)
|
||||||
|
const existing = JSON.parse(readFileSync(CHATBOT_FILE, 'utf8'))
|
||||||
|
// Remove any previously generated episode entries to avoid duplication
|
||||||
|
const baseEntries = existing.filter(e => e.type !== 'episode')
|
||||||
|
|
||||||
|
const newEntries = []
|
||||||
|
for (const { file, ep } of FILES) {
|
||||||
|
try {
|
||||||
|
const raw = extractText(file)
|
||||||
|
const entry = parseEpisode(raw, ep)
|
||||||
|
newEntries.push(entry)
|
||||||
|
console.log(`✓ Ep ${ep}: ${entry.title.slice(0, 80)}`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`✗ Ep ${ep}: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const combined = [...baseEntries, ...newEntries]
|
||||||
|
writeFileSync(CHATBOT_FILE, JSON.stringify(combined, null, 2), 'utf8')
|
||||||
|
console.log(`\nDone. ${newEntries.length} episode entries added. Total: ${combined.length}`)
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const root = process.cwd()
|
||||||
|
const dataPath = path.join(root, 'data', 'chatbot-content.json')
|
||||||
|
const evalPath = path.join(root, 'data', 'chatbot-eval.json')
|
||||||
|
|
||||||
|
const STOP_WORDS = new Set([
|
||||||
|
'a','an','the','is','are','was','were','be','been','being','have','has','had','do','does','did',
|
||||||
|
'will','would','could','should','may','might','shall','can','i','you','he','she','it','we','they',
|
||||||
|
'me','him','her','us','them','my','your','his','its','our','their','this','that','these','those',
|
||||||
|
'and','but','or','nor','so','yet','for','of','in','on','at','to','from','with','by','about',
|
||||||
|
'what','how','why','when','where','who','which','if','then','than','as','just','not',
|
||||||
|
])
|
||||||
|
|
||||||
|
const TOKEN_ALIASES = {
|
||||||
|
bible: ['translation', 'version', 'scripture', 'bsb', 'berean'],
|
||||||
|
translation: ['version', 'bsb', 'berean', 'bible'],
|
||||||
|
version: ['translation', 'bsb', 'berean', 'bible'],
|
||||||
|
elders: ['elder', 'leadership', 'leaders', 'overseer', 'pastor'],
|
||||||
|
leadership: ['elders', 'elder', 'overseer', 'leaders'],
|
||||||
|
grace: ['salvation', 'saved', 'godliness', 'mercy'],
|
||||||
|
salvation: ['saved', 'grace', 'mercy', 'gospel'],
|
||||||
|
saved: ['salvation', 'grace', 'mercy', 'gospel'],
|
||||||
|
hope: ['blessed', 'appearing', 'return', 'coming'],
|
||||||
|
politics: ['public', 'government', 'authorities'],
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenize(text) {
|
||||||
|
return text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9\s]/g, ' ')
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(token => token.length > 2 && !STOP_WORDS.has(token))
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandTokens(tokens) {
|
||||||
|
const expanded = new Set(tokens)
|
||||||
|
for (const token of tokens) {
|
||||||
|
const aliases = TOKEN_ALIASES[token] ?? []
|
||||||
|
for (const alias of aliases) {
|
||||||
|
for (const aliasToken of tokenize(alias)) expanded.add(aliasToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...expanded]
|
||||||
|
}
|
||||||
|
|
||||||
|
function literalTerms(text) {
|
||||||
|
return [...new Set(
|
||||||
|
text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9:\-\s']/g, ' ')
|
||||||
|
.split(/\s+/)
|
||||||
|
.map(term => term.trim())
|
||||||
|
.filter(term => term.length >= 2 && !STOP_WORDS.has(term)),
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreEntry(entry, query) {
|
||||||
|
const indexText = `${entry.title} ${entry.content} ${entry.keywords.join(' ')}`.toLowerCase()
|
||||||
|
const queryTokens = expandTokens(tokenize(query))
|
||||||
|
const terms = literalTerms(query)
|
||||||
|
|
||||||
|
let score = 0
|
||||||
|
|
||||||
|
for (const token of queryTokens) {
|
||||||
|
if (entry.title.toLowerCase().includes(token)) score += 4
|
||||||
|
else if (indexText.includes(token)) score += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const term of terms) {
|
||||||
|
if (entry.title.toLowerCase().includes(term)) score += 2
|
||||||
|
else if (indexText.includes(term)) score += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const phrase = query.toLowerCase().replace(/[^a-z0-9:\-\s']/g, ' ').replace(/\s+/g, ' ').trim()
|
||||||
|
if (phrase.length > 6) {
|
||||||
|
if (entry.title.toLowerCase().includes(phrase)) score += 10
|
||||||
|
else if (indexText.includes(phrase)) score += 6
|
||||||
|
}
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const [contentRaw, evalRaw] = await Promise.all([
|
||||||
|
fs.readFile(dataPath, 'utf8'),
|
||||||
|
fs.readFile(evalPath, 'utf8'),
|
||||||
|
])
|
||||||
|
|
||||||
|
const entries = JSON.parse(contentRaw)
|
||||||
|
const tests = JSON.parse(evalRaw)
|
||||||
|
|
||||||
|
let pass = 0
|
||||||
|
const failures = []
|
||||||
|
|
||||||
|
for (const test of tests) {
|
||||||
|
const ranked = entries
|
||||||
|
.map(entry => ({ entry, score: scoreEntry(entry, test.query) }))
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
|
||||||
|
const topK = ranked.slice(0, test.expectedTopK)
|
||||||
|
const hit = topK.some(item => item.entry.id === test.expectedEntryId)
|
||||||
|
|
||||||
|
if (hit) {
|
||||||
|
pass += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
failures.push({
|
||||||
|
query: test.query,
|
||||||
|
expectedEntryId: test.expectedEntryId,
|
||||||
|
expectedTopK: test.expectedTopK,
|
||||||
|
actualTop: topK.map(item => ({ id: item.entry.id, title: item.entry.title, score: item.score })),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = tests.length
|
||||||
|
const pct = ((pass / total) * 100).toFixed(1)
|
||||||
|
|
||||||
|
console.log(`Chatbot eval: ${pass}/${total} (${pct}%) passed`)
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.log('\nFailures:')
|
||||||
|
for (const failure of failures) {
|
||||||
|
console.log(`- Query: ${failure.query}`)
|
||||||
|
console.log(` Expected: ${failure.expectedEntryId} in top ${failure.expectedTopK}`)
|
||||||
|
console.log(` Actual: ${failure.actualTop.map(item => `${item.id} (${item.score})`).join(', ')}`)
|
||||||
|
}
|
||||||
|
process.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
process.exitCode = 1
|
||||||
|
})
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { readFileSync, writeFileSync } from 'fs'
|
||||||
|
|
||||||
|
const path = '/Users/nate.emmert/Documents/github/Siteforge/data/chatbot-content.json'
|
||||||
|
const data = JSON.parse(readFileSync(path, 'utf8'))
|
||||||
|
|
||||||
|
const fixes = {
|
||||||
|
2: { title: 'Episode 2 — Introduction to Titus (Background & Overview)', extra: ['introduction', 'background', 'overview', 'crete', 'letter'] },
|
||||||
|
5: { title: 'Episode 5 — The Danger of Empty Words (Titus 1:10–13a)', extra: ['danger', 'empty', 'words', 'false', 'teacher', 'titus 1:10', 'titus 1:13'] },
|
||||||
|
6: { title: 'Episode 6 — Words That Deny What We Claim to Believe (Titus 1:13b–16)', extra: ['deny', 'claim', 'believe', 'titus 1:13', 'titus 1:16'] },
|
||||||
|
14: { title: 'Episode 14 — Grace: Where It Starts and Where It Ends (Titus 3:12–15)', extra: ['grace', 'starts', 'ends', 'review', 'titus 3:12', 'titus 3:15'] },
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
for (const entry of data) {
|
||||||
|
const m = entry.title.match(/^Episode (\d+)/)
|
||||||
|
if (!m) continue
|
||||||
|
const ep = Number(m[1])
|
||||||
|
if (fixes[ep]) {
|
||||||
|
entry.title = fixes[ep].title
|
||||||
|
entry.keywords = [...new Set([...entry.keywords, ...fixes[ep].extra])].slice(0, 20)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(path, JSON.stringify(data, null, 2), 'utf8')
|
||||||
|
console.log(`Fixed ${count} entries. Total: ${data.length}`)
|
||||||
+1223
-6
File diff suppressed because it is too large
Load Diff
+1711
-1
File diff suppressed because it is too large
Load Diff
+845
-7
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Routes, Route } from 'react-router-dom'
|
import { Link, Routes, Route, useNavigate } from 'react-router-dom'
|
||||||
import AdminPage from './AdminPage'
|
import AdminPage from './AdminPage'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ const YOUTUBE_URL = 'https://www.youtube.com/@blackzebraem5558'
|
|||||||
const AMAZON_MUSIC_URL =
|
const AMAZON_MUSIC_URL =
|
||||||
'https://music.amazon.com/podcasts/202322bf-db86-4e7d-9a6b-4db7cbccbccf/verse-by-verse-with-nate'
|
'https://music.amazon.com/podcasts/202322bf-db86-4e7d-9a6b-4db7cbccbccf/verse-by-verse-with-nate'
|
||||||
const FACEBOOK_URL = 'https://facebook.com/versebyversewithnate'
|
const FACEBOOK_URL = 'https://facebook.com/versebyversewithnate'
|
||||||
|
const CONSENT_KEY = 'vbn_analytics_consent_choice'
|
||||||
|
|
||||||
function FacebookIcon() {
|
function FacebookIcon() {
|
||||||
return (
|
return (
|
||||||
@@ -35,6 +36,32 @@ export interface CustomBlock {
|
|||||||
body: string
|
body: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ArchivedSeriesResourceLink {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArchivedSeriesNote {
|
||||||
|
id: string
|
||||||
|
heading: string
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArchivedSeries {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
imageUrl: string
|
||||||
|
listenUrl: string
|
||||||
|
studyGuideTitle: string
|
||||||
|
studyGuideDescription: string
|
||||||
|
studyGuideUrl: string
|
||||||
|
resourceLinks: ArchivedSeriesResourceLink[]
|
||||||
|
notes: ArchivedSeriesNote[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface SiteContent {
|
export interface SiteContent {
|
||||||
eyebrow: string
|
eyebrow: string
|
||||||
heroTagline: string
|
heroTagline: string
|
||||||
@@ -45,6 +72,8 @@ export interface SiteContent {
|
|||||||
seriesLabel: string
|
seriesLabel: string
|
||||||
seriesTitle: string
|
seriesTitle: string
|
||||||
seriesDescription: string
|
seriesDescription: string
|
||||||
|
seriesImageUrl: string
|
||||||
|
seriesListenUrl: string
|
||||||
studyGuideTitle: string
|
studyGuideTitle: string
|
||||||
studyGuideDescription: string
|
studyGuideDescription: string
|
||||||
studyGuideUrl: string
|
studyGuideUrl: string
|
||||||
@@ -52,6 +81,7 @@ export interface SiteContent {
|
|||||||
shareP: string
|
shareP: string
|
||||||
customLinks: CustomLink[]
|
customLinks: CustomLink[]
|
||||||
customBlocks: CustomBlock[]
|
customBlocks: CustomBlock[]
|
||||||
|
archivedSeries: ArchivedSeries[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULTS: SiteContent = {
|
export const DEFAULTS: SiteContent = {
|
||||||
@@ -63,19 +93,22 @@ export const DEFAULTS: SiteContent = {
|
|||||||
aboutShowP2:
|
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.",
|
"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.",
|
||||||
aboutNate:
|
aboutNate:
|
||||||
'Nate Emmert is a husband, dad, and lifelong student of the Bible from Nashville, TN. He created Verse by Verse to share the joy of deep Scripture study in a format anyone can follow along with — no seminary required.',
|
'Nate Emmert is a husband, dad, and lifelong student of the Bible from Lynchburg, Va. He created Verse by Verse to share the joy of deep Scripture study in a format anyone can follow along with — no seminary required.',
|
||||||
seriesLabel: 'Now Playing',
|
seriesLabel: 'Now Playing',
|
||||||
seriesTitle: 'Study of Titus: Sound Doctrine',
|
seriesTitle: 'Study of Titus: Sound Doctrine',
|
||||||
seriesDescription:
|
seriesDescription:
|
||||||
"A deep-dive into Paul's letter to Titus — unpacking what it means to build a church and a life on sound doctrine.",
|
"A deep-dive into Paul's letter to Titus — unpacking what it means to build a church and a life on sound doctrine.",
|
||||||
|
seriesImageUrl: '/images/titus-cover.png',
|
||||||
|
seriesListenUrl: SPOTIFY_SHOW_URL,
|
||||||
studyGuideTitle: 'Companion Study Guide',
|
studyGuideTitle: 'Companion Study Guide',
|
||||||
studyGuideDescription:
|
studyGuideDescription:
|
||||||
'Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.',
|
'Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.',
|
||||||
studyGuideUrl: 'https://a.co/d/01sG2tOJ',
|
studyGuideUrl: 'https://a.co/d/01sG2tOJ',
|
||||||
shareHeading: "Know someone who'd love this?",
|
shareHeading: 'Help one more person hear the Word this week.',
|
||||||
shareP: 'Scan the QR code or share the link to introduce a friend to Verse by Verse with Nate.',
|
shareP: 'Scan the QR code or text the show link to a friend who needs encouragement today.',
|
||||||
customLinks: [],
|
customLinks: [],
|
||||||
customBlocks: [],
|
customBlocks: [],
|
||||||
|
archivedSeries: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
function SpotifyIcon() {
|
function SpotifyIcon() {
|
||||||
@@ -102,7 +135,367 @@ function AmazonMusicIcon() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PublicQuestion {
|
||||||
|
id: string
|
||||||
|
firstName: string
|
||||||
|
question: string
|
||||||
|
answer: string
|
||||||
|
topic?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const QA_PAGE_SIZE = 6
|
||||||
|
|
||||||
|
function QASection() {
|
||||||
|
const [questions, setQuestions] = useState<PublicQuestion[]>([])
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [selectedTopic, setSelectedTopic] = useState<string | null>(null)
|
||||||
|
const [expanded, setExpanded] = useState<{ [key: string]: boolean }>({})
|
||||||
|
const [page, setPage] = useState(0)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/questions')
|
||||||
|
.then(r => (r.ok ? r.json() : Promise.reject(new Error('Failed to load questions'))))
|
||||||
|
.then(data => {
|
||||||
|
setQuestions((data as { questions: PublicQuestion[] }).questions ?? [])
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const topics = Array.from(new Set(questions.map(q => q.topic).filter(Boolean))) as string[]
|
||||||
|
|
||||||
|
const filteredQuestions = questions.filter(q => {
|
||||||
|
const matchesTopic = !selectedTopic || q.topic === selectedTopic
|
||||||
|
const matchesSearch = !searchQuery ||
|
||||||
|
q.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
q.answer.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
return matchesTopic && matchesSearch
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(filteredQuestions.length / QA_PAGE_SIZE)
|
||||||
|
const pagedQuestions = filteredQuestions.slice(page * QA_PAGE_SIZE, (page + 1) * QA_PAGE_SIZE)
|
||||||
|
|
||||||
|
const toggleExpanded = (id: string) => {
|
||||||
|
setExpanded(e => ({ ...e, [id]: !e[id] }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearchQuery(value)
|
||||||
|
setPage(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTopic = (topic: string | null) => {
|
||||||
|
setSelectedTopic(topic)
|
||||||
|
setPage(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="qa" className="section-qa" aria-label="Questions and answers">
|
||||||
|
<div className="section-inner">
|
||||||
|
<h2 className="section-heading">
|
||||||
|
<span className="ornament">✦</span> Questions & Answers <span className="ornament">✦</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<p className="qa-no-results">Loading questions…</p>
|
||||||
|
) : questions.length === 0 ? (
|
||||||
|
<div className="qa-no-results">
|
||||||
|
<p>No questions have been answered yet. <a href="#contact">Submit yours below!</a></p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="qa-filters">
|
||||||
|
<div className="qa-topics">
|
||||||
|
<button
|
||||||
|
className={`qa-topic-btn${selectedTopic === null ? ' qa-topic-btn--active' : ''}`}
|
||||||
|
onClick={() => handleTopic(null)}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
{topics.map(topic => (
|
||||||
|
<button
|
||||||
|
key={topic}
|
||||||
|
className={`qa-topic-btn${selectedTopic === topic ? ' qa-topic-btn--active' : ''}`}
|
||||||
|
onClick={() => handleTopic(topic)}
|
||||||
|
>
|
||||||
|
{topic}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="qa-search">
|
||||||
|
<label>
|
||||||
|
<span className="visually-hidden">Search Questions</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search questions…"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={e => handleSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredQuestions.length === 0 ? (
|
||||||
|
<div className="qa-no-results">
|
||||||
|
<p>No matching questions found. <a href="#contact">Submit your question</a></p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="qa-cards">
|
||||||
|
{pagedQuestions.map(question => (
|
||||||
|
<div key={question.id} className="qa-card-scene">
|
||||||
|
<div
|
||||||
|
className={`qa-card-inner ${expanded[question.id] ? 'flipped' : ''}`}
|
||||||
|
onClick={() => toggleExpanded(question.id)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-expanded={!!expanded[question.id]}
|
||||||
|
aria-label={question.question}
|
||||||
|
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && toggleExpanded(question.id)}
|
||||||
|
>
|
||||||
|
<div className="qa-card-face qa-card-front">
|
||||||
|
<span className="qa-face-label">Q</span>
|
||||||
|
<p className="qa-question-text">{question.question}</p>
|
||||||
|
<span className="qa-flip-hint">{expanded[question.id] ? '▲' : '▼'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="qa-card-face qa-card-back">
|
||||||
|
<span className="qa-face-label">A</span>
|
||||||
|
<p className="qa-answer-text">{question.answer}</p>
|
||||||
|
<p style={{ margin: '0.75rem 0 0', fontSize: '0.8rem', color: '#a89060', fontStyle: 'italic' }}>— Answered by Nate</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="qa-pagination">
|
||||||
|
<button
|
||||||
|
className="qa-page-btn"
|
||||||
|
onClick={() => setPage(p => p - 1)}
|
||||||
|
disabled={page === 0}
|
||||||
|
aria-label="Previous page"
|
||||||
|
>
|
||||||
|
‹ Prev
|
||||||
|
</button>
|
||||||
|
<span className="qa-page-info">
|
||||||
|
{page + 1} / {totalPages}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="qa-page-btn"
|
||||||
|
onClick={() => setPage(p => p + 1)}
|
||||||
|
disabled={page >= totalPages - 1}
|
||||||
|
aria-label="Next page"
|
||||||
|
>
|
||||||
|
Next ›
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContactForm() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [fields, setFields] = useState({ name: '', email: '', message: '', messageType: 'question' })
|
||||||
|
const [subscribe, setSubscribe] = useState(true)
|
||||||
|
const [honey, setHoney] = useState('')
|
||||||
|
const [status, setStatus] = useState<'idle' | 'submitting' | 'error'>('idle')
|
||||||
|
const [errorMsg, setErrorMsg] = useState('')
|
||||||
|
|
||||||
|
function handleChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
||||||
|
setFields(f => ({ ...f, [e.target.name]: e.target.value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
|
setFields(f => ({ ...f, [e.target.name]: e.target.value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
setStatus('submitting')
|
||||||
|
setErrorMsg('')
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/contact', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ...fields, subscribe, _honey: honey }),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
|
setErrorMsg((data as { message?: string }).message ?? 'Something went wrong. Please try again.')
|
||||||
|
setStatus('error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigate('/thanks')
|
||||||
|
} catch {
|
||||||
|
setErrorMsg('Could not connect. Please try again later.')
|
||||||
|
setStatus('error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="contact-form" onSubmit={handleSubmit} noValidate>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="contact-honeypot"
|
||||||
|
tabIndex={-1}
|
||||||
|
autoComplete="off"
|
||||||
|
aria-hidden="true"
|
||||||
|
value={honey}
|
||||||
|
onChange={e => setHoney(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label>
|
||||||
|
Name
|
||||||
|
<input type="text" name="name" required autoComplete="name" value={fields.name} onChange={handleChange} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Email
|
||||||
|
<input type="email" name="email" required autoComplete="email" value={fields.email} onChange={handleChange} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Message Type
|
||||||
|
<select name="messageType" value={fields.messageType} onChange={handleSelectChange}>
|
||||||
|
<option value="question">Bible Question</option>
|
||||||
|
<option value="testimony">Testimony</option>
|
||||||
|
<option value="topic">Topic Request</option>
|
||||||
|
<option value="general">General Message</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Message
|
||||||
|
<textarea name="message" rows={6} required value={fields.message} onChange={handleChange} />
|
||||||
|
</label>
|
||||||
|
<label className="contact-consent">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={subscribe}
|
||||||
|
onChange={e => setSubscribe(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>Send me updates from Verse by Verse with Nate. I can unsubscribe anytime.</span>
|
||||||
|
</label>
|
||||||
|
{status === 'error' && <p className="contact-error">{errorMsg}</p>}
|
||||||
|
<button type="submit" className="btn-primary" disabled={status === 'submitting'}>
|
||||||
|
{status === 'submitting' ? 'Sending…' : 'Send Message'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnalyticsConsentBanner() {
|
||||||
|
const [choice, setChoice] = useState<'unknown' | 'accepted' | 'declined'>(() => {
|
||||||
|
const saved = localStorage.getItem(CONSENT_KEY)
|
||||||
|
if (saved === 'accepted' || saved === 'declined') return saved
|
||||||
|
return 'unknown'
|
||||||
|
})
|
||||||
|
|
||||||
|
async function sendChoice(consent: boolean) {
|
||||||
|
await fetch('/api/analytics-consent', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ consent }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function accept() {
|
||||||
|
setChoice('accepted')
|
||||||
|
localStorage.setItem(CONSENT_KEY, 'accepted')
|
||||||
|
try {
|
||||||
|
await sendChoice(true)
|
||||||
|
} catch {
|
||||||
|
// Keep local preference even if network fails.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decline() {
|
||||||
|
setChoice('declined')
|
||||||
|
localStorage.setItem(CONSENT_KEY, 'declined')
|
||||||
|
try {
|
||||||
|
await sendChoice(false)
|
||||||
|
} catch {
|
||||||
|
// Keep local preference even if network fails.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (choice !== 'unknown') return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="consent-banner" role="region" aria-label="Analytics consent">
|
||||||
|
<p>
|
||||||
|
We use optional analytics cookies to measure visits and location trends for site improvement.
|
||||||
|
</p>
|
||||||
|
<div className="consent-actions">
|
||||||
|
<button type="button" className="btn-primary" onClick={accept}>Accept</button>
|
||||||
|
<button type="button" className="btn-secondary" onClick={decline}>Decline</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShareShowButton() {
|
||||||
|
const [feedback, setFeedback] = useState('')
|
||||||
|
|
||||||
|
async function handleShare() {
|
||||||
|
const shareUrl = window.location.origin
|
||||||
|
const shareTitle = 'Verse by Verse with Nate'
|
||||||
|
const shareText = 'Check out Verse by Verse with Nate.'
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (navigator.share) {
|
||||||
|
await navigator.share({ title: shareTitle, text: shareText, url: shareUrl })
|
||||||
|
setFeedback('Shared successfully.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(shareUrl)
|
||||||
|
setFeedback('Link copied. Share it with a friend.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setFeedback(`Copy this link: ${shareUrl}`)
|
||||||
|
} catch {
|
||||||
|
setFeedback('Share canceled.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="share-show-wrap">
|
||||||
|
<button type="button" className="btn-primary btn-share-show" onClick={handleShare}>
|
||||||
|
Share the Show
|
||||||
|
</button>
|
||||||
|
{feedback && <p className="share-feedback">{feedback}</p>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function LandingPage({ content }: { content: SiteContent }) {
|
function LandingPage({ content }: { content: SiteContent }) {
|
||||||
|
const archivedSeries = content.archivedSeries ?? []
|
||||||
|
const [activeArchivedIndex, setActiveArchivedIndex] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveArchivedIndex(0)
|
||||||
|
}, [archivedSeries.length])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (archivedSeries.length <= 1) return undefined
|
||||||
|
|
||||||
|
const intervalId = window.setInterval(() => {
|
||||||
|
setActiveArchivedIndex(i => (i + 1) % archivedSeries.length)
|
||||||
|
}, 7000)
|
||||||
|
|
||||||
|
return () => window.clearInterval(intervalId)
|
||||||
|
}, [archivedSeries.length])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="site">
|
<div className="site">
|
||||||
{/* ── HEADER ── */}
|
{/* ── HEADER ── */}
|
||||||
@@ -112,7 +505,9 @@ function LandingPage({ content }: { content: SiteContent }) {
|
|||||||
<nav className="header-nav">
|
<nav className="header-nav">
|
||||||
<a href="#listen">Listen</a>
|
<a href="#listen">Listen</a>
|
||||||
<a href="#about">About</a>
|
<a href="#about">About</a>
|
||||||
|
<a href="#qa">Q&A</a>
|
||||||
<a href="#series">Series</a>
|
<a href="#series">Series</a>
|
||||||
|
<a href="#contact">Contact</a>
|
||||||
<a
|
<a
|
||||||
href={SPOTIFY_SHOW_URL}
|
href={SPOTIFY_SHOW_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -241,7 +636,7 @@ function LandingPage({ content }: { content: SiteContent }) {
|
|||||||
<div className="series-grid">
|
<div className="series-grid">
|
||||||
<article className="series-card">
|
<article className="series-card">
|
||||||
<img
|
<img
|
||||||
src="/images/titus-cover.png"
|
src={content.seriesImageUrl}
|
||||||
alt={content.seriesTitle}
|
alt={content.seriesTitle}
|
||||||
className="series-art"
|
className="series-art"
|
||||||
/>
|
/>
|
||||||
@@ -250,7 +645,7 @@ function LandingPage({ content }: { content: SiteContent }) {
|
|||||||
<h3>{content.seriesTitle}</h3>
|
<h3>{content.seriesTitle}</h3>
|
||||||
<p>{content.seriesDescription}</p>
|
<p>{content.seriesDescription}</p>
|
||||||
<a
|
<a
|
||||||
href={SPOTIFY_SHOW_URL}
|
href={content.seriesListenUrl || SPOTIFY_SHOW_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="btn-primary"
|
className="btn-primary"
|
||||||
@@ -264,6 +659,117 @@ function LandingPage({ content }: { content: SiteContent }) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{archivedSeries.length > 0 && (
|
||||||
|
<section className="section-archive-series" aria-label="Archived studies">
|
||||||
|
<div className="section-inner">
|
||||||
|
<h2 className="section-heading">
|
||||||
|
<span className="ornament">✦</span> Archived Studies <span className="ornament">✦</span>
|
||||||
|
</h2>
|
||||||
|
<div className="archive-series-rotator" role="region" aria-label="Archived studies carousel">
|
||||||
|
{archivedSeries.length > 1 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="archive-rotate-btn"
|
||||||
|
aria-label="Previous archived study"
|
||||||
|
onClick={() => setActiveArchivedIndex(i => (i - 1 + archivedSeries.length) % archivedSeries.length)}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="archive-series-list">
|
||||||
|
<div
|
||||||
|
className="archive-series-track"
|
||||||
|
style={{ transform: `translateX(-${activeArchivedIndex * 100}%)` }}
|
||||||
|
>
|
||||||
|
{archivedSeries.map(series => (
|
||||||
|
<article key={series.id} className="archive-series-card">
|
||||||
|
{series.imageUrl && (
|
||||||
|
<img src={series.imageUrl} alt={series.title} className="archive-series-art" />
|
||||||
|
)}
|
||||||
|
<div className="archive-series-copy">
|
||||||
|
{series.label && <p className="series-label">{series.label}</p>}
|
||||||
|
<h3>{series.title}</h3>
|
||||||
|
<p>{series.description}</p>
|
||||||
|
|
||||||
|
<div className="archive-series-actions">
|
||||||
|
{series.listenUrl && (
|
||||||
|
<a href={series.listenUrl} target="_blank" rel="noreferrer" className="btn-secondary">
|
||||||
|
Listen to Series
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{series.studyGuideUrl && (
|
||||||
|
<a href={series.studyGuideUrl} target="_blank" rel="noreferrer" className="btn-primary">
|
||||||
|
{series.studyGuideTitle || 'Study Guide'}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{series.studyGuideDescription && (
|
||||||
|
<p className="archive-series-study-guide">{series.studyGuideDescription}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(series.resourceLinks ?? []).length > 0 && (
|
||||||
|
<div className="archive-series-resources">
|
||||||
|
<h4>Resources</h4>
|
||||||
|
<div className="resources-list">
|
||||||
|
{series.resourceLinks.map(link => (
|
||||||
|
<a key={link.id} href={link.url} target="_blank" rel="noreferrer" className="resource-link">
|
||||||
|
{link.label} →
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(series.notes ?? []).length > 0 && (
|
||||||
|
<div className="archive-series-notes">
|
||||||
|
{series.notes.map(note => (
|
||||||
|
<div key={note.id} className="archive-series-note">
|
||||||
|
<h4>{note.heading}</h4>
|
||||||
|
<p>{note.body}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{archivedSeries.length > 1 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="archive-rotate-btn"
|
||||||
|
aria-label="Next archived study"
|
||||||
|
onClick={() => setActiveArchivedIndex(i => (i + 1) % archivedSeries.length)}
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{archivedSeries.length > 1 && (
|
||||||
|
<div className="archive-rotator-dots" aria-label="Archived study selection">
|
||||||
|
{archivedSeries.map((series, idx) => (
|
||||||
|
<button
|
||||||
|
key={`${series.id}-dot`}
|
||||||
|
type="button"
|
||||||
|
className={`archive-rotator-dot${idx === activeArchivedIndex ? ' archive-rotator-dot--active' : ''}`}
|
||||||
|
aria-label={`Show archived study ${idx + 1}`}
|
||||||
|
onClick={() => setActiveArchivedIndex(idx)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="archive-rotator-count">
|
||||||
|
{activeArchivedIndex + 1} / {archivedSeries.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── STUDY GUIDE ── */}
|
{/* ── STUDY GUIDE ── */}
|
||||||
<section className="section-guide" aria-label="Companion study guide">
|
<section className="section-guide" aria-label="Companion study guide">
|
||||||
<div className="section-inner guide-inner">
|
<div className="section-inner guide-inner">
|
||||||
@@ -307,6 +813,7 @@ function LandingPage({ content }: { content: SiteContent }) {
|
|||||||
>
|
>
|
||||||
creators.spotify.com/pod/profile/nmemmert
|
creators.spotify.com/pod/profile/nmemmert
|
||||||
</a>
|
</a>
|
||||||
|
<ShareShowButton />
|
||||||
</div>
|
</div>
|
||||||
<div className="qr-code-wrap">
|
<div className="qr-code-wrap">
|
||||||
<div className="qr-frame">
|
<div className="qr-frame">
|
||||||
@@ -321,6 +828,26 @@ function LandingPage({ content }: { content: SiteContent }) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* ── Q&A ── */}
|
||||||
|
<QASection />
|
||||||
|
|
||||||
|
{/* ── CONTACT ── */}
|
||||||
|
<section className="section-contact" id="contact" aria-label="Contact form">
|
||||||
|
<div className="section-inner contact-inner">
|
||||||
|
<div className="contact-copy">
|
||||||
|
<p className="eyebrow">Get in Touch</p>
|
||||||
|
<h2 className="section-heading">
|
||||||
|
<span className="ornament">✦</span> Contact Nate <span className="ornament">✦</span>
|
||||||
|
</h2>
|
||||||
|
<p className="contact-highlight">
|
||||||
|
Ask a Bible question, share a testimony, or request a topic for a future episode.
|
||||||
|
</p>
|
||||||
|
<p>We usually respond within 48 hours.</p>
|
||||||
|
</div>
|
||||||
|
<ContactForm />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* ── CUSTOM RESOURCES ── */}
|
{/* ── CUSTOM RESOURCES ── */}
|
||||||
{(content.customLinks ?? []).filter(l => l.placement === 'resources').length > 0 && (
|
{(content.customLinks ?? []).filter(l => l.placement === 'resources').length > 0 && (
|
||||||
<section className="section-resources" aria-label="More resources">
|
<section className="section-resources" aria-label="More resources">
|
||||||
@@ -355,6 +882,11 @@ function LandingPage({ content }: { content: SiteContent }) {
|
|||||||
<p className="footer-ornament">✦ ✦ ✦</p>
|
<p className="footer-ornament">✦ ✦ ✦</p>
|
||||||
<p className="footer-title">Verse by Verse with Nate</p>
|
<p className="footer-title">Verse by Verse with Nate</p>
|
||||||
<p className="footer-sub">A Journey Through Scripture</p>
|
<p className="footer-sub">A Journey Through Scripture</p>
|
||||||
|
<p className="footer-contact">
|
||||||
|
Contact:{' '}
|
||||||
|
<a href="mailto:hello@versebyversewithnate.us">hello@versebyversewithnate.us</a>
|
||||||
|
</p>
|
||||||
|
<p className="footer-response">We usually reply within 48 hours.</p>
|
||||||
<nav className="footer-links" aria-label="Footer links">
|
<nav className="footer-links" aria-label="Footer links">
|
||||||
<a href={SPOTIFY_SHOW_URL} target="_blank" rel="noreferrer">Spotify</a>
|
<a href={SPOTIFY_SHOW_URL} target="_blank" rel="noreferrer">Spotify</a>
|
||||||
<span aria-hidden="true">·</span>
|
<span aria-hidden="true">·</span>
|
||||||
@@ -372,12 +904,288 @@ function LandingPage({ content }: { content: SiteContent }) {
|
|||||||
<a key={l.id} href={l.url} target="_blank" rel="noreferrer">{l.label}</a>,
|
<a key={l.id} href={l.url} target="_blank" rel="noreferrer">{l.label}</a>,
|
||||||
])}
|
])}
|
||||||
</nav>
|
</nav>
|
||||||
|
<nav className="footer-links footer-links--legal" aria-label="Legal links">
|
||||||
|
<Link to="/privacy">Privacy Policy</Link>
|
||||||
|
<span aria-hidden="true">·</span>
|
||||||
|
<Link to="/terms">Terms</Link>
|
||||||
|
</nav>
|
||||||
<p className="footer-copy">© 2026 Nate Emmert · Made with faith.</p>
|
<p className="footer-copy">© 2026 Nate Emmert · Made with faith.</p>
|
||||||
|
<p className="footer-privacy">
|
||||||
|
Privacy: with consent, analytics may store masked IP-based location data (country/state/county/city) and returning visitor activity.
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<AnalyticsConsentBanner />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ThankYouPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [secondsLeft, setSecondsLeft] = useState(15)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
navigate('/')
|
||||||
|
}, 15000)
|
||||||
|
|
||||||
|
const intervalId = window.setInterval(() => {
|
||||||
|
setSecondsLeft(s => (s > 0 ? s - 1 : 0))
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timeoutId)
|
||||||
|
window.clearInterval(intervalId)
|
||||||
|
}
|
||||||
|
}, [navigate])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="thanks-page" aria-label="Thank you">
|
||||||
|
<div className="thanks-card">
|
||||||
|
<p className="eyebrow">Message Received</p>
|
||||||
|
<h1>Thank You</h1>
|
||||||
|
<p>
|
||||||
|
Your message was sent successfully. We appreciate you reaching out and will get back to you soon.
|
||||||
|
</p>
|
||||||
|
<p className="thanks-countdown">Returning to the main site in {secondsLeft} seconds...</p>
|
||||||
|
<button type="button" className="btn-primary" onClick={() => navigate('/')}>
|
||||||
|
Go Back Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubscribePage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [honey, setHoney] = useState('')
|
||||||
|
const [status, setStatus] = useState<'idle' | 'submitting' | 'error'>('idle')
|
||||||
|
const [errorMsg, setErrorMsg] = useState('')
|
||||||
|
|
||||||
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
setStatus('submitting')
|
||||||
|
setErrorMsg('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/contact', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
messageType: 'general',
|
||||||
|
message: 'Newsletter signup from subscribe page',
|
||||||
|
subscribe: true,
|
||||||
|
_honey: honey,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
|
setErrorMsg((data as { message?: string }).message ?? 'Something went wrong. Please try again.')
|
||||||
|
setStatus('error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate('/subscribe/thanks')
|
||||||
|
} catch {
|
||||||
|
setErrorMsg('Could not connect. Please try again later.')
|
||||||
|
setStatus('error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="thanks-page" aria-label="Subscribe">
|
||||||
|
<div className="thanks-card subscribe-card">
|
||||||
|
<p className="eyebrow">Verse by Verse with Nate</p>
|
||||||
|
<h1>Subscribe for Updates</h1>
|
||||||
|
<p>
|
||||||
|
Get ministry updates and new episode announcements by email.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form className="contact-form subscribe-form" onSubmit={handleSubmit} noValidate>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="contact-honeypot"
|
||||||
|
tabIndex={-1}
|
||||||
|
autoComplete="off"
|
||||||
|
aria-hidden="true"
|
||||||
|
value={honey}
|
||||||
|
onChange={e => setHoney(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label>
|
||||||
|
Name
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
required
|
||||||
|
autoComplete="name"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Email
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
autoComplete="email"
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{status === 'error' && <p className="contact-error">{errorMsg}</p>}
|
||||||
|
|
||||||
|
<button type="submit" className="btn-primary" disabled={status === 'submitting'}>
|
||||||
|
{status === 'submitting' ? 'Subscribing…' : 'Subscribe'}
|
||||||
|
</button>
|
||||||
|
<Link to="/" className="btn-secondary">Back to Site</Link>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubscribeThankYouPage() {
|
||||||
|
return (
|
||||||
|
<main className="thanks-page" aria-label="Subscription confirmed">
|
||||||
|
<div className="thanks-card">
|
||||||
|
<p className="eyebrow">Verse by Verse with Nate</p>
|
||||||
|
<h1>You are subscribed</h1>
|
||||||
|
<p>
|
||||||
|
Thank you for subscribing. You will receive future ministry updates and episode announcements.
|
||||||
|
</p>
|
||||||
|
<Link to="/" className="btn-primary">Back to Site</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegalPage({ title, body }: { title: string; body: string[] }) {
|
||||||
|
return (
|
||||||
|
<main className="thanks-page" aria-label={title}>
|
||||||
|
<div className="thanks-card">
|
||||||
|
<p className="eyebrow">Verse by Verse with Nate</p>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
{body.map((line, idx) => (
|
||||||
|
<p key={`${title}-${idx}`}>{line}</p>
|
||||||
|
))}
|
||||||
|
<Link to="/" className="btn-primary">Back to Site</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AdminShell({ content, onSave }: { content: SiteContent; onSave: (c: SiteContent) => void }) {
|
||||||
|
const [status, setStatus] = useState<'checking' | 'authenticated' | 'unauthenticated' | 'misconfigured'>('checking')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [errorMsg, setErrorMsg] = useState('')
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/admin-auth/status')
|
||||||
|
.then(r => (r.ok ? r.json() : Promise.reject(new Error('Status failed'))))
|
||||||
|
.then(data => {
|
||||||
|
const next = data as { authenticated?: boolean; configured?: boolean }
|
||||||
|
if (next.configured === false) {
|
||||||
|
setStatus('misconfigured')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStatus(next.authenticated ? 'authenticated' : 'unauthenticated')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setStatus('unauthenticated')
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function handleLogin(e: React.FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
setSubmitting(true)
|
||||||
|
setErrorMsg('')
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin-auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ password }),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
|
setErrorMsg((data as { message?: string }).message ?? 'Login failed.')
|
||||||
|
setSubmitting(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStatus('authenticated')
|
||||||
|
setPassword('')
|
||||||
|
} catch {
|
||||||
|
setErrorMsg('Login failed.')
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
try {
|
||||||
|
await fetch('/api/admin-auth/logout', { method: 'POST' })
|
||||||
|
} finally {
|
||||||
|
setStatus('unauthenticated')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'authenticated') {
|
||||||
|
return <AdminPage content={content} onSave={onSave} onLogout={handleLogout} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="admin-auth-page" aria-label="Admin sign in">
|
||||||
|
<div className="admin-auth-card">
|
||||||
|
<p className="eyebrow">Admin Access</p>
|
||||||
|
<h1>{status === 'misconfigured' ? 'Admin Not Configured' : 'Sign in to Admin'}</h1>
|
||||||
|
{status === 'checking' && <p className="admin-auth-note">Checking session...</p>}
|
||||||
|
{status === 'misconfigured' && (
|
||||||
|
<p className="admin-auth-note">Set the ADMIN_PASSWORD environment variable on the server to enable admin login.</p>
|
||||||
|
)}
|
||||||
|
{status === 'unauthenticated' && (
|
||||||
|
<form className="admin-auth-form" onSubmit={handleLogin}>
|
||||||
|
<label>
|
||||||
|
Password
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{errorMsg && <p className="admin-auth-error">{errorMsg}</p>}
|
||||||
|
<button type="submit" className="btn-primary" disabled={submitting}>
|
||||||
|
{submitting ? 'Signing In…' : 'Sign In'}
|
||||||
|
</button>
|
||||||
|
<Link to="/" className="btn-secondary">Back to Site</Link>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function QuestionsPage() {
|
||||||
|
return (
|
||||||
|
<main className="thanks-page" aria-label="Questions and Answers">
|
||||||
|
<div className="thanks-card" style={{ maxWidth: '1000px', width: '100%' }}>
|
||||||
|
<QASection />
|
||||||
|
<div style={{ marginTop: '2rem', textAlign: 'center' }}>
|
||||||
|
<Link to="/" className="btn-secondary">Back to Site</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [content, setContent] = useState<SiteContent>(DEFAULTS)
|
const [content, setContent] = useState<SiteContent>(DEFAULTS)
|
||||||
|
|
||||||
@@ -395,7 +1203,37 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<LandingPage content={content} />} />
|
<Route path="/" element={<LandingPage content={content} />} />
|
||||||
<Route path="/admin" element={<AdminPage content={content} onSave={setContent} />} />
|
<Route path="/questions" element={<QuestionsPage />} />
|
||||||
|
<Route path="/thanks" element={<ThankYouPage />} />
|
||||||
|
<Route path="/subscribe" element={<SubscribePage />} />
|
||||||
|
<Route path="/subscribe/thanks" element={<SubscribeThankYouPage />} />
|
||||||
|
<Route path="/admin" element={<AdminShell content={content} onSave={setContent} />} />
|
||||||
|
<Route
|
||||||
|
path="/privacy"
|
||||||
|
element={(
|
||||||
|
<LegalPage
|
||||||
|
title="Privacy Policy"
|
||||||
|
body={[
|
||||||
|
'We respect your privacy and collect limited data to operate and improve this site.',
|
||||||
|
'If you consent to analytics cookies, we may store masked IP-based location signals and returning visitor activity.',
|
||||||
|
'Contact form details are used only to respond to your message and ministry communication requests.',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/terms"
|
||||||
|
element={(
|
||||||
|
<LegalPage
|
||||||
|
title="Terms"
|
||||||
|
body={[
|
||||||
|
'Content on this site is for informational and ministry purposes.',
|
||||||
|
'External links are provided for convenience and are subject to third-party policies.',
|
||||||
|
'By using this site, you agree to lawful use and respectful communication.',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user