// useRSS โ€” fetch RSS/Atom feeds via the backend proxy and parse them // in the browser using DOMParser. Handles RSS 2.0, RSS 1.0 (RDF), Atom. import { ref } from 'vue' const BACKEND = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8082' function pickText(el, ...selectors) { for (const sel of selectors) { const node = el.querySelector(sel) if (node?.textContent) return node.textContent.trim() } return '' } function pickLink(el) { // Atom : const atomLink = [...el.querySelectorAll('link')].find((l) => l.getAttribute('href')) if (atomLink) return atomLink.getAttribute('href') // RSS 2.0 / 1.0 : text const rssLink = el.querySelector('link') return rssLink?.textContent?.trim() || '' } export function parseFeed(xmlText, label) { // Some servers return HTML error pages instead of XML let trimmed = xmlText.trimStart() if (trimmed.startsWith('', '', '']) { const idx = trimmed.indexOf(end) if (idx !== -1) { trimmed = trimmed.slice(0, idx + end.length) break } } const doc = new DOMParser().parseFromString(trimmed, 'application/xml') const err = doc.querySelector('parsererror') if (err) { throw new Error(`${label || 'feed'}: XML parse error โ€” ${err.textContent.slice(0, 80)}`) } // Items can be (RSS) or (Atom) const nodes = [...doc.querySelectorAll('item, entry')] return nodes.map((n) => ({ title: pickText(n, 'title'), link: pickLink(n), date: pickText(n, 'pubDate', 'published', 'updated', 'date'), })) } async function fetchOne(feed) { const r = await fetch(`${BACKEND}/api/rss?url=${encodeURIComponent(feed.url)}`) if (!r.ok) throw new Error(`${feed.label || feed.url}: HTTP ${r.status}`) const text = await r.text() return parseFeed(text, feed.label || feed.url).map((it) => ({ ...it, source: feed.label || '' })) } export function useRSS() { const items = ref([]) const loading = ref(false) const error = ref(null) async function load(url) { return loadMultiple([{ url, label: '' }]) } async function loadMultiple(feeds) { loading.value = true error.value = null try { const results = await Promise.allSettled(feeds.map(fetchOne)) const merged = [] const errors = [] for (let i = 0; i < results.length; i++) { if (results[i].status === 'fulfilled') { merged.push(...results[i].value) } else { errors.push(results[i].reason.message) } } merged.sort((a, b) => { const da = new Date(a.date).getTime() || 0 const db = new Date(b.date).getTime() || 0 return db - da }) items.value = merged if (errors.length) error.value = errors.join(' ยท ') } finally { loading.value = false } } return { items, loading, error, load, loadMultiple } }