Skip to content
Live demo

Parsing XML

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.

SparkFeed uses a custom parser built on top of Node.js’s fast-xml-parser library:

// src/server/lib/rss-parser.ts
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_',
parseAttributeValue: false,
trimValues: true,
parseTagValue: false,
});
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)),
})),
};
}
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)),
})),
};
}

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.

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';
}