From 9e602e29eccd6e7f3f31e854ddd22275de7da30c Mon Sep 17 00:00:00 2001 From: nmemmert Date: Mon, 10 Aug 2026 10:12:25 -0400 Subject: [PATCH] Fetch BSB passage text from Bolls.life API in study lesson view; v1.1.35 Adds /api/bible-passage server route that proxies the Berean Standard Bible from Bolls.life per-verse, with in-memory caching. Study lesson scripture block now displays live BSB text instead of static passageText. Adds bibleBook field to StudyProgram so admins can set the book name explicitly when the study slug doesn't match (e.g. a study titled "ephesians-part-2"). Co-Authored-By: Claude Sonnet 4.6 --- data/analytics-events.json | 5 ++- data/study-reminders.json | 2 +- package.json | 2 +- server.js | 2 + server/routes/bible-passage.js | 76 ++++++++++++++++++++++++++++++++++ src/AdminPage.tsx | 5 +++ src/colossiansStudy.tsx | 29 ++++++++++++- src/content.ts | 1 + 8 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 server/routes/bible-passage.js diff --git a/data/analytics-events.json b/data/analytics-events.json index 6167972..bdd0bde 100644 --- a/data/analytics-events.json +++ b/data/analytics-events.json @@ -9,8 +9,8 @@ }, "timeOnPage": { "/": { - "totalSeconds": 9, - "count": 2 + "totalSeconds": 51, + "count": 4 }, "/episodes": { "totalSeconds": 318967, @@ -22,6 +22,7 @@ } }, "outboundClicks": {}, + "linkClicks": {}, "utmSources": {}, "searchQueries": {}, "notFound": {}, diff --git a/data/study-reminders.json b/data/study-reminders.json index c23b4fa..02e42e8 100644 --- a/data/study-reminders.json +++ b/data/study-reminders.json @@ -1,4 +1,4 @@ { "users": {}, - "updatedAt": "2026-08-10T12:37:17.805Z" + "updatedAt": "2026-08-10T14:07:00.476Z" } \ No newline at end of file diff --git a/package.json b/package.json index cb05770..dd6b1b0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "1.1.34", + "version": "1.1.35", "type": "module", "scripts": { "dev": "vite", diff --git a/server.js b/server.js index 04cfa7a..2f659f7 100644 --- a/server.js +++ b/server.js @@ -63,6 +63,7 @@ import { register as registerDownloads } from './server/routes/downloads.js' import { register as registerEpisodes } from './server/routes/episodes.js' import { register as registerFeed } from './server/routes/feed.js' import { register as registerQrCodes } from './server/routes/qr-codes.js' +import { register as registerBiblePassage } from './server/routes/bible-passage.js' import { register as registerPublic } from './server/routes/public.js' const app = express() @@ -110,6 +111,7 @@ registerDownloads(app) registerEpisodes(app) registerFeed(app) registerQrCodes(app) +registerBiblePassage(app) // Hit-counting middleware (must come before public routes) app.use((req, res, next) => { diff --git a/server/routes/bible-passage.js b/server/routes/bible-passage.js new file mode 100644 index 0000000..b200673 --- /dev/null +++ b/server/routes/bible-passage.js @@ -0,0 +1,76 @@ +const passageCache = new Map() + +const BOLLS_BOOK_MAP = { + genesis: 1, exodus: 2, leviticus: 3, numbers: 4, deuteronomy: 5, + joshua: 6, judges: 7, ruth: 8, '1samuel': 9, '2samuel': 10, + '1kings': 11, '2kings': 12, '1chronicles': 13, '2chronicles': 14, + ezra: 15, nehemiah: 16, esther: 17, job: 18, psalms: 19, proverbs: 20, + ecclesiastes: 21, songofsolomon: 22, isaiah: 23, jeremiah: 24, + lamentations: 25, ezekiel: 26, daniel: 27, hosea: 28, joel: 29, + amos: 30, obadiah: 31, jonah: 32, micah: 33, nahum: 34, habakkuk: 35, + zephaniah: 36, haggai: 37, zechariah: 38, malachi: 39, + matthew: 40, mark: 41, luke: 42, john: 43, acts: 44, + romans: 45, '1corinthians': 46, '2corinthians': 47, galatians: 48, + ephesians: 49, philippians: 50, colossians: 51, '1thessalonians': 52, + '2thessalonians': 53, '1timothy': 54, '2timothy': 55, titus: 56, + philemon: 57, hebrews: 58, james: 59, '1peter': 60, '2peter': 61, + '1john': 62, '2john': 63, '3john': 64, jude: 65, revelation: 66, +} + +function parseReference(reference) { + const match = reference.match(/^(\d+):(\d+)(?:-(\d+))?$/) + if (!match) return null + return { + chapter: parseInt(match[1], 10), + verseStart: parseInt(match[2], 10), + verseEnd: match[3] ? parseInt(match[3], 10) : parseInt(match[2], 10), + } +} + +export function register(app) { + app.get('/api/bible-passage', async (req, res) => { + const book = String(req.query.book ?? '').trim().toLowerCase().replace(/[\s-]/g, '') + const reference = String(req.query.reference ?? '').trim() + + const bookNum = BOLLS_BOOK_MAP[book] + if (!bookNum) { + res.status(400).json({ message: 'Unknown book.' }) + return + } + + const parsed = parseReference(reference) + if (!parsed) { + res.status(400).json({ message: 'Invalid reference format. Expected format: chapter:verseStart-verseEnd' }) + return + } + + const cacheKey = `BSB:${bookNum}:${parsed.chapter}:${parsed.verseStart}:${parsed.verseEnd}` + if (passageCache.has(cacheKey)) { + res.json({ text: passageCache.get(cacheKey) }) + return + } + + try { + const verseNums = [] + for (let v = parsed.verseStart; v <= parsed.verseEnd; v++) verseNums.push(v) + + const results = await Promise.all( + verseNums.map(v => + fetch(`https://bolls.life/get-verse/BSB/${bookNum}/${parsed.chapter}/${v}/`, { signal: AbortSignal.timeout(8000) }) + .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))) + ) + ) + + if (results.length === 0) { + res.status(404).json({ message: 'Passage not found.' }) + return + } + + const text = results.map(v => `v. ${v.verse} ${String(v.text).trim()}`).join('\n\n') + passageCache.set(cacheKey, text) + res.json({ text }) + } catch { + res.status(502).json({ message: 'Unable to fetch passage right now.' }) + } + }) +} diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index a7087b7..ea57ce9 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -4285,6 +4285,11 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { updateStudyProgram(study.id, 'slug', e.target.value.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-'))} /> +
+ + updateStudyProgram(study.id, 'bibleBook', e.target.value.trim().toLowerCase().replace(/\s+/g, ''))} /> +

Lowercase book name used to fetch BSB passage text. E.g. colossians, ephesians, 1corinthians. Leave blank if the slug matches the book name.

+