UNPKG

@bliztek/feed-generator

Version:

A simple and lightweight Node.js library for generating RSS 2.0, Atom, and JSON Feed formats.

632 lines (509 loc) 19.7 kB
# Feed Generator: RSS, Atom, JSON ![Build Status](https://github.com/bliztek/feed-generator/actions/workflows/ci.yml/badge.svg) [![Codecov](https://codecov.io/gh/bliztek/feed-generator/graph/badge.svg?token=4GLOVKLC2W)](https://codecov.io/gh/bliztek/feed-generator) ![npm](https://img.shields.io/npm/v/@bliztek/feed-generator) ![License](https://img.shields.io/npm/l/@bliztek/feed-generator) ![Downloads](https://img.shields.io/npm/dm/@bliztek/feed-generator) ![Bundle Size](https://img.shields.io/bundlephobia/min/@bliztek/feed-generator) A simple and lightweight Node.js library for generating **RSS 2.0**, **Atom**, and **JSON Feed** formats. Define your feed once, output to any format. Zero dependencies. Podcast-ready with full iTunes namespace support. --- ## Features - **One unified API** -- define your feed once, generate RSS, Atom, or JSON Feed output. - **Fluent builder** -- construct feeds incrementally with a chainable `FeedBuilder` API. - **Syndication-ready** -- RSS `content:encoded` and `media:content` for full-content syndication to platforms like Dev.to. - **Custom extensions** -- inject arbitrary XML namespaces, elements, and JSON Feed properties (Dublin Core, GeoRSS, etc.). - **Podcast support** -- full iTunes namespace for Apple Podcasts, Spotify, and other directories. - **Content sanitization** -- opt-in whitelist-based HTML sanitization with XSS, `data:` URI, and CDATA injection protection. - **Input validation** -- validates URLs, dates, enclosures, and RSS-specific fields with structured `FeedValidationError` errors. - **XSLT stylesheets** -- optional `<?xml-stylesheet?>` processing instruction for browser-friendly feeds. - **WebSub hub** -- declare a hub link for real-time push notifications across all formats. - **Compact output** -- toggle between pretty-printed and minified output. - **Feed pagination** -- built-in support for paginated feeds across all formats. - **ESM + CJS** -- dual module build with proper `exports` field and tree-shaking support. - Type-safe with full TypeScript support. Zero external dependencies. --- ## Installation ```bash npm install @bliztek/feed-generator # or pnpm add @bliztek/feed-generator ``` --- ## Quick Start ```typescript import { generateFeed } from "@bliztek/feed-generator"; const feed = { title: "My Blog", link: "https://example.com", description: "The latest articles from my blog.", items: [ { id: "https://example.com/post-1", title: "First Post", link: "https://example.com/post-1", content: "<p>The full content of the first post.</p>", summary: "A summary of the first post.", date: "2024-12-14T10:00:00Z", }, ], }; // Generate a single format const rss = generateFeed(feed, "rss"); const atom = generateFeed(feed, "atom"); const json = generateFeed(feed, "json"); ``` ### Generate All Formats at Once ```typescript import { generateFeeds } from "@bliztek/feed-generator"; const { rss, atom, json } = generateFeeds(feed); ``` ### FeedBuilder (Fluent API) ```typescript import { FeedBuilder } from "@bliztek/feed-generator"; const rss = new FeedBuilder() .title("My Blog") .link("https://example.com") .description("The latest articles from my blog.") .language("en-us") .ttl(60) .addItem({ id: "post-1", title: "First Post", link: "https://example.com/post-1", content: "<p>Hello world!</p>", date: "2024-12-14T10:00:00Z", }) .generate("rss"); // Or generate all formats at once const all = new FeedBuilder() .title("My Blog") .link("https://example.com") .description("Latest posts.") .addItem({ id: "1", title: "Post" }) .generateAll(); // all.rss, all.atom, all.json // Or get the raw Feed object const feedData = new FeedBuilder() .title("My Blog") .link("https://example.com") .addItem({ id: "1", title: "Post" }) .build(); ``` --- ## Options Both `generateFeed` and `generateFeeds` accept an optional options argument: ```typescript type FeedOptions = { sanitize?: boolean; // Sanitize HTML content (default: false) pretty?: boolean; // Pretty-print output (default: true) stylesheet?: string; // XSLT stylesheet URL for RSS/Atom (ignored for JSON) }; ``` ### Content Sanitization When `sanitize: true` is set, HTML content in items is processed through a whitelist-based sanitizer that: - **Allows** safe tags: `<p>`, `<a>`, `<img>`, `<strong>`, `<em>`, `<ul>`, `<table>`, and ~40 others - **Strips with content**: `<script>`, `<style>`, `<svg>`, `<math>`, `<iframe>`, `<object>`, `<embed>`, `<applet>` - **Strips tags only** (keeps text): any other tag not in the allowlist - **Blocks dangerous URLs**: `javascript:`, `data:`, and `vbscript:` in `href`, `src`, and `action` attributes - **Removes event handlers**: all `on*` attributes (`onclick`, `onerror`, etc.) - **Filters attributes**: only safe attributes are kept per tag (e.g., `<a>` keeps `href`, `title`, `rel`, `target`) ```typescript const safeFeed = generateFeed(feed, "rss", { sanitize: true }); ``` ### Compact Output ```typescript // Minified XML/JSON -- no indentation or newlines const compact = generateFeed(feed, "rss", { pretty: false }); ``` --- ## Error Handling All validation errors throw `FeedValidationError` with structured `field` and `reason` properties: ```typescript import { generateFeed, FeedValidationError } from "@bliztek/feed-generator"; try { generateFeed(feed, "rss"); } catch (e) { if (e instanceof FeedValidationError) { console.log(e.field); // "title", "items[0].link", etc. console.log(e.reason); // Human-readable description } } ``` The following validations are performed automatically: - Feed `title` and `link` are required - Feed `link` must be a valid URL - RSS feeds require `description` - All URLs are validated (`link`, `nextUrl`, `image.url`, item `link`, enclosure `url`) - All dates are validated (`updated`, item `date`, item `updated`) - RSS enclosures require `length` - `ttl` must be a non-negative integer - `skipHours` values must be integers 0--23 - `skipDays` values must be valid day names (Monday--Sunday) - Item `image` must be a valid URL - `feedLinks.hub` must be a valid URL - Extension `xmlNamespaces` keys must start with `xmlns:` - Extension `jsonProperties` keys must start with `_` (JSON Feed spec) --- ## Podcast Feeds Generate podcast RSS feeds with full iTunes namespace support: ```typescript const podcastFeed = { title: "My Podcast", link: "https://example.com/podcast", description: "A podcast about tech.", authors: [{ name: "Jane Doe", email: "jane@example.com" }], podcast: { type: "episodic", explicit: false, owner: { name: "Jane Doe", email: "jane@example.com" }, category: [ { name: "Technology", subcategory: "Software How-To" }, { name: "Education" }, ], image: "https://example.com/podcast/cover.jpg", }, items: [ { id: "ep1", title: "Episode 1: Getting Started", link: "https://example.com/podcast/ep1", summary: "An introduction to the show.", date: "2024-12-14T10:00:00Z", enclosures: [ { url: "https://example.com/podcast/ep1.mp3", type: "audio/mpeg", length: 50000000, }, ], podcast: { duration: 1830, // seconds, or "30:30" string episode: 1, season: 1, episodeType: "full", // "full" | "trailer" | "bonus" explicit: false, }, }, ], }; const rss = generateFeed(podcastFeed, "rss"); // Includes xmlns:itunes, <itunes:category>, <itunes:duration>, etc. ``` ### Podcast Types ```typescript type PodcastFeed = { type?: "episodic" | "serial"; explicit?: boolean; owner?: { name: string; email: string }; category?: string | { name: string; subcategory?: string }[]; image?: string; newFeedUrl?: string; block?: boolean; complete?: boolean; }; type PodcastItem = { duration?: number | string; // seconds or "HH:MM:SS" explicit?: boolean; episode?: number; season?: number; episodeType?: "full" | "trailer" | "bonus"; image?: string; block?: boolean; }; ``` --- ## Feed Pagination All three formats support pagination via `nextUrl`: ```typescript const page1 = { title: "My Blog", link: "https://example.com", description: "Latest posts.", nextUrl: "https://example.com/feed?page=2", items: [/* ... */], }; // RSS: <atom:link rel="next" href="..." /> // Atom: <link rel="next" href="..." /> // JSON: "next_url": "..." ``` --- ## Custom Extensions Inject arbitrary XML namespaces and elements into RSS/Atom output, or custom properties into JSON Feed, without waiting for library support: ```typescript import { generateFeed, XmlNode } from "@bliztek/feed-generator"; const feed = { title: "My Blog", link: "https://example.com", description: "A blog with custom extensions.", extensions: { xmlNamespaces: { "xmlns:georss": "http://www.georss.org/georss", }, xmlElements: [ { tag: "georss:point", children: ["45.256 -71.92"] } as XmlNode, ], jsonProperties: { _custom_source: "my-cms", // JSON Feed extensions must use _-prefixed keys }, }, items: [ { id: "1", title: "Post", content: "<p>Hello</p>", extensions: { xmlElements: [ { tag: "georss:point", children: ["40.714 -74.006"] } as XmlNode, ], jsonProperties: { _reading_time: 5 }, }, }, ], }; const rss = generateFeed(feed, "rss"); // RSS output includes xmlns:georss and <georss:point> elements ``` Validation enforces that XML namespace keys start with `xmlns:` and JSON Feed extension keys start with `_` per the JSON Feed spec. --- ## XSLT Stylesheets Make RSS/Atom feeds render nicely in browsers by adding an XSLT stylesheet reference: ```typescript const rss = generateFeed(feed, "rss", { stylesheet: "/feed.xsl" }); // Output: <?xml version="1.0" encoding="UTF-8"?> // <?xml-stylesheet type="text/xsl" href="/feed.xsl"?> // <rss ...> ``` The `stylesheet` option is ignored for JSON Feed output. --- ## WebSub Hub Declare a WebSub hub for real-time push notifications of feed updates: ```typescript const feed = { title: "My Blog", link: "https://example.com", description: "A blog.", feedLinks: { rss: "https://example.com/rss.xml", hub: "https://hub.example.com", }, items: [/* ... */], }; // RSS: <atom:link rel="hub" href="https://hub.example.com" /> // Atom: <link rel="hub" href="https://hub.example.com" /> // JSON: "hubs": [{ "type": "WebSub", "url": "https://hub.example.com" }] ``` --- ## Feed Data Reference ### `Feed` | Field | Type | Required | Description | |---|---|---|---| | `title` | `string` | Yes | Feed title | | `link` | `string` | Yes | Site URL (validated) | | `description` | `string` | RSS only | Feed description (required for RSS) | | `id` | `string` | No | Feed ID (defaults to `link`) | | `language` | `string` | No | Language code (e.g., `"en-us"`) | | `copyright` | `string` | No | Copyright notice | | `updated` | `string \| Date` | No | Last update (auto-derived from items if omitted) | | `generator` | `string` | No | Generator name | | `image` | `FeedImage` | No | Feed image/logo | | `favicon` | `string` | No | Favicon URL (JSON Feed) | | `authors` | `FeedAuthor[]` | No | Feed authors | | `categories` | `FeedCategory[]` | No | Feed categories | | `items` | `FeedItem[]` | Yes | Feed items/entries | | `ttl` | `number` | No | Time-to-live in minutes (RSS cache hint) | | `skipHours` | `number[]` | No | Hours (0--23) aggregators can skip (RSS) | | `skipDays` | `string[]` | No | Day names aggregators can skip (RSS) | | `nextUrl` | `string` | No | Next page URL for paginated feeds | | `podcast` | `PodcastFeed` | No | Podcast metadata (RSS only) | | `feedLinks` | `object` | No | Self-referencing URLs (`{ rss?, atom?, json?, hub? }`) | | `extensions` | `FeedExtensions` | No | Custom XML namespaces, elements, and JSON properties | ### `FeedItem` | Field | Type | Required | Description | |---|---|---|---| | `id` | `string` | Yes | Unique item identifier | | `title` | `string` | No | Item title | | `link` | `string` | No | Item URL (validated) | | `content` | `string` | No | HTML content | | `contentText` | `string` | No | Plain text content (JSON Feed) | | `summary` | `string` | No | Short summary | | `date` | `string \| Date` | No | Publication date (validated) | | `updated` | `string \| Date` | No | Last modified date (validated) | | `authors` | `FeedAuthor[]` | No | Item authors (all rendered in RSS) | | `categories` | `FeedCategory[]` | No | Item categories/tags | | `image` | `string` | No | Item image URL (RSS, Atom, JSON Feed) | | `enclosures` | `FeedEnclosure[]` | No | Attached media (podcasts, etc.) | | `language` | `string` | No | Item language (JSON Feed) | | `podcast` | `PodcastItem` | No | Podcast episode metadata (RSS only) | | `extensions` | `FeedItemExtensions` | No | Custom XML elements and JSON properties | ### Supporting Types ```typescript type FeedAuthor = { name: string; url?: string; email?: string; avatar?: string; }; type FeedCategory = { name: string; domain?: string; label?: string; }; type FeedEnclosure = { url: string; type: string; // MIME type length?: number; // Size in bytes (required for RSS) title?: string; duration?: number; // Duration in seconds }; type FeedImage = { url: string; title?: string; link?: string; width?: number; height?: number; }; ``` --- ## API Reference ### `generateFeed(feed, format, options?): string` Generate a feed in a single format. - **`format`**: `"rss"` | `"atom"` | `"json"` - **`options`**: `{ sanitize?: boolean, pretty?: boolean, stylesheet?: string }` - **Returns**: XML string (RSS/Atom) or JSON string - **Throws**: `FeedValidationError` if feed data is invalid ### `generateFeeds(feed, options?): FeedOutput` Generate all three formats at once. - **Returns**: `{ rss: string, atom: string, json: string }` - **Throws**: `FeedValidationError` if feed data is invalid ### `FeedBuilder` Fluent builder for constructing feeds incrementally. | Method | Description | |---|---| | `.title(s)` | Set feed title | | `.link(s)` | Set feed link | | `.description(s)` | Set feed description | | `.id(s)` | Set feed ID | | `.language(s)` | Set language code | | `.copyright(s)` | Set copyright notice | | `.updated(s)` | Set last update date | | `.generator(s)` | Set generator name | | `.image(img)` | Set feed image | | `.favicon(s)` | Set favicon URL | | `.author(a)` | Add an author (can be called multiple times) | | `.category(c)` | Add a category (can be called multiple times) | | `.nextUrl(s)` | Set pagination URL | | `.ttl(n)` | Set TTL in minutes | | `.skipHours(h[])` | Set skip hours | | `.skipDays(d[])` | Set skip days | | `.podcast(p)` | Set podcast metadata | | `.feedLinks(links)` | Set self-referencing URLs | | `.hub(url)` | Set WebSub hub URL | | `.extensions(ext)` | Set custom extensions | | `.stylesheet(url)` | Set XSLT stylesheet URL | | `.addItem(item)` | Add a single item | | `.addItems(items)` | Add multiple items | | `.build()` | Return the raw `Feed` object | | `.generate(format, options?)` | Render a single format | | `.generateAll(options?)` | Render all three formats | ### `FeedValidationError` Structured error class thrown on invalid feed data. - **`field`**: `string` -- path to the invalid field (e.g., `"items[0].link"`) - **`reason`**: `string` -- human-readable description of the issue --- ## Example Output ### RSS ```xml <?xml version="1.0" encoding="UTF-8"?> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"> <channel> <title>My Blog</title> <link>https://example.com</link> <description>The latest articles from my blog.</description> <item> <title>First Post</title> <link>https://example.com/post-1</link> <guid isPermaLink="true">https://example.com/post-1</guid> <description><![CDATA[<p>The full content of the first post.</p>]]></description> <content:encoded><![CDATA[<p>The full content of the first post.</p>]]></content:encoded> <pubDate>Sat, 14 Dec 2024 10:00:00 GMT</pubDate> </item> </channel> </rss> ``` ### Atom ```xml <?xml version="1.0" encoding="UTF-8"?> <feed xmlns="http://www.w3.org/2005/Atom"> <title type="text">My Blog</title> <link href="https://example.com" rel="alternate" type="text/html" /> <id>https://example.com</id> <updated>2024-12-14T10:00:00.000Z</updated> <entry> <title type="text">First Post</title> <link href="https://example.com/post-1" rel="alternate" type="text/html" /> <id>https://example.com/post-1</id> <published>2024-12-14T10:00:00.000Z</published> <content type="html">&lt;p&gt;The full content of the first post.&lt;/p&gt;</content> <summary type="text">A summary of the first post.</summary> </entry> </feed> ``` ### JSON Feed ```json { "version": "https://jsonfeed.org/version/1.1", "title": "My Blog", "home_page_url": "https://example.com", "items": [ { "id": "https://example.com/post-1", "url": "https://example.com/post-1", "title": "First Post", "content_html": "<p>The full content of the first post.</p>", "summary": "A summary of the first post.", "date_published": "2024-12-14T10:00:00.000Z" } ] } ``` --- ## Feed Validation - **RSS/Atom:** [W3C Feed Validator](https://validator.w3.org/feed/) - **JSON Feed:** [JSON Feed Validator](https://validator.jsonfeed.org/) --- ## Upgrading from 2.0 v2.1 is fully backwards compatible. No code changes are required. **What changes in output:** - RSS feeds with `item.content` now include an additional `<content:encoded>` element alongside the existing `<description>`. The `<description>` element is unchanged. - RSS feeds with `item.image` now include `<media:content>` elements and the `xmlns:media` namespace declaration. - Atom feeds with `item.image` now include an enclosure `<link>` element. These are additive changes. Existing feed consumers will not be affected. If you need the previous output exactly, pin to `2.0.x`. **New optional features** (opt-in, no effect unless used): - `extensions` on `Feed` and `FeedItem` for custom namespaces and elements - `stylesheet` in `FeedOptions` for XSLT support - `feedLinks.hub` for WebSub hub declarations See the full [CHANGELOG](./CHANGELOG.md) for details. --- ## Contributing Contributions are welcome! If you'd like to improve this package: 1. Fork the repository. 2. Create a new branch: ```bash git checkout -b feature-name ``` 3. Make your changes and commit: ```bash git commit -m "Add new feature" ``` 4. Push your branch and open a Pull Request. --- ## More Information This package is built and maintained by [Bliztek](https://bliztek.com). We design and develop customized websites and web applications to help businesses of all sizes achieve their goals. Our solutions prioritize scalability, security, and performance to drive growth and ensure long-term success. - Follow [@Dev4TheWeb](https://x.com/Dev4TheWeb) on Twitter/X for updates from the creator - Follow [@Bliztek](https://x.com/bliztek) on Twitter/X for updates from the company side - Read our [Company blog](https://bliztek.com/blog) to learn more about our contributions to open source! - Read my [Personal blog](https://stevendevelops.com) to learn more about what I do! --- ## License This package is licensed under the [MIT License](./LICENSE).