@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
Markdown
# Feed Generator: RSS, Atom, JSON

[](https://codecov.io/gh/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 `` 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"><p>The full content of the first post.</p></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).