From 1ddb8ee7be82072fe44f75602ac776a2dc8b718a Mon Sep 17 00:00:00 2001 From: nmemmert Date: Thu, 17 Sep 2026 10:27:12 -0400 Subject: [PATCH] Add Nuggets to global search and RSS-sourced scripture refs to index; v2.0.26 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Raise RSS description truncation from 220 → 800 chars so scripture references in episode descriptions are preserved - Global search now indexes Nuggets podcast episodes from /api/nuggets/episodes/all (title, subtitle, description via Fuse) - Scripture Index (/api/public/scripture-index) now extracts verse references from both main podcast and Nuggets RSS descriptions using a regex built from the shared BIBLE_BOOKS list; duplicate refs across sources are merged by the existing client-side dedup logic - Export fetchNuggetsEpisodes from nuggets.js for reuse Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- server/routes/nuggets.js | 2 +- server/routes/public.js | 55 +++++++++++++++++++++++++++++++++++- server/rss-helpers.js | 2 +- src/hooks/useGlobalSearch.ts | 21 +++++++++++++- 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index dd72867..b61f840 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.0.25", + "version": "2.0.26", "type": "module", "scripts": { "dev": "vite", diff --git a/server/routes/nuggets.js b/server/routes/nuggets.js index bfa1107..67a4a0f 100644 --- a/server/routes/nuggets.js +++ b/server/routes/nuggets.js @@ -9,7 +9,7 @@ function feedUrl() { return state.cachedSiteContent?.nuggets?.rssFeedUrl?.trim() ?? '' } -async function fetchNuggetsEpisodes() { +export async function fetchNuggetsEpisodes() { const url = feedUrl() if (!url) { cache = null; return [] } diff --git a/server/routes/public.js b/server/routes/public.js index 6e547dd..47568ad 100644 --- a/server/routes/public.js +++ b/server/routes/public.js @@ -16,6 +16,33 @@ import { state } from '../state.js' import { loadSiteContentFile } from '../data.js' import { DATA_FILE } from '../config.js' import { sanitizeRedirectRules } from '../study-helpers.js' +import { fetchAllEpisodes } from './episodes.js' +import { fetchNuggetsEpisodes } from './nuggets.js' +import { BIBLE_BOOKS } from '../../shared/bible-books.js' + +// Build a regex that matches scripture references like "Titus 1:5-9" or "1 Cor 13:4-7" +// out of plain text. Sorted longest-first so "1 Corinthians" beats "1 Cor". +const _refPatterns = [] +for (const b of BIBLE_BOOKS) { + _refPatterns.push(b.name, ...(b.aliases ?? [])) +} +_refPatterns.sort((a, z) => z.length - a.length) +const _bookPart = _refPatterns.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') +const SCRIPTURE_REF_RE = new RegExp( + `(${_bookPart})\\s+(\\d+):(\\d+)(?:-((?:\\d+):)?(\\d+))?`, + 'gi', +) + +function extractScriptureRefs(text) { + const refs = [] + const seen = new Set() + for (const m of String(text || '').matchAll(SCRIPTURE_REF_RE)) { + const ref = m[0].trim() + const key = ref.toLowerCase() + if (!seen.has(key)) { seen.add(key); refs.push(ref) } + } + return refs +} export function register(app) { const SALVATION_INDEX_FILE = path.join(DIST_DIR, 'salvation', 'index.html') @@ -160,7 +187,7 @@ export function register(app) { }) // Scripture index — every passage covered across released study lessons and podcast episodes - app.get('/api/public/scripture-index', (_req, res) => { + app.get('/api/public/scripture-index', async (_req, res) => { const content = state.cachedSiteContent const allStudies = Array.isArray(content?.studies) ? content.studies : [] const allEpisodes = Array.isArray(content?.podcastFeaturedLinks) ? content.podcastFeaturedLinks : [] @@ -196,6 +223,32 @@ export function register(app) { } } + // Extract scripture refs from main podcast episode descriptions + try { + const mainEps = await fetchAllEpisodes() + for (const ep of mainEps) { + if (!ep.description) continue + const refs = extractScriptureRefs(ep.description) + for (const ref of refs) { + const title = ep.episode ? `Episode ${ep.episode}: ${ep.title}` : ep.title + entries.push({ reference: ref, sourceType: 'episode', sourceTitle: title, url: ep.link || '/episodes' }) + } + } + } catch { /* keep going if RSS is down */ } + + // Extract scripture refs from Nuggets episode descriptions + try { + const nuggetsEps = await fetchNuggetsEpisodes() + for (const ep of nuggetsEps) { + if (!ep.description) continue + const refs = extractScriptureRefs(ep.description) + for (const ref of refs) { + const title = ep.episode ? `Nuggets Ep. ${ep.episode}: ${ep.title}` : ep.title + entries.push({ reference: ref, sourceType: 'episode', sourceTitle: title, url: '/nuggets' }) + } + } + } catch { /* keep going if RSS is down */ } + res.json({ entries }) }) diff --git a/server/rss-helpers.js b/server/rss-helpers.js index eb34868..79261f3 100644 --- a/server/rss-helpers.js +++ b/server/rss-helpers.js @@ -42,7 +42,7 @@ export function parseRssItems(xml, limit = Infinity) { items.push({ title, pubDate, link, audioUrl: enclosureUrl, - description: descText.slice(0, 220) + (descText.length > 220 ? '…' : ''), + description: descText.slice(0, 800) + (descText.length > 800 ? '…' : ''), duration, episode, season, }) } diff --git a/src/hooks/useGlobalSearch.ts b/src/hooks/useGlobalSearch.ts index 4dd04e1..aa4290a 100644 --- a/src/hooks/useGlobalSearch.ts +++ b/src/hooks/useGlobalSearch.ts @@ -35,6 +35,7 @@ export function useGlobalSearch(content: SiteContent) { const [query, setQuery] = useState('') const [questions, setQuestions] = useState([]) const [episodeItems, setEpisodeItems] = useState([]) + const [nuggetsItems, setNuggetsItems] = useState([]) const [scriptResults, setScriptResults] = useState([]) const scriptDebounceRef = useRef | null>(null) @@ -75,6 +76,24 @@ export function useGlobalSearch(content: SiteContent) { .catch(() => {}) }, [content.podcastFeaturedLinks]) + // Fetch nuggets episodes once on mount + useEffect(() => { + fetch('/api/nuggets/episodes/all') + .then(r => r.ok ? r.json() : Promise.reject()) + .then((data: { episodes?: Array<{ title: string; link: string; description?: string; episode?: string }> }) => { + const eps: SearchResult[] = (data.episodes ?? []).map(ep => ({ + id: `nuggets-${ep.episode ?? ep.title}`, + type: 'episode' as const, + title: ep.title, + subtitle: ep.episode ? `Nuggets Ep. ${ep.episode}` : 'Nuggets', + description: ep.description, + href: '/nuggets', + })) + setNuggetsItems(eps) + }) + .catch(() => {}) + }, []) + // Debounced server-side script search — runs whenever query changes useEffect(() => { if (scriptDebounceRef.current) clearTimeout(scriptDebounceRef.current) @@ -132,7 +151,7 @@ export function useGlobalSearch(content: SiteContent) { return results }, [content, questions]) - const allFuseItems = useMemo(() => [...episodeItems, ...staticItems], [episodeItems, staticItems]) + const allFuseItems = useMemo(() => [...episodeItems, ...nuggetsItems, ...staticItems], [episodeItems, nuggetsItems, staticItems]) const fuse = useMemo(() => new Fuse(allFuseItems, { keys: [