Parsing XML
The Challenge of RSS Parsing
Section titled “The Challenge of RSS Parsing”RSS parsing sounds easy. It’s just XML. In practice, it’s surprisingly tricky:
- Multiple formats: RSS 0.91, 1.0, 2.0 and Atom 1.0 are all in use
- Non-standard extensions: Content namespaces, Dublin Core, Media RSS, etc.
- Malformed XML: Many publishers have invalid HTML inside
<description>tags - Encoding issues: Some feeds incorrectly declare their encoding
SparkFeed handles all of these cases.
The Parser
Section titled “The Parser”SparkFeed uses a custom parser built on top of Node.js’s fast-xml-parser library:
// src/server/lib/rss-parser.tsimport { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_', parseAttributeValue: false, trimValues: true, parseTagValue: false,});RSS 2.0 Parsing
Section titled “RSS 2.0 Parsing”function parseRSS2(xml: unknown): ParsedFeed { const channel = xml?.rss?.channel;
return { title: extractText(channel?.title), url: extractText(channel?.link), items: (channel?.item ?? []).map((item: unknown) => ({ guid: extractText(item?.guid) ?? extractText(item?.link), title: extractText(item?.title), url: extractText(item?.link), content: extractText(item?.['content:encoded']) ?? extractText(item?.description), summary: stripHtml(extractText(item?.description) ?? ''), author: extractText(item?.author) ?? extractText(item?.['dc:creator']), publishedAt: parseDate(extractText(item?.pubDate)), })), };}Atom 1.0 Parsing
Section titled “Atom 1.0 Parsing”function parseAtom(xml: unknown): ParsedFeed { const feed = xml?.feed;
return { title: extractText(feed?.title), url: extractText(feed?.link?.['@_href']) ?? extractText(feed?.id), items: (feed?.entry ?? []).map((entry: unknown) => ({ guid: extractText(entry?.id), title: extractText(entry?.title), url: extractLink(entry?.link), content: extractText(entry?.content) ?? extractText(entry?.summary), summary: stripHtml(extractText(entry?.summary) ?? ''), author: extractText(entry?.author?.name), publishedAt: parseDate(extractText(entry?.published) ?? extractText(entry?.updated)), })), };}HTML Sanitization
Section titled “HTML Sanitization”Feed content often contains HTML. SparkFeed sanitizes it before storing to prevent XSS:
import DOMPurify from 'isomorphic-dompurify';
const ALLOWED_TAGS = ['p', 'h1', 'h2', 'h3', 'h4', 'ul', 'ol', 'li', 'blockquote', 'code', 'pre', 'a', 'strong', 'em', 'img', 'figure', 'figcaption', 'hr', 'br'];
export function sanitizeHtml(dirty: string): string { return DOMPurify.sanitize(dirty, { ALLOWED_TAGS, ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class'], FORCE_BODY: true, });}This strips scripts, iframes, and other dangerous elements while preserving the article’s visual structure.
Auto-Detecting Feed Type
Section titled “Auto-Detecting Feed Type”Before parsing, SparkFeed detects whether a feed is RSS or Atom:
export function detectFeedType(xml: unknown): 'rss' | 'atom' | 'unknown' { if (xml?.rss) return 'rss'; if (xml?.feed?.['@_xmlns'] === 'http://www.w3.org/2005/Atom') return 'atom'; if (xml?.feed) return 'atom'; return 'unknown';}