Refine homepage sections and contact card design

This commit is contained in:
nmemmert
2026-04-20 15:38:55 -04:00
parent cc4a4b4d63
commit 8a71aecb4e
4 changed files with 450 additions and 58 deletions
+64
View File
@@ -1404,6 +1404,70 @@ app.delete('/api/admin-questions/:id', requireAdminAuth, (req, res) => {
res.json({ ok: true })
})
// ── Episodes (RSS feed proxy) ──────────────────────────────────────────────
const RSS_FEED_URL = 'https://anchor.fm/nmemmert/podcast/rss'
let episodesCache = null
let episodesCacheAt = 0
const EPISODES_CACHE_TTL = 30 * 60 * 1000 // 30 minutes
function extractCdata(raw) {
const cdata = /^<!\[CDATA\[([\s\S]*?)\]\]>$/.exec(raw.trim())
return cdata ? cdata[1].trim() : raw.trim()
}
function parseRssItems(xml, limit = 6) {
const items = []
const itemRegex = /<item>([\s\S]*?)<\/item>/g
let match
while ((match = itemRegex.exec(xml)) !== null && items.length < limit) {
const block = match[1]
const titleRaw = /<title>([\s\S]*?)<\/title>/.exec(block)?.[1] ?? ''
const title = extractCdata(titleRaw)
if (!title) continue
const pubDate = (/<pubDate>([\s\S]*?)<\/pubDate>/.exec(block)?.[1] ?? '').trim()
const guidRaw = /<guid[^>]*>([\s\S]*?)<\/guid>/.exec(block)?.[1] ?? ''
const guid = extractCdata(guidRaw)
const enclosureUrl = /<enclosure[^>]+url="([^"]+)"/.exec(block)?.[1] ?? ''
const link = guid.startsWith('http') ? guid : enclosureUrl
const descRaw = /<description>([\s\S]*?)<\/description>/.exec(block)?.[1] ?? ''
const descText = extractCdata(descRaw).replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim()
const duration = (/<itunes:duration>([\s\S]*?)<\/itunes:duration>/.exec(block)?.[1] ?? '').trim()
const episode = (/<itunes:episode>([\s\S]*?)<\/itunes:episode>/.exec(block)?.[1] ?? '').trim()
items.push({
title,
pubDate,
link,
description: descText.slice(0, 220) + (descText.length > 220 ? '…' : ''),
duration,
episode,
})
}
return items
}
app.get('/api/episodes', async (_req, res) => {
const now = Date.now()
if (episodesCache && (now - episodesCacheAt) < EPISODES_CACHE_TTL) {
return res.json({ episodes: episodesCache })
}
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 8000)
const response = await fetch(RSS_FEED_URL, { signal: controller.signal })
clearTimeout(timeout)
if (!response.ok) throw new Error(`RSS fetch failed: ${response.status}`)
const xml = await response.text()
const episodes = parseRssItems(xml, 6)
episodesCache = episodes
episodesCacheAt = now
res.json({ episodes })
} catch (err) {
console.error('[episodes] RSS fetch error:', err.message)
res.json({ episodes: episodesCache ?? [] })
}
})
app.use(express.static(DIST_DIR))
app.use(async (_req, res) => {