RSS, Atom and JSON feeds
Publish public content with negotiated representations and cache validators.
Model the publication#
import { createRssFeed } from '@zeruxjs/feed';
const xml = createRssFeed({ title: 'Updates', description: 'Product news',
siteUrl: 'https://example.com', feedUrl: 'https://example.com/feed.xml',
items: [{ id: 'release-1', title: 'First release',
url: 'https://example.com/releases/1', date: '2025-01-02' }] });Equivalent generators cover Atom 1.0 and JSON Feed 1.1. URLs must be credential-free HTTP(S); dates and enclosure sizes are validated. Keep entry IDs stable as content changes.
Fetch publication#
createFeedFetchHandler from @zeruxjs/feed/fetch supports a fixed format or Accept negotiation. It handles GET/HEAD, 405, 406, ETags, Last-Modified, Vary: Accept and public cache lifetimes. Set a maximum item count and supply a current publication function.
Scope#
Only publish content intended for all feed readers. Personalized feeds require an application-specific private response/cache policy. Feed generation does not send SSE, schedule polling or notify subscribers automatically.
Install and select the integration#
npm install @zeruxjs/feed@0.0.4There is no automatic feed startup service in zerux.config.js for this package. Import its public APIs in the owning Node application and retain any Fetch handler for awaited close. There is no browser controller. If a config object holds custom settings, your plugin/application code must explicitly consume them. See configuration integration boundaries and TypeScript.
A practical integration workflow#
Build a feed from published public items with stable IDs, valid HTTP(S) URLs, dates and the correct feed/site origin. Choose RSS, Atom or JSON Feed for the client population; use the Fetch helper when negotiation, HEAD and conditional responses are needed. Supply a changing content function rather than a stale array when publication can change.
TypeScript: inputs, results and ownership#
import { createRssFeed } from '@zeruxjs/feed';
type Inputs = Parameters<typeof createRssFeed>;
type Result = Awaited<ReturnType<typeof createRssFeed>>;Inputs is the function's complete argument tuple, including optional parameters. Result removes a Promise when the function is asynchronous. These aliases follow the installed declarations; no internal src/dist import is required. For a generic factory, keep the concrete call's inferred type when an application needs a narrower result than ReturnType preserves. Use satisfies or a public option type for configuration literals, and validate unknown request/file data separately.
In a JavaScript application the equivalent editor annotation is /** @type {Parameters<typeof createRssFeed>} */ on a matching argument tuple. This provides diagnostics only when JavaScript checking is enabled. It does not validate network data or create a resource automatically. See strict application setup for the complete NodeNext checker configuration.
Failure modes and resource lifetime#
Keep public feed helpers separate from personalized/private feeds. A crawler or RSS client may cache content after publication, so do not include unpublished fields or assume later deletion revokes copies. Test Accept negotiation, 406, representation-aware Vary, ETag/304 and item/date/size limits. Feed generation does not create an SSE/WebSocket subscription.
Verify before deploying#
- Exercise the example with the actual owning runtime/browser, not only an imported module.
- Test one invalid input and the relevant unavailable/denied dependency path; do not turn those into a silent success.
- Confirm teardown leaves no owned listeners, timers, connections or stale per-user state.
- Check the production entry point and installed export map; a workspace checkout can contain features not yet published to npm.
Public option and data contracts#
These tables list fields declared directly on the named public contract. Referenced/inherited types retain their own declarations; optional does not imply a default value. Use the lifecycle and configuration chapters above for actual initialization behavior.
FeedOptions#
| Field | Accepted type/value | Optional |
|---|---|---|
title | string | No |
description | string | No |
siteUrl | string | No |
feedUrl | string | Yes |
id | Authored feed identity, at most 2048 units | Yes |
authors | FeedAuthor[], at most 32 | Yes |
tags | string[], at most 100 | Yes |
categories | FeedCategory[], at most 100 | Yes |
strict | Atom author/IRI/date checks; default false | Yes |
maxItems | Integer 1–10000; direct-generator default 10000 | Yes |
maxBytes | UTF-8 output bytes 256–67108864; default 8388608 | Yes |
language | string | Yes |
copyright | string | Yes |
image | string | Yes |
updated | string|Date | Yes |
items | FeedItem[] | No |
FeedItem#
| Field | Accepted type/value | Optional |
|---|---|---|
id | string | No |
atomId | Separate authored Atom identity | Yes |
contentText | Plain text, at most 1048576 units | Yes |
authors | FeedAuthor[], at most 32 | Yes |
categories | FeedCategory[], at most 100 | Yes |
attachments | FeedAttachment[], at most 16; RSS at most one | Yes |
language | Canonicalized BCP 47 tag, at most 64 units | Yes |
title | string | No |
url | string | No |
description | string | Yes |
contentHtml | string | Yes |
date | Date | string | Yes |
updated | Date | string | Yes |
author | string | Yes |
tags | string[] | Yes |
image | string | Yes |
enclosure | {url:string;type:string;length:number} | Yes |
Choose deliberate format coverage#
The package README provides a precise RSS/Atom/JSON Feed compatibility matrix. Structured authors/categories/attachments supplement legacy author/tags/enclosure fields. RSS supports one enclosure and rejects multiple; Atom/JSON support multiple. Person email/URI/avatar and category label/domain have format-specific mappings, not a promise that every field survives every representation.
Set strict:true for Atom author, absolute IRI identity and known timestamp checks. Default legacy mode preserves opaque IDs/missing authors and deterministic epoch fallback, which is not a content fact or a full Atom-validity claim. Duplicate logical IDs reject; supplied atomId values must also be unique. No ID or timestamp is randomized at build time. Only validated authored dates drive Last-Modified.
Use contentText or host-sanitized contentHtml. XML escaping prevents markup from breaking the feed document but does not sanitize HTML that readers later render. Attachment URL/MIME/length are validated metadata, not permission to fetch a file; generation performs no remote inspection. Do not publish signed/private URLs.
Fetch publication preserves weighted Accept, representation ETags, HEAD/304 and Vary behavior, adds explicit truncation reporting and bounded producer cancellation, and returns redacted 503 on dependency failure. Await handler.close during teardown; actual producer settlement owns capacity and borrowed resources remain with the host. This public helper does not authenticate a tenant or personalize a feed. The public-import example covers strict authored publication; parser/HTTP fixtures are separate from live reader or HTML-sanitizer qualification.
Source and cleanup ownership#
The Node-oriented root validates/serializes public content with explicit UTF-16 string and UTF-8 byte budgets. Selection helpers may borrow existing records; normalizeFeed produces a detached mutable candidate. Encoding XML delimiters is separate from Unicode validation and HTML sanitization. Raw JSON must not be inserted into an HTML script block. Host objects/producers are trusted code, not a sandbox boundary.
The low-level createFeedResponse always returns public cache headers. Only the explicit /fetch owner performs negotiation, validators, defensive credential-bearing no-store responses and cooperative producer cancellation. Its timer cannot preempt synchronous serialization; timed-out work retains capacity until it actually settles. Await close without expecting it to close borrowed databases or sockets. The public-import example uses stable authored dates and owns no remote downloads.
FeedAuthor#
| Field | Accepted type/value | Optional |
|---|---|---|
name | string, at most 512 UTF-16 units | No |
url | Absolute credential-free HTTP(S) URL | Yes |
email | Validated mailbox, at most 254 units | Yes |
avatar | HTTP(S) URL, JSON Feed only | Yes |
FeedCategory#
| Field | Accepted type/value | Optional |
|---|---|---|
term | string, at most 512 units | No |
scheme | HTTP(S) taxonomy URL | Yes |
label | string, at most 512 units; Atom only | Yes |
FeedAttachment#
| Field | Accepted type/value | Optional |
|---|---|---|
url | Absolute credential-free HTTP(S) URL | No |
type | Bare MIME type/subtype, at most 128 units | No |
length | Safe nonnegative integer bytes | No |
title | string, at most 512 units | Yes |
duration | Finite seconds, 0–315360000; JSON Feed only | Yes |
FeedFetchOptions#
| Field | Accepted type/value | Optional |
|---|---|---|
feed | FeedOptions or trusted (context: {signal: AbortSignal}) => FeedOptions / Promise producer | No |
format | rss, atom, json, negotiate; default rss | Yes |
cacheSeconds | Integer 0–31536000 seconds; default 300 | Yes |
maxItems | Selected prefix 1–10000; default 1000 | Yes |
maxBytes | Returned UTF-8 bytes 256–67108864; default 8388608 | Yes |
timeoutMs | Cooperative observation 1–60000ms; default 5000 | Yes |
maxPending | Actual unsettled jobs 1–128 per handler; default 8 | Yes |
The handler also exposes status() with immutable closed/pending counters and an idempotent awaited close(). These are process-local lifecycle facts, not distributed coordination, durable storage or reader interoperability guarantees.
Public entry points
Manifest reference for @zeruxjs/feed@0.0.4. Each target describes a published entry, not automatic framework registration.
| Import | Target |
|---|---|
@zeruxjs/feed | ./dist/index.js |
@zeruxjs/feed/fetch | ./dist/fetch.js |
Runtime requirements and dependencies
{
"engines": {},
"dependencies": {},
"peerDependencies": {},
"bin": {}
}Full package README
@zeruxjs/feed#
Modular RSS 2.0, Atom 1.0, and JSON Feed 1.1 generators.
import { createRssFeed, createAtomFeed, createJsonFeed, createFeedResponse } from '@zeruxjs/feed';
const config = {
title: 'Updates', description: 'Product releases', siteUrl: 'https://example.com',
feedUrl: 'https://example.com/feed.xml',
items: [{ id: 'release-1', title: 'Version 1', url: 'https://example.com/releases/1', date: '2025-01-02' }],
};
const xml = createRssFeed(config);
const http = createFeedResponse('rss', config, 300); // { status, headers, body }Separated into types, validation/XML escaping utilities, RSS, Atom, JSON Feed, content types and HTTP helpers. URLs must be HTTP(S) and cannot contain credentials. Date and enclosure size validation are included.
Fetch-compatible publication endpoints#
import { createFeedFetchHandler } from '@zeruxjs/feed/fetch';
const feed = createFeedFetchHandler({
feed: () => getPublishedArticles(),
format: 'negotiate', // RSS, Atom or JSON Feed from Accept
maxItems: 1000,
cacheSeconds: 300
});Supports GET/HEAD/405, quality-weighted Accept negotiation (406 for unacceptable types), ETag/304, Last-Modified conditional responses, representation-aware Vary: Accept, public caching and input limits. Supply only public feed content; personalized or authenticated feeds should be handled by application-specific private routes instead of these public helpers. This does not push SSE/WebSocket updates.
Format coverage and explicit compatibility#
See the runnable public-import example. Generators preserve authored item/author/category/attachment order and do not fetch images, author URLs or enclosure files. The model is deliberately a subset of RSS 2.0, Atom 1.0 and JSON Feed 1.1:
| Field | RSS | Atom | JSON Feed |
|---|---|---|---|
| Identity | item.id as opaque guid, isPermaLink=false | feed id or siteUrl; item.atomId or item.id | item.id string |
| Authors | repeated Dublin Core creator names; one item email also emits standard author | multiple name/uri/email, feed inheritance | multiple name/url/avatar, feed/item inheritance |
| Categories | feed/item term, optional domain from scheme | feed/item term, scheme, label | item terms as tags; no feed taxonomy field |
| Language | channel language; item dc:language extension | feed/entry xml:lang | feed/item language |
| Attachments | one enclosure: URL, MIME, byte length | multiple enclosure links plus optional title | multiple attachments plus title/duration seconds |
| Content | description/plain text, content:encoded HTML extension | summary; text or escaped HTML content | content_text/content_html |
| Dates | pubDate and known-content lastBuildDate | updated/published | date_published/date_modified |
| Images | channel image | feed logo | feed icon/item image and author avatars |
Email has no JSON Feed author field; avatar has no Atom/RSS person field; category labels and attachment duration/title have only the mappings listed. Unsupported podcast/media extension vocabularies, contributors, source records, RDF feeds, streaming feeds and complete reader/vendor parity are not claimed. RSS rejects multiple attachments rather than silently discarding all but one.
Legacy author:string, tags:string[] and enclosure:{url,type,length} remain supported. New structured fields are authors, categories, attachments, contentText, language, feed id and item atomId. Legacy author/enclosure cannot be combined with their structured replacements on the same item. Tags and categories append in declared order. URLs are absolute HTTP(S), credential-free and bounded; language uses canonical BCP 47. MIME is a bare type/subtype, without parameters. Attachment length is a safe nonnegative integer; duration is a finite nonnegative number up to ten years. Metadata does not grant fetch/download access.
Stable IDs, dates and strict Atom#
Duplicate item IDs reject by exact case-sensitive string comparison; Atom also rejects duplicate effective atomId values. No random IDs or build-time timestamps are generated. Feed authors and IDs must be content-owned facts, not guessed from an address/path. For standards-oriented Atom output, set strict:true: it requires feed authors or authors on every entry, a known feed update time and an own date/updated on every entry, plus absolute HTTP(S), urn or tag identifiers. Feed id defaults to the validated site URL if not explicitly supplied. ID checks are a narrow absolute-IRI policy, not validation of every possible IRI scheme.
For API compatibility, default/false Atom mode still permits legacy opaque IDs, missing authors and an epoch fallback when no update facts exist. That fallback is deterministic, not a real content modification date; this legacy mode is not a claim of full Atom validity. Existing root imports/factories remain usable.
Dates accept valid Date objects or supported ISO calendar dates/RFC3339-style strings with seconds, optional 1–3 millisecond digits and explicit timezone. Date-only input normalizes to UTC midnight. Impossible calendar dates and hour 24 reject rather than rolling forward; ambiguous natural-language dates are not accepted. updated/HTTP Last-Modified uses the maximum supplied feed/item update or publication date, not generation time. For title/author/other metadata changes, the host must advance its authored update date; the helper cannot infer that fact. ETags still reflect every serialized byte. With no known dates, HTTP Last-Modified is omitted. JSON Feed items without supplied text/HTML use their known title as minimal content_text to satisfy the required content-field shape.
Untrusted HTML and runtime bounds#
contentHtml is not sanitized. XML escaping keeps it within a text node; a feed reader may decode and render that HTML. The host must sanitize untrusted HTML before publishing, or use contentText. RSS readers may also treat descriptions as HTML. Receivers must sanitize before rendering; XML escaping is not an HTML sanitizer or script encoder. JSON output must not be inserted directly into an HTML script block. The verified security package's context-specific encoders and validator HTTP boundaries remain separate contracts; no homemade HTML sanitizer, remote inspection or application authorization is introduced here.
All generators validate runtime records, plain dense arrays, dates, URL schemes, MIME, finite numbers and duplicate identities before serialization; accessors, class records, unknown fields and unsafe XML text reject. Defaults are 10000 items and 8 MiB output, configurable with FeedOptions.maxItems/maxBytes up to 10000 and 64 MiB. Selected item input caps at 16 MiB. Authors cap at 32, tags/categories at 100 each, attachments at 16, URLs at 8192 characters and text/HTML bodies at 1 MiB per field. Output limits describe returned payload bytes, not total JS heap.
Fetch negotiation, producer ownership and failures#
The existing callable createFeedFetchHandler now also has close() and status(). Sources may remain zero-argument callbacks, or accept {signal}. No request/tenant identity is inferred. Only public content belongs in this handler: host filtering, consent and authorization must happen before publication. Requests carrying Cookie or Authorization receive private,no-store as a defensive cache boundary, not as an authentication check. Use an explicitly authorized private route for personalized feeds. Never put signed URLs, private subjects or credentials into a public feed.
GET/HEAD negotiate the three declared MIME types with quality and specificity; exact q=0 exclusions override wildcards, malformed/out-of-range qualities reject the range, and unavailable Accept values return 406 before calling the producer. Negotiated responses vary on Accept. Format-specific ETags, weak If-None-Match, If-Modified-Since precedence and body-free HEAD/304 are supported. Source order is preserved, so stable content/config/order yields stable representation ETags. The Fetch maxItems option selects the first N items as before; truncation is now reported with X-Feed-Truncated:true, and uniqueness applies to the emitted prefix. Source arrays cap at 10000. Invalid formats reject instead of falling through to JSON.
Fetch settings add maxBytes (8 MiB default, 64 MiB maximum), timeoutMs (5000 default, 1–60000) and maxPending (8 default, 1–128). Supplier errors, invalid selected content, overload and cancellation/deadlines return body-free no-store 503 without raw causes. Capacity remains occupied until the producer actually settles. close stops new calls, aborts cooperative producers and drains native settlement; a noncooperative callback can delay close. It does not close host databases/HTTP clients or forcibly stop JavaScript. Imports and fixed-feed generation own no network resources.
Qualification uses the development-only Saxes namespace-aware XML parser, JSON.parse, format assertions and focused HTTP/ownership fixtures. It covers special characters, strict missing facts, duplicates, invalid dates/MIME/URLs, limits, 304/HEAD/406, partial Accept exclusions, outages and drain. XML parsing proves well-formedness, not full schema validation or all-reader interoperability. No live feed reader, browser HTML sanitizer or remote enclosure service was exercised; the host must qualify those if used. Generation itself needs no live external prerequisite.
Reading the implementation and owning publication#
The public root is Node-oriented: utils.ts uses Node UTF-8 byte accounting for bounded normalization/output. The /fetch entry additionally uses Node cryptographic hashing and native Fetch/Abort APIs. No browser controller or automatic framework service is installed. types.ts documents content-owned fields and units; content-type.ts selects only declared media types. rss.ts, atom.ts and json.ts serialize the documented subset without downloading references. Generated XML literals intentionally retain their exact whitespace because representation bytes and ETags are observable.
The root authors, attachments and categories helpers select/project values; they are not standalone validators or deep clones. normalizeFeed stages a detached candidate, while validate discards that candidate after checking it. String limits count UTF-16 code units; byte limits count UTF-8. Direct escapeXml encodes delimiters/whitespace and strips selected controls; full Unicode validation belongs to normalization. Neither helper is an HTML sanitizer or a sandbox for application Proxy/coercion code.
createFeedResponse always returns public cache headers and does not inspect credentials or negotiate a request. The retained /fetch handler supplies conditional responses and defensive private headers for credential-bearing requests, but still has no principal/tenant authorization. Its timer bounds cooperative observation when the event loop runs; synchronous serialization and arbitrary producer JavaScript cannot be preempted. close() stops admission, aborts cooperative work and awaits actual producer settlement; it never closes a host database/client or revokes copies readers already cached.