Fix Scripture Index doubled/unparseable refs, Titus passage fetch 400, Ko-fi QR 404, auto-load study comments; stop probe URLs inflating Real Hits, real 404 status for unknown URLs; v2.43.1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-15 11:30:05 -04:00
1 parent 09177f47ed
commit 6a5d525d6e
10 files changed
+59 -10

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "siteforge", "name": "siteforge",
"version": "2.42.1", "version": "2.43.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "siteforge", "name": "siteforge",
"version": "2.42.1", "version": "2.43.1",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.1135.0", "@aws-sdk/client-s3": "^3.1135.0",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "siteforge", "name": "siteforge",
"private": true, "private": true,
"version": "2.43.0", "version": "2.43.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+4 -2
View File
@@ -39,6 +39,7 @@ import {
sanitizeUserAgent, sanitizeUserAgent,
shouldCountHit, shouldCountHit,
recordHit, recordHit,
isKnownSitePath,
scheduleStudyReminders, scheduleStudyReminders,
scheduleReengagementEmails, scheduleReengagementEmails,
scheduleWeeklyDigestEmails, scheduleWeeklyDigestEmails,
@@ -143,8 +144,9 @@ registerSalvation(app)
app.use((req, res, next) => { app.use((req, res, next) => {
if (shouldCountHit(req)) { if (shouldCountHit(req)) {
const ua = sanitizeUserAgent(req.get('user-agent')) const ua = sanitizeUserAgent(req.get('user-agent'))
const botDetection = detectBot(ua) const knownPath = isKnownSitePath(req.path)
recordHit(req.path, botDetection.isBot, botDetection.reason) const botDetection = knownPath ? detectBot(ua) : { isBot: true, reason: 'unknown-path' }
recordHit(knownPath ? req.path : '(unknown)', botDetection.isBot, botDetection.reason)
queueHitStatsWrite() queueHitStatsWrite()
if (hasVisitorConsent(req) && !botDetection.isBot && !isValidAdminSession(req)) { if (hasVisitorConsent(req) && !botDetection.isBot && !isValidAdminSession(req)) {
try { try {
+2
View File
@@ -20,6 +20,7 @@ import {
buildLastNDaysStats, buildLastNDaysStats,
shouldCountHit, shouldCountHit,
recordHit, recordHit,
isKnownSitePath,
getStudyCatalog, getStudyCatalog,
} from '../study-helpers.js' } from '../study-helpers.js'
@@ -260,6 +261,7 @@ export function register(app) {
const ip = getClientIp(req) const ip = getClientIp(req)
if (!pageviewAllowed(ip)) { res.json({ ok: false, reason: 'rate-limited' }); return } if (!pageviewAllowed(ip)) { res.json({ ok: false, reason: 'rate-limited' }); return }
const rawPath = typeof req.body?.path === 'string' ? req.body.path : '/' const rawPath = typeof req.body?.path === 'string' ? req.body.path : '/'
if (!isKnownSitePath(rawPath)) { res.json({ ok: false, reason: 'unknown-path' }); return }
const rawReferrer = typeof req.body?.referrer === 'string' ? req.body.referrer : '' const rawReferrer = typeof req.body?.referrer === 'string' ? req.body.referrer : ''
recordHit(rawPath, false) recordHit(rawPath, false)
queueHitStatsWrite() queueHitStatsWrite()
+6 -1
View File
@@ -29,7 +29,12 @@ BIBLE_BOOKS.forEach((b, index) => {
}) })
function parseReference(reference) { function parseReference(reference) {
const stripped = reference.replace(/^[a-z]+/i, '') // Accept a leading book name ("Titus 1:1-2", "1 John 2:3"), en/em dashes and verse-part letters ("1:13a")
const stripped = reference
.replace(/[–—]/g, '-')
.replace(/(\d)[a-z]\b/gi, '$1')
.replace(/^\s*(?:[1-3]\s*)?[a-z][a-z .]*?\s*(?=\d+:)/i, '')
.trim()
const match = stripped.match(/^(\d+):(\d+)(?:-(\d+))?$/) const match = stripped.match(/^(\d+):(\d+)(?:-(\d+))?$/)
if (!match) return null if (!match) return null
return { return {
+5 -2
View File
@@ -16,7 +16,7 @@ import {
} from '../config.js' } from '../config.js'
import { state } from '../state.js' import { state } from '../state.js'
import { loadSiteContentFile } from '../data.js' import { loadSiteContentFile } from '../data.js'
import { sanitizeRedirectRules } from '../study-helpers.js' import { sanitizeRedirectRules, isKnownSitePath } from '../study-helpers.js'
import { fetchAllEpisodes } from './episodes.js' import { fetchAllEpisodes } from './episodes.js'
import { fetchNuggetsEpisodes } from './nuggets.js' import { fetchNuggetsEpisodes } from './nuggets.js'
import { BIBLE_BOOKS } from '../../shared/bible-books.js' import { BIBLE_BOOKS } from '../../shared/bible-books.js'
@@ -217,7 +217,8 @@ export function register(app) {
if (!sec.id || !sec.reference || !sec.releasedAt) continue if (!sec.id || !sec.reference || !sec.releasedAt) continue
if (new Date(sec.releasedAt) > cutoff) continue if (new Date(sec.releasedAt) > cutoff) continue
entries.push({ entries.push({
reference: `${bookName} ${sec.reference}`, // Section references often already include the book ("Titus 1:1-2"); only prepend when they don't
reference: /^\s*(?:[1-3]\s*)?[A-Za-z]/.test(sec.reference) ? sec.reference.trim() : `${bookName} ${sec.reference}`,
sourceType: 'study', sourceType: 'study',
sourceTitle: s.title || s.slug, sourceTitle: s.title || s.slug,
sectionTitle: sec.title || '', sectionTitle: sec.title || '',
@@ -274,6 +275,8 @@ export function register(app) {
app.use(async (_req, res) => { app.use(async (_req, res) => {
try { try {
const html = await readFile(INDEX_FILE, 'utf8') const html = await readFile(INDEX_FILE, 'utf8')
// Unknown URLs still render the app's "Page not found" screen, but with a real 404 status
if (!isKnownSitePath(_req.path)) res.status(404)
res.type('html').send(injectSeoIntoHtml(html, state.cachedSiteContent)) res.type('html').send(injectSeoIntoHtml(html, state.cachedSiteContent))
} catch { } catch {
res.status(503).send('Frontend build not found. Run "npm run build" first.') res.status(503).send('Frontend build not found. Run "npm run build" first.')
+13
View File
@@ -476,6 +476,19 @@ export function normalizeHitPath(pathname) {
return pathname return pathname
} }
// Top-level URL segments the SPA / static pages actually serve. Anything else is a probe or
// scanner hit, which must not count as a real visit or add a new key to the per-path stats.
const KNOWN_SITE_SEGMENTS = new Set([
'start-here', 'study', 'finished', 'episodes', 'nuggets', 'resources', 'downloads', 'about', 'contact',
'questions', 'newsletter', 'scripture', 'thanks', 'subscribe', 'certificate', 'host', 'admin', 'preview',
'privacy', 'terms', 'salvation', 'qr',
])
export function isKnownSitePath(pathname) {
const first = String(pathname ?? '/').split('/')[1] ?? ''
return first === '' || KNOWN_SITE_SEGMENTS.has(first)
}
export function shouldCountHit(req) { export function shouldCountHit(req) {
if (req.method !== 'GET') return false if (req.method !== 'GET') return false
if (req.path.startsWith('/api/')) return false if (req.path.startsWith('/api/')) return false
+7
View File
@@ -2422,6 +2422,13 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
const [commentsLoaded, setCommentsLoaded] = useState(false) const [commentsLoaded, setCommentsLoaded] = useState(false)
const [commentFilter, setCommentFilter] = useState<'all' | 'pending' | 'approved'>('all') const [commentFilter, setCommentFilter] = useState<'all' | 'pending' | 'approved'>('all')
const [commentSearch, setCommentSearch] = useState('') const [commentSearch, setCommentSearch] = useState('')
// Load moderation queue as soon as the section is opened; a failed load leaves the manual button available
const commentsAutoTried = useRef(false)
useEffect(() => {
if (adminView !== 'study-comments' || commentsLoaded || commentsAutoTried.current) return
commentsAutoTried.current = true
void loadStudyComments()
}, [adminView, commentsLoaded])
// Episode scripts state // Episode scripts state
const [scriptsList, setScriptsList] = useState<Array<{ key: string; episodeNumber: string; title: string; filename: string; season: string | null; uploadedAt: string | null; wordCount: number }>>([]) const [scriptsList, setScriptsList] = useState<Array<{ key: string; episodeNumber: string; title: string; filename: string; season: string | null; uploadedAt: string | null; wordCount: number }>>([])
+4 -2
View File
@@ -662,7 +662,7 @@ function KofiSupportSection({ content }: { content: SiteContent }) {
</div> </div>
{qrUrl && ( {qrUrl && (
<figure className="cx-support-qr"> <figure className="cx-support-qr">
<img src={qrUrl} alt="Ko-fi donation QR code" /> <img src={qrUrl} alt="Ko-fi donation QR code" onError={e => { const fig = e.currentTarget.parentElement; if (fig) fig.style.display = 'none' }} />
<figcaption>Scan to give on Ko-fi</figcaption> <figcaption>Scan to give on Ko-fi</figcaption>
</figure> </figure>
)} )}
@@ -2655,7 +2655,9 @@ for (const b of BIBLE_BOOKS) {
function parseSIRef(ref: string) { function parseSIRef(ref: string) {
// Supports same-chapter ranges ("5:15-20") and chapter-spanning ranges ("5:15-6:2"). // Supports same-chapter ranges ("5:15-20") and chapter-spanning ranges ("5:15-6:2").
const m = ref.match(/^(.*?)\s+(\d+):(\d+)(?:-(?:(\d+):)?(\d+))?$/) // Tolerate en/em dashes and verse-part suffixes like "10-12a"
const clean = ref.replace(/[\u2013\u2014]/g, '-').replace(/(\d)[a-z]\b/gi, '$1').trim()
const m = clean.match(/^(.*?)\s+(\d+):(\d+)(?:-(?:(\d+):)?(\d+))?$/)
if (!m) return null if (!m) return null
const bookId = SI_BOOK_MAP[m[1].toLowerCase().replace(/\.\s*/g, ' ').trim()] const bookId = SI_BOOK_MAP[m[1].toLowerCase().replace(/\.\s*/g, ' ').trim()]
if (!bookId) return null if (!bookId) return null
+15
View File
@@ -0,0 +1,15 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { isKnownSitePath } from '../server/study-helpers.js'
test('real site pages are known paths', () => {
for (const p of ['/', '/study', '/study/titus/titus-abc', '/episodes/123', '/salvation', '/scripture', '/privacy']) {
assert.equal(isKnownSitePath(p), true, p)
}
})
test('scanner and probe paths are not known paths', () => {
for (const p of ['/.env', '/.git/config', '/wp-login', '/actuator/health', '/cgi-bin/x', '/random-path-123']) {
assert.equal(isKnownSitePath(p), false, p)
}
})