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:
1 parent
09177f47ed
commit
6a5d525d6e
10 files changed
+59
-10
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "siteforge",
|
||||
"version": "2.42.1",
|
||||
"version": "2.43.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "siteforge",
|
||||
"version": "2.42.1",
|
||||
"version": "2.43.1",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1135.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "siteforge",
|
||||
"private": true,
|
||||
"version": "2.43.0",
|
||||
"version": "2.43.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
sanitizeUserAgent,
|
||||
shouldCountHit,
|
||||
recordHit,
|
||||
isKnownSitePath,
|
||||
scheduleStudyReminders,
|
||||
scheduleReengagementEmails,
|
||||
scheduleWeeklyDigestEmails,
|
||||
@@ -143,8 +144,9 @@ registerSalvation(app)
|
||||
app.use((req, res, next) => {
|
||||
if (shouldCountHit(req)) {
|
||||
const ua = sanitizeUserAgent(req.get('user-agent'))
|
||||
const botDetection = detectBot(ua)
|
||||
recordHit(req.path, botDetection.isBot, botDetection.reason)
|
||||
const knownPath = isKnownSitePath(req.path)
|
||||
const botDetection = knownPath ? detectBot(ua) : { isBot: true, reason: 'unknown-path' }
|
||||
recordHit(knownPath ? req.path : '(unknown)', botDetection.isBot, botDetection.reason)
|
||||
queueHitStatsWrite()
|
||||
if (hasVisitorConsent(req) && !botDetection.isBot && !isValidAdminSession(req)) {
|
||||
try {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
buildLastNDaysStats,
|
||||
shouldCountHit,
|
||||
recordHit,
|
||||
isKnownSitePath,
|
||||
getStudyCatalog,
|
||||
} from '../study-helpers.js'
|
||||
|
||||
@@ -260,6 +261,7 @@ export function register(app) {
|
||||
const ip = getClientIp(req)
|
||||
if (!pageviewAllowed(ip)) { res.json({ ok: false, reason: 'rate-limited' }); return }
|
||||
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 : ''
|
||||
recordHit(rawPath, false)
|
||||
queueHitStatsWrite()
|
||||
|
||||
@@ -29,7 +29,12 @@ BIBLE_BOOKS.forEach((b, index) => {
|
||||
})
|
||||
|
||||
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+))?$/)
|
||||
if (!match) return null
|
||||
return {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '../config.js'
|
||||
import { state } from '../state.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 { fetchNuggetsEpisodes } from './nuggets.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 (new Date(sec.releasedAt) > cutoff) continue
|
||||
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',
|
||||
sourceTitle: s.title || s.slug,
|
||||
sectionTitle: sec.title || '',
|
||||
@@ -274,6 +275,8 @@ export function register(app) {
|
||||
app.use(async (_req, res) => {
|
||||
try {
|
||||
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))
|
||||
} catch {
|
||||
res.status(503).send('Frontend build not found. Run "npm run build" first.')
|
||||
|
||||
@@ -476,6 +476,19 @@ export function normalizeHitPath(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) {
|
||||
if (req.method !== 'GET') return false
|
||||
if (req.path.startsWith('/api/')) return false
|
||||
|
||||
@@ -2422,6 +2422,13 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
const [commentsLoaded, setCommentsLoaded] = useState(false)
|
||||
const [commentFilter, setCommentFilter] = useState<'all' | 'pending' | 'approved'>('all')
|
||||
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
|
||||
const [scriptsList, setScriptsList] = useState<Array<{ key: string; episodeNumber: string; title: string; filename: string; season: string | null; uploadedAt: string | null; wordCount: number }>>([])
|
||||
|
||||
+4
-2
@@ -662,7 +662,7 @@ function KofiSupportSection({ content }: { content: SiteContent }) {
|
||||
</div>
|
||||
{qrUrl && (
|
||||
<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>
|
||||
</figure>
|
||||
)}
|
||||
@@ -2655,7 +2655,9 @@ for (const b of BIBLE_BOOKS) {
|
||||
|
||||
function parseSIRef(ref: string) {
|
||||
// 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
|
||||
const bookId = SI_BOOK_MAP[m[1].toLowerCase().replace(/\.\s*/g, ' ').trim()]
|
||||
if (!bookId) return null
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user