Files
Siteforge/server/routes/nuggets.js
T
nmemmertandClaude Sonnet 5.5 5d5c026762
Publish Container / build (push) Successful in 16m20s
Redo /newsletter and /nuggets pages; add Nuggets episode tags with admin editor; v2.37.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 13:49:15 -04:00

100 lines
3.3 KiB
JavaScript

import { parseRssItems } from '../rss-helpers.js'
import { state } from '../state.js'
import { requireAdminAuth } from '../auth.js'
import { queueNuggetsTagsWrite } from '../data.js'
let cache = null // { url, episodes }
let cacheAt = 0
const CACHE_TTL = 30 * 60 * 1000
function feedUrl() {
return state.cachedSiteContent?.nuggets?.rssFeedUrl?.trim() ?? ''
}
export async function fetchNuggetsEpisodes() {
const url = feedUrl()
if (!url) { cache = null; return [] }
const now = Date.now()
if (cache && cache.url === url && (now - cacheAt) < CACHE_TTL) {
return cache.episodes
}
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 8000)
try {
const response = await fetch(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)
cache = { url, episodes }
cacheAt = now
return episodes
} catch (err) {
clearTimeout(timeout)
// Serve stale cache for this same URL rather than a hard failure, if we have one.
if (cache && cache.url === url) return cache.episodes
throw err
}
}
function withTags(episodes) {
return episodes.map(ep => ({ ...ep, tags: state.nuggetsEpisodeTags[ep.audioUrl] ?? [] }))
}
function cleanTags(input) {
const list = Array.isArray(input) ? input : []
const seen = new Set()
const out = []
for (const t of list) {
const tag = String(t ?? '').trim().replace(/\s+/g, ' ').slice(0, 40)
const key = tag.toLowerCase()
if (tag && !seen.has(key)) { seen.add(key); out.push(tag) }
}
return out.slice(0, 12)
}
export function register(app) {
app.get('/api/admin/nuggets/episode-tags', requireAdminAuth, async (_req, res) => {
try {
const episodes = await fetchNuggetsEpisodes()
res.json({ episodes: withTags(episodes).map(({ title, pubDate, audioUrl, episode, tags }) => ({ title, pubDate, audioUrl, episode, tags })) })
} catch (err) {
res.status(502).json({ message: 'Could not load episodes from the feed.' })
}
})
app.put('/api/admin/nuggets/episode-tags', requireAdminAuth, (req, res) => {
const audioUrl = String(req.body?.audioUrl ?? '')
if (!audioUrl) { res.status(400).json({ message: 'audioUrl is required.' }); return }
const tags = cleanTags(req.body?.tags)
if (tags.length) state.nuggetsEpisodeTags[audioUrl] = tags
else delete state.nuggetsEpisodeTags[audioUrl]
queueNuggetsTagsWrite()
res.json({ ok: true, tags })
})
app.get('/api/nuggets/episodes', async (_req, res) => {
const configured = Boolean(feedUrl())
try {
const episodes = await fetchNuggetsEpisodes()
res.json({ episodes: withTags(episodes.slice(0, 6)), configured })
} catch (err) {
console.error('[nuggets/episodes] RSS fetch error:', err.message)
res.json({ episodes: [], configured })
}
})
app.get('/api/nuggets/episodes/all', async (_req, res) => {
const configured = Boolean(feedUrl())
try {
const episodes = await fetchNuggetsEpisodes()
res.json({ episodes: withTags(episodes), configured })
} catch (err) {
console.error('[nuggets/episodes/all] RSS fetch error:', err.message)
res.json({ episodes: [], configured })
}
})
}