Add Nuggets to global search and RSS-sourced scripture refs to index; v2.0.26
Publish Container / docker (push) Successful in 15m20s
Publish Container / docker (push) Successful in 15m20s
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
590885f6b1
commit
1ddb8ee7be
5 files changed
+77
-5
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.25",
|
"version": "2.0.26",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ function feedUrl() {
|
|||||||
return state.cachedSiteContent?.nuggets?.rssFeedUrl?.trim() ?? ''
|
return state.cachedSiteContent?.nuggets?.rssFeedUrl?.trim() ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchNuggetsEpisodes() {
|
export async function fetchNuggetsEpisodes() {
|
||||||
const url = feedUrl()
|
const url = feedUrl()
|
||||||
if (!url) { cache = null; return [] }
|
if (!url) { cache = null; return [] }
|
||||||
|
|
||||||
|
|||||||
+54
-1
@@ -16,6 +16,33 @@ import { state } from '../state.js'
|
|||||||
import { loadSiteContentFile } from '../data.js'
|
import { loadSiteContentFile } from '../data.js'
|
||||||
import { DATA_FILE } from '../config.js'
|
import { DATA_FILE } from '../config.js'
|
||||||
import { sanitizeRedirectRules } from '../study-helpers.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) {
|
export function register(app) {
|
||||||
const SALVATION_INDEX_FILE = path.join(DIST_DIR, 'salvation', 'index.html')
|
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
|
// 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 content = state.cachedSiteContent
|
||||||
const allStudies = Array.isArray(content?.studies) ? content.studies : []
|
const allStudies = Array.isArray(content?.studies) ? content.studies : []
|
||||||
const allEpisodes = Array.isArray(content?.podcastFeaturedLinks) ? content.podcastFeaturedLinks : []
|
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 })
|
res.json({ entries })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function parseRssItems(xml, limit = Infinity) {
|
|||||||
items.push({
|
items.push({
|
||||||
title, pubDate, link,
|
title, pubDate, link,
|
||||||
audioUrl: enclosureUrl,
|
audioUrl: enclosureUrl,
|
||||||
description: descText.slice(0, 220) + (descText.length > 220 ? '…' : ''),
|
description: descText.slice(0, 800) + (descText.length > 800 ? '…' : ''),
|
||||||
duration, episode, season,
|
duration, episode, season,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export function useGlobalSearch(content: SiteContent) {
|
|||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [questions, setQuestions] = useState<PublicQuestion[]>([])
|
const [questions, setQuestions] = useState<PublicQuestion[]>([])
|
||||||
const [episodeItems, setEpisodeItems] = useState<SearchResult[]>([])
|
const [episodeItems, setEpisodeItems] = useState<SearchResult[]>([])
|
||||||
|
const [nuggetsItems, setNuggetsItems] = useState<SearchResult[]>([])
|
||||||
const [scriptResults, setScriptResults] = useState<SearchResult[]>([])
|
const [scriptResults, setScriptResults] = useState<SearchResult[]>([])
|
||||||
const scriptDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const scriptDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
@@ -75,6 +76,24 @@ export function useGlobalSearch(content: SiteContent) {
|
|||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}, [content.podcastFeaturedLinks])
|
}, [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
|
// Debounced server-side script search — runs whenever query changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scriptDebounceRef.current) clearTimeout(scriptDebounceRef.current)
|
if (scriptDebounceRef.current) clearTimeout(scriptDebounceRef.current)
|
||||||
@@ -132,7 +151,7 @@ export function useGlobalSearch(content: SiteContent) {
|
|||||||
return results
|
return results
|
||||||
}, [content, questions])
|
}, [content, questions])
|
||||||
|
|
||||||
const allFuseItems = useMemo(() => [...episodeItems, ...staticItems], [episodeItems, staticItems])
|
const allFuseItems = useMemo(() => [...episodeItems, ...nuggetsItems, ...staticItems], [episodeItems, nuggetsItems, staticItems])
|
||||||
|
|
||||||
const fuse = useMemo(() => new Fuse(allFuseItems, {
|
const fuse = useMemo(() => new Fuse(allFuseItems, {
|
||||||
keys: [
|
keys: [
|
||||||
|
|||||||
Reference in new issue
Block a user