feeds
Syndication feed FORMAT toolkit: parse, detect, convert, and merge web feeds — RSS 2.0, Atom 1.0, and JSON Feed 1.1. Pure hand-rolled lightweight XML/JSON parsing (zero dependencies, Node built-ins...
feeds — syndication feed FORMAT toolkit
Parse, detect, convert, and merge web feeds across the three syndication formats: RSS 2.0, Atom 1.0, and JSON Feed 1.1.
Pure JavaScript — zero npm dependencies, Node built-ins only. The XML reader is a small, forgiving, hand-rolled tag-scanner (no xml2js / fast-xml-parser / cheerio). It handles CDATA, XML/HTML entities (named + numeric), namespaced tags (atom:, dc:, content:encoded, media:), and both RFC-822 (RSS) and ISO-8601 (Atom/JSON) dates.
Design
Everything routes through one canonical item model, so any format converts to any other by passing through the middle:
parseRss ─┐ ┌─ toRss
parseAtom ─┼──-> normalized items ──┼─ toAtom
parseJson ─┘ (title, link, id, └─ toJsonFeed
summary, content,
author, published,
updated, categories,
enclosures)
Normalized item shape:
{
id, title, link,
summary, content,
author,
published, // ISO-8601 string | null
updated, // ISO-8601 string | null
publishedMs, // epoch ms | null (sort key)
categories, // string[]
enclosures, // [{ url, type, length }]
source, // string | null
}
Tools (14)
| Tool | Input | Returns | |||
|---|---|---|---|---|---|
parse | { feed, options? } | auto-detects format, then { format, meta, items } | |||
parseRss | { feed, options? } | parse an RSS 2.0 / RDF string → { format, meta, items } | |||
parseAtom | { feed, options? } | parse an Atom 1.0 string → { format, meta, items } | |||
parseJsonFeed | { feed, options? } | parse a JSON Feed 1.x string/object → { format, meta, items } | |||
detect | { feed } | `{ format: 'rss'\ | 'atom'\ | 'json'\ | 'unknown' }` |
toRss | { items, meta? } | { format:'rss', xml } — valid RSS 2.0 | |||
toAtom | { items, meta? } | { format:'atom', xml } — valid Atom 1.0 | |||
toJsonFeed | { items, meta? } | { format:'json', feed } — JSON Feed 1.1 object | |||
merge | { feeds, options? } | combine parsed feeds/strings into one date-sorted, de-duped list | |||
normalizeItems | { items } | coerce loose objects into the canonical item shape | |||
sortItems | { items, options? } | sort by date (order: 'desc' default, or 'asc') | |||
latest | `{ items \ | feed, count? }` | the count most-recent items (default 10) | ||
extractLinks | { feed } | unique set of all URLs (home + item links + enclosures) | |||
extractCategories | { feed } | unique sorted set of all tags/categories |
options: { limit } (parsers, cap item count) · { order: 'desc'\|'asc' } (sort/merge) · { dedupe: boolean } (merge, on by default — keys on id then link then title).
Every tool takes ONE args object (so an HTTP POST body maps 1:1). Invalid input throws a TypeError/Error; an undetectable feed format throws an error tagged status: 422.
Usage
import feeds from './index.js';
const A = feeds.adapters;
// 1. Parse whatever format you were handed
const parsed = A.parse({ feed: someXmlOrJsonString }); // { format, meta, items }
// 2. Convert RSS → JSON Feed
const rss = A.parseRss({ feed: rssXml });
const { feed } = A.toJsonFeed({ items: rss.items, meta: rss.meta });
// 3. Aggregate several sources into one river, newest-first
const river = A.merge({ feeds: [rssXml, atomXml, jsonFeedString] });
const top5 = A.latest({ items: river.items, count: 5 });
// 4. Re-emit an aggregated Atom feed
const { xml } = A.toAtom({ items: top5.items, meta: { title: 'My River' } });
DRY boundary
This pack owns feed SYNDICATION FORMATS — reading and writing the RSS / Atom / JSON Feed grammar and converting between them. It does not fetch over the network and it does not scrape arbitrary HTML: raw HTML extraction, link discovery, sitemap.xml, and robots.txt belong to the scraping pack. If you need to turn a webpage into a feed, scrape with scraping, shape the results, then hand them to feeds (normalizeItems → toRss/toAtom/toJsonFeed). Numeric/date math stays in numbers; this pack only parses/emits the dates a feed standard requires.