{
  "schema": "leumas.docs.page/1",
  "id": "adapter:domain/feeds",
  "slug": "adapters/domain/feeds",
  "kind": "capabilities",
  "bucket": "package",
  "title": "feeds — syndication feed FORMAT toolkit",
  "name": "feeds",
  "eyebrow": "syndication feed FORMAT toolkit",
  "chip": null,
  "summary": "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...",
  "keywords": [
    "feeds",
    "syndication",
    "atom",
    "parserss",
    "parsejsonfeed",
    "feeds api",
    "enclosures",
    "torss"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# feeds — syndication feed FORMAT toolkit\n\nParse, detect, convert, and merge web feeds across the three syndication formats:\n**RSS 2.0**, **Atom 1.0**, and **JSON Feed 1.1**.\n\nPure JavaScript — **zero npm dependencies**, Node built-ins only. The XML reader is a small,\nforgiving, hand-rolled tag-scanner (no `xml2js` / `fast-xml-parser` / `cheerio`). It handles\nCDATA, XML/HTML entities (named + numeric), namespaced tags (`atom:`, `dc:`, `content:encoded`,\n`media:`), and both RFC-822 (RSS) and ISO-8601 (Atom/JSON) dates.\n\n## Design\n\nEverything routes through **one canonical item model**, so any format converts to any other by\npassing through the middle:\n\n```\nparseRss  ─┐                        ┌─ toRss\nparseAtom ─┼──-> normalized items ──┼─ toAtom\nparseJson ─┘   (title, link, id,    └─ toJsonFeed\n                summary, content,\n                author, published,\n                updated, categories,\n                enclosures)\n```\n\nNormalized item shape:\n\n```js\n{\n  id, title, link,\n  summary, content,\n  author,\n  published,      // ISO-8601 string | null\n  updated,        // ISO-8601 string | null\n  publishedMs,    // epoch ms | null  (sort key)\n  categories,     // string[]\n  enclosures,     // [{ url, type, length }]\n  source,         // string | null\n}\n```\n\n## Tools (14)\n\n| Tool | Input | Returns |\n|---|---|---|\n| `parse` | `{ feed, options? }` | auto-detects format, then `{ format, meta, items }` |\n| `parseRss` | `{ feed, options? }` | parse an RSS 2.0 / RDF string → `{ format, meta, items }` |\n| `parseAtom` | `{ feed, options? }` | parse an Atom 1.0 string → `{ format, meta, items }` |\n| `parseJsonFeed` | `{ feed, options? }` | parse a JSON Feed 1.x string/object → `{ format, meta, items }` |\n| `detect` | `{ feed }` | `{ format: 'rss'\\|'atom'\\|'json'\\|'unknown' }` |\n| `toRss` | `{ items, meta? }` | `{ format:'rss', xml }` — valid RSS 2.0 |\n| `toAtom` | `{ items, meta? }` | `{ format:'atom', xml }` — valid Atom 1.0 |\n| `toJsonFeed` | `{ items, meta? }` | `{ format:'json', feed }` — JSON Feed 1.1 object |\n| `merge` | `{ feeds, options? }` | combine parsed feeds/strings into one date-sorted, de-duped list |\n| `normalizeItems` | `{ items }` | coerce loose objects into the canonical item shape |\n| `sortItems` | `{ items, options? }` | sort by date (`order: 'desc'` default, or `'asc'`) |\n| `latest` | `{ items \\| feed, count? }` | the `count` most-recent items (default 10) |\n| `extractLinks` | `{ feed }` | unique set of all URLs (home + item links + enclosures) |\n| `extractCategories` | `{ feed }` | unique sorted set of all tags/categories |\n\n`options`: `{ limit }` (parsers, cap item count) · `{ order: 'desc'\\|'asc' }` (sort/merge) ·\n`{ dedupe: boolean }` (merge, on by default — keys on `id` then `link` then `title`).\n\nEvery tool takes ONE args object (so an HTTP POST body maps 1:1). Invalid input throws a\n`TypeError`/`Error`; an undetectable feed format throws an error tagged `status: 422`.\n\n## Usage\n\n```js\nimport feeds from './index.js';\nconst A = feeds.adapters;\n\n// 1. Parse whatever format you were handed\nconst parsed = A.parse({ feed: someXmlOrJsonString });   // { format, meta, items }\n\n// 2. Convert RSS → JSON Feed\nconst rss = A.parseRss({ feed: rssXml });\nconst { feed } = A.toJsonFeed({ items: rss.items, meta: rss.meta });\n\n// 3. Aggregate several sources into one river, newest-first\nconst river = A.merge({ feeds: [rssXml, atomXml, jsonFeedString] });\nconst top5 = A.latest({ items: river.items, count: 5 });\n\n// 4. Re-emit an aggregated Atom feed\nconst { xml } = A.toAtom({ items: top5.items, meta: { title: 'My River' } });\n```\n\n## DRY boundary\n\nThis pack owns feed **SYNDICATION FORMATS** — reading and writing the RSS / Atom / JSON Feed\ngrammar and converting between them. It does **not** fetch over the network and it does **not**\nscrape arbitrary HTML: raw HTML extraction, link discovery, `sitemap.xml`, and `robots.txt` belong\nto the **`scraping`** pack. If you need to *turn a webpage into a feed*, scrape with `scraping`,\nshape the results, then hand them to `feeds` (`normalizeItems` → `toRss`/`toAtom`/`toJsonFeed`).\nNumeric/date math stays in `numbers`; this pack only parses/emits the dates a feed standard requires.\n",
  "source": {
    "path": "shared/engines/adapters/domain/feeds/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 4494,
    "hash": "b96db4d91e2cf8b458b616c494fa4079ef1cda0d"
  },
  "urls": {
    "html": "/p/adapters/domain/feeds",
    "json": "/docs/adapters/domain/feeds.json",
    "md": "/docs/adapters/domain/feeds.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  }
}
