Refine homepage sections and contact card design
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user