@bliztek/feed-generator
v2.2.0
Published
A simple and lightweight Node.js library for generating RSS 2.0, Atom, and JSON Feed formats.
Maintainers
Readme
Feed Generator: RSS, Atom, JSON
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
FeedBuilderAPI. - Syndication-ready -- RSS
content:encodedandmedia:contentfor 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
FeedValidationErrorerrors. - 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
exportsfield and tree-shaking support. - Type-safe with full TypeScript support. Zero external dependencies.
Installation
npm install @bliztek/feed-generator
# or
pnpm add @bliztek/feed-generatorQuick Start
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
import { generateFeeds } from "@bliztek/feed-generator";
const { rss, atom, json } = generateFeeds(feed);FeedBuilder (Fluent API)
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:
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:, andvbscript:inhref,src, andactionattributes - Removes event handlers: all
on*attributes (onclick,onerror, etc.) - Filters attributes: only safe attributes are kept per tag (e.g.,
<a>keepshref,title,rel,target)
const safeFeed = generateFeed(feed, "rss", { sanitize: true });Compact Output
// 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:
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
titleandlinkare required - Feed
linkmust be a valid URL - RSS feeds require
description - All URLs are validated (
link,nextUrl,image.url, itemlink, enclosureurl) - All dates are validated (
updated, itemdate, itemupdated) - RSS enclosures require
length ttlmust be a non-negative integerskipHoursvalues must be integers 0--23skipDaysvalues must be valid day names (Monday--Sunday)- Item
imagemust be a valid URL feedLinks.hubmust be a valid URL- Extension
xmlNamespaceskeys must start withxmlns: - Extension
jsonPropertieskeys must start with_(JSON Feed spec)
Podcast Feeds
Generate podcast RSS feeds with full iTunes namespace support:
const podcastFeed = {
title: "My Podcast",
link: "https://example.com/podcast",
description: "A podcast about tech.",
authors: [{ name: "Jane Doe", email: "[email protected]" }],
podcast: {
type: "episodic",
explicit: false,
owner: { name: "Jane Doe", email: "[email protected]" },
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
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:
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:
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> elementsValidation 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:
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:
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
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:
FeedValidationErrorif feed data is invalid
generateFeeds(feed, options?): FeedOutput
Generate all three formats at once.
- Returns:
{ rss: string, atom: string, json: string } - Throws:
FeedValidationErrorif 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 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 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
{
"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
- JSON Feed: JSON Feed Validator
Upgrading from 2.0
v2.1 is fully backwards compatible. No code changes are required.
What changes in output:
- RSS feeds with
item.contentnow include an additional<content:encoded>element alongside the existing<description>. The<description>element is unchanged. - RSS feeds with
item.imagenow include<media:content>elements and thexmlns:medianamespace declaration. - Atom feeds with
item.imagenow 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):
extensionsonFeedandFeedItemfor custom namespaces and elementsstylesheetinFeedOptionsfor XSLT supportfeedLinks.hubfor WebSub hub declarations
See the full CHANGELOG for details.
Contributing
Contributions are welcome! If you'd like to improve this package:
- Fork the repository.
- Create a new branch:
git checkout -b feature-name - Make your changes and commit:
git commit -m "Add new feature" - Push your branch and open a Pull Request.
More Information
This package is built and maintained by Bliztek. 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 on Twitter/X for updates from the creator
- Follow @Bliztek on Twitter/X for updates from the company side
- Read our Company blog to learn more about our contributions to open source!
- Read my Personal blog to learn more about what I do!
License
This package is licensed under the MIT License.
