import express from 'express'
import path from 'node:path'
import { readFile } from 'node:fs/promises'
import { escapeHtml, escapeXml, injectSeoIntoHtml, normalizeSitemapPath } from '../helpers.js'
import {
DIST_DIR,
DIST_IMAGES_DIR,
PUBLIC_IMAGES_DIR,
UPLOADS_DIR,
INDEX_FILE,
DEFAULT_SEO,
APP_VERSION,
GIT_COMMIT,
DATA_FILE,
} from '../config.js'
import { state } from '../state.js'
import { loadSiteContentFile } from '../data.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')
app.get('/api/version', (_req, res) => {
res.json({ version: APP_VERSION, commit: GIT_COMMIT })
})
app.get('/robots.txt', async (_req, res) => {
let content = state.cachedSiteContent
if (!content) {
try {
const parsed = await loadSiteContentFile(DATA_FILE)
content = parsed.siteContent
} catch { content = {} }
}
const seo = content?.seo ?? DEFAULT_SEO
const canonical = seo.canonicalUrl || DEFAULT_SEO.canonicalUrl
const root = canonical.endsWith('/') ? canonical.slice(0, -1) : canonical
res.type('text/plain').send(
['User-agent: *', 'Allow: /', 'Disallow: /salvation', `Sitemap: ${root}/sitemap.xml`].join('\n'),
)
})
app.get('/sitemap.xml', async (_req, res) => {
let content = state.cachedSiteContent
if (!content) {
try {
const parsed = await loadSiteContentFile(DATA_FILE)
content = parsed.siteContent
} catch { content = {} }
}
const seo = content?.seo ?? DEFAULT_SEO
const canonical = seo.canonicalUrl || DEFAULT_SEO.canonicalUrl
const root = canonical.endsWith('/') ? canonical.slice(0, -1) : canonical
const paths = Array.isArray(seo.sitemapPaths) && seo.sitemapPaths.length > 0
? seo.sitemapPaths
: DEFAULT_SEO.sitemapPaths
const urls = paths
.map(item => normalizeSitemapPath(item))
.filter(Boolean)
.map(item => `${root}${item}`)
// Dynamic study lesson URLs
const studies = Array.isArray(state.cachedSiteContent?.studies) ? state.cachedSiteContent.studies : []
const now = new Date()
for (const study of studies) {
if (!study.slug || !Array.isArray(study.sections)) continue
for (const section of study.sections) {
if (section.id && section.releasedAt && new Date(section.releasedAt) <= now) {
urls.push(`${root}/study/${encodeURIComponent(study.slug)}/${encodeURIComponent(section.id)}`)
}
}
}
// Dynamic episode highlight URLs
const highlights = [
...(Array.isArray(state.cachedSiteContent?.podcastFeaturedLinks) ? state.cachedSiteContent.podcastFeaturedLinks : []),
...(Array.isArray(state.cachedSiteContent?.finishedBooks) ? state.cachedSiteContent.finishedBooks.flatMap(book => (Array.isArray(book?.highlights) ? book.highlights : [])) : []),
]
for (const ep of highlights) {
if (ep.id) urls.push(`${root}/episodes/${encodeURIComponent(ep.id)}`)
}
// Newsletter archive + published issues
urls.push(`${root}/newsletter`)
for (const issue of state.newsletterIssues) {
if (issue.slug && issue.public !== false) urls.push(`${root}/newsletter/${encodeURIComponent(issue.slug)}`)
}
const xml = [
'',
'