@molecule/api-feed-rss 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +749 -0
- package/package.json +9 -3
package/README.md
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
3
|
+
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
|
+
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
|
+
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
+
Generated: 2026-08-04T00:36:30.162Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/api-feed-rss
|
|
10
|
+
|
|
11
|
+
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
12
|
+
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
13
|
+
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
14
|
+
|
|
15
|
+
RSS / Atom / JSON Feed **producer** for molecule.dev — pure-function
|
|
16
|
+
serializers that turn a single normalized {@link Feed} structure into
|
|
17
|
+
a valid RSS 2.0, Atom 1.0, or JSON Feed 1.1 document.
|
|
18
|
+
|
|
19
|
+
Companion to `@molecule/api-feed-rss-parser` (the consumer side). Both
|
|
20
|
+
packages share a similar normalized shape, so apps can ingest external
|
|
21
|
+
feeds, store the items, and re-publish them through this package
|
|
22
|
+
without intermediate translation layers.
|
|
23
|
+
|
|
24
|
+
Apps that use this:
|
|
25
|
+
|
|
26
|
+
- `blog` — emit `/feed.rss`, `/feed.atom`, `/feed.json` for posts.
|
|
27
|
+
- `news-aggregator` — re-publish curated cross-source feeds.
|
|
28
|
+
- `podcast` — RSS 2.0 with the iTunes namespace (set `feed.itunes`).
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
One serializer per call
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { serializeRss2, serializeAtom1, serializeJsonFeed } from '@molecule/api-feed-rss'
|
|
36
|
+
|
|
37
|
+
const feed = {
|
|
38
|
+
title: 'My Blog',
|
|
39
|
+
link: 'https://example.com',
|
|
40
|
+
description: 'Posts about software',
|
|
41
|
+
items: [{ id: '1', title: 'Hello', link: 'https://example.com/p/1' }],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rss = serializeRss2(feed)
|
|
45
|
+
const atom = serializeAtom1(feed)
|
|
46
|
+
const json = serializeJsonFeed(feed)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
HTTP handler with caching — Express adapter
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createFeedHandler, type Feed } from '@molecule/api-feed-rss'
|
|
53
|
+
|
|
54
|
+
const loadFromDb = async (): Promise<Feed> => ({
|
|
55
|
+
title: 'My Feed',
|
|
56
|
+
link: 'https://example.com',
|
|
57
|
+
description: 'Latest posts',
|
|
58
|
+
items: [],
|
|
59
|
+
}) // your DB lookup
|
|
60
|
+
const app = { get(_path: string, _fn: (req: any, res: any) => void): void {} } // your Express app
|
|
61
|
+
|
|
62
|
+
const handle = createFeedHandler({
|
|
63
|
+
loadFeed: async () => loadFromDb(),
|
|
64
|
+
cacheTtlMs: 60_000,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Serves GET /feed.rss, /feed.atom, /feed.json (keyed by extension).
|
|
68
|
+
// The handler returns { status, headers, body } — map those onto any
|
|
69
|
+
// framework's response (Fastify/Hono/raw http look the same).
|
|
70
|
+
app.get('/feed.:ext', async (req, res) => {
|
|
71
|
+
const result = await handle({
|
|
72
|
+
extension: req.params.ext,
|
|
73
|
+
ifNoneMatch: req.get('If-None-Match'),
|
|
74
|
+
})
|
|
75
|
+
res.status(result.status).set(result.headers).send(result.body)
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Security**: every user-supplied field is escaped before serialization.
|
|
80
|
+
RSS / Atom XML use {@link escapeXml} for text nodes, {@link escapeAttr}
|
|
81
|
+
for attributes, {@link escapeUrl} for URL attributes (which neutralizes
|
|
82
|
+
`javascript:` URIs), and {@link wrapCdata} for HTML content (with
|
|
83
|
+
`]]>` splitting). JSON Feed relies on `JSON.stringify` for escaping.
|
|
84
|
+
|
|
85
|
+
## Type
|
|
86
|
+
|
|
87
|
+
`utility`
|
|
88
|
+
|
|
89
|
+
## Installation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm install @molecule/api-feed-rss
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API
|
|
96
|
+
|
|
97
|
+
### Interfaces
|
|
98
|
+
|
|
99
|
+
#### `CreateFeedHandlerOptions`
|
|
100
|
+
|
|
101
|
+
Configuration for {@link createFeedHandler}.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
interface CreateFeedHandlerOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Async loader returning the feed snapshot. Called whenever the cache is
|
|
107
|
+
* cold or expired. Must throw on data-source failure — the handler then
|
|
108
|
+
* returns `503 Service Unavailable`.
|
|
109
|
+
*/
|
|
110
|
+
loadFeed: () => Promise<Feed> | Feed
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Cache TTL in milliseconds. Defaults to `60_000` (1 minute). Set to
|
|
114
|
+
* `0` to disable caching (every request re-loads).
|
|
115
|
+
*/
|
|
116
|
+
cacheTtlMs?: number
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Map of URL-suffix → {@link FeedOutputFormat}.
|
|
120
|
+
*
|
|
121
|
+
* Defaults to `{ rss: 'rss-2.0', atom: 'atom-1.0', json: 'json-feed' }`.
|
|
122
|
+
*
|
|
123
|
+
* The handler reads the request path's extension via {@link FeedRequest.extension}
|
|
124
|
+
* (callers compute this from the URL — see the function-level docs).
|
|
125
|
+
*/
|
|
126
|
+
extensions?: Record<string, FeedOutputFormat>
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Optional logger called on errors. Defaults to a no-op.
|
|
130
|
+
*/
|
|
131
|
+
onError?: (err: unknown) => void
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### `Feed`
|
|
136
|
+
|
|
137
|
+
Feed-level metadata + items, format-agnostic input shape consumed by
|
|
138
|
+
every serializer.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
interface Feed {
|
|
142
|
+
/** Channel / feed title. Required. */
|
|
143
|
+
title: string
|
|
144
|
+
|
|
145
|
+
/** Canonical website URL (NOT the feed self-link). Required. */
|
|
146
|
+
link: string
|
|
147
|
+
|
|
148
|
+
/** Long-form description / subtitle / tagline. Required by RSS 2.0. */
|
|
149
|
+
description: string
|
|
150
|
+
|
|
151
|
+
/** Self-referential URL of the feed document. Recommended for Atom + JSON Feed. */
|
|
152
|
+
feedUrl?: string
|
|
153
|
+
|
|
154
|
+
/** RFC 5646 / BCP 47 language tag (e.g. `en`, `en-US`). */
|
|
155
|
+
language?: string
|
|
156
|
+
|
|
157
|
+
/** ISO 8601 / `Date` of the feed's last update. Defaults to "now" for Atom. */
|
|
158
|
+
updatedAt?: string | Date
|
|
159
|
+
|
|
160
|
+
/** Channel author(s). */
|
|
161
|
+
authors?: FeedAuthor[]
|
|
162
|
+
|
|
163
|
+
/** Copyright / rights statement. */
|
|
164
|
+
copyright?: string
|
|
165
|
+
|
|
166
|
+
/** Channel image / logo / icon URL. */
|
|
167
|
+
imageUrl?: string
|
|
168
|
+
|
|
169
|
+
/** Channel-level categories (RSS only). */
|
|
170
|
+
categories?: string[]
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Channel-level iTunes podcast metadata. Presence of this field switches
|
|
174
|
+
* the RSS 2.0 serializer into "podcast mode" — it declares
|
|
175
|
+
* `xmlns:itunes` and emits per-item `<itunes:*>` elements.
|
|
176
|
+
*/
|
|
177
|
+
itunes?: FeedChannelItunes
|
|
178
|
+
|
|
179
|
+
/** Items in display order. The serializer does not re-sort by date. */
|
|
180
|
+
items: FeedItem[]
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### `FeedAuthor`
|
|
185
|
+
|
|
186
|
+
Author of a feed or item.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
interface FeedAuthor {
|
|
190
|
+
/** Display name. Required for Atom; optional everywhere else. */
|
|
191
|
+
name: string
|
|
192
|
+
|
|
193
|
+
/** Author email address. Optional. */
|
|
194
|
+
email?: string
|
|
195
|
+
|
|
196
|
+
/** Author profile / homepage URL. Optional. */
|
|
197
|
+
url?: string
|
|
198
|
+
|
|
199
|
+
/** Avatar URL (JSON Feed only). */
|
|
200
|
+
avatarUrl?: string
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### `FeedChannelItunes`
|
|
205
|
+
|
|
206
|
+
Channel-level iTunes podcast metadata.
|
|
207
|
+
|
|
208
|
+
Trigger condition: when present (even as `{}`), the RSS 2.0 serializer
|
|
209
|
+
declares the iTunes namespace and emits the configured `<itunes:*>`
|
|
210
|
+
channel elements.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
interface FeedChannelItunes {
|
|
214
|
+
/** Show author / host name (`<itunes:author>`). */
|
|
215
|
+
author?: string
|
|
216
|
+
|
|
217
|
+
/** Show owner contact info — both fields required by Apple Podcasts. */
|
|
218
|
+
owner?: {
|
|
219
|
+
/** Owner's display name. */
|
|
220
|
+
name: string
|
|
221
|
+
/** Owner's email address. */
|
|
222
|
+
email: string
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Show artwork URL (`<itunes:image href="...">`). 1400x1400+ recommended. */
|
|
226
|
+
imageUrl?: string
|
|
227
|
+
|
|
228
|
+
/** Whether the show contains explicit material. */
|
|
229
|
+
explicit?: boolean
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* iTunes show categories — top-level only (subcategories not currently
|
|
233
|
+
* supported by this serializer).
|
|
234
|
+
*/
|
|
235
|
+
categories?: string[]
|
|
236
|
+
|
|
237
|
+
/** Long-form show summary. */
|
|
238
|
+
summary?: string
|
|
239
|
+
|
|
240
|
+
/** Show type — `episodic` (default) or `serial`. */
|
|
241
|
+
type?: 'episodic' | 'serial'
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### `FeedItem`
|
|
246
|
+
|
|
247
|
+
A single feed item / entry / post.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
interface FeedItem {
|
|
251
|
+
/**
|
|
252
|
+
* Stable, opaque identifier. Required.
|
|
253
|
+
*
|
|
254
|
+
* RSS 2.0 emits this as `<guid>`; Atom as `<id>`; JSON Feed as `id`.
|
|
255
|
+
* If {@link FeedItem.link} is set and `id` looks like an HTTP(S) URL,
|
|
256
|
+
* RSS will mark `<guid isPermaLink="true">`.
|
|
257
|
+
*/
|
|
258
|
+
id: string
|
|
259
|
+
|
|
260
|
+
/** Item title. Required. */
|
|
261
|
+
title: string
|
|
262
|
+
|
|
263
|
+
/** Canonical URL the item points to. */
|
|
264
|
+
link?: string
|
|
265
|
+
|
|
266
|
+
/** Short summary / excerpt. Plain text or HTML. */
|
|
267
|
+
summary?: string
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Full content body. HTML is allowed; the serializer wraps it in CDATA
|
|
271
|
+
* for RSS / Atom and escapes it for JSON Feed.
|
|
272
|
+
*/
|
|
273
|
+
content?: string
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Whether {@link FeedItem.content} is HTML (`true`, default) or plain text.
|
|
277
|
+
* Drives JSON Feed `content_html` vs `content_text` selection and Atom
|
|
278
|
+
* `<content type="html">` vs `<content type="text">`.
|
|
279
|
+
*/
|
|
280
|
+
contentIsHtml?: boolean
|
|
281
|
+
|
|
282
|
+
/** Item author(s). RSS uses the first entry; Atom and JSON Feed list all. */
|
|
283
|
+
authors?: FeedAuthor[]
|
|
284
|
+
|
|
285
|
+
/** Publication timestamp. ISO 8601 string or `Date`. */
|
|
286
|
+
publishedAt?: string | Date
|
|
287
|
+
|
|
288
|
+
/** Last modification timestamp. ISO 8601 string or `Date`. */
|
|
289
|
+
updatedAt?: string | Date
|
|
290
|
+
|
|
291
|
+
/** Tags / categories / keywords. */
|
|
292
|
+
categories?: string[]
|
|
293
|
+
|
|
294
|
+
/** Media enclosures (podcast audio, attachments, etc.). */
|
|
295
|
+
enclosures?: FeedItemEnclosure[]
|
|
296
|
+
|
|
297
|
+
/** Per-item iTunes podcast metadata. See {@link FeedItemItunes}. */
|
|
298
|
+
itunes?: FeedItemItunes
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### `FeedItemEnclosure`
|
|
303
|
+
|
|
304
|
+
Media enclosure (podcast audio, video, image, etc.).
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
interface FeedItemEnclosure {
|
|
308
|
+
/** Direct URL to the media resource. Required. */
|
|
309
|
+
url: string
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* MIME type. Required by RSS 2.0 spec — defaults to
|
|
313
|
+
* `application/octet-stream` when omitted.
|
|
314
|
+
*/
|
|
315
|
+
type?: string
|
|
316
|
+
|
|
317
|
+
/** Length in bytes. Required by RSS 2.0 spec — defaults to `0` when omitted. */
|
|
318
|
+
length?: number
|
|
319
|
+
|
|
320
|
+
/** Display title or alt text. JSON Feed only. */
|
|
321
|
+
title?: string
|
|
322
|
+
|
|
323
|
+
/** Duration in seconds. Used by JSON Feed `attachments[].duration_in_seconds`. */
|
|
324
|
+
durationSeconds?: number
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
#### `FeedItemItunes`
|
|
329
|
+
|
|
330
|
+
Optional iTunes podcast metadata for an item.
|
|
331
|
+
|
|
332
|
+
When supplied AND the channel-level {@link Feed.itunes} block is also
|
|
333
|
+
present, the RSS 2.0 serializer emits the iTunes podcast namespace
|
|
334
|
+
(`xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"`) and per-item
|
|
335
|
+
`<itunes:duration>`, `<itunes:author>`, `<itunes:explicit>`,
|
|
336
|
+
`<itunes:image>`, `<itunes:episode>`, `<itunes:season>`, `<itunes:summary>`
|
|
337
|
+
elements.
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
interface FeedItemItunes {
|
|
341
|
+
/** Episode duration in seconds. Serialized as HH:MM:SS. */
|
|
342
|
+
durationSeconds?: number
|
|
343
|
+
|
|
344
|
+
/** Episode artwork URL. Emitted as `<itunes:image href="...">`. */
|
|
345
|
+
imageUrl?: string
|
|
346
|
+
|
|
347
|
+
/** Author display name (overrides item-level `<itunes:author>`). */
|
|
348
|
+
author?: string
|
|
349
|
+
|
|
350
|
+
/** Whether the episode contains explicit material. */
|
|
351
|
+
explicit?: boolean
|
|
352
|
+
|
|
353
|
+
/** Episode number within the season / series. */
|
|
354
|
+
episode?: number
|
|
355
|
+
|
|
356
|
+
/** Season number. */
|
|
357
|
+
season?: number
|
|
358
|
+
|
|
359
|
+
/** Plain-text episode summary (`<itunes:summary>`). */
|
|
360
|
+
summary?: string
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
#### `FeedRequest`
|
|
365
|
+
|
|
366
|
+
Minimal request shape consumed by the generated handler.
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
interface FeedRequest {
|
|
370
|
+
/**
|
|
371
|
+
* URL extension portion — `'rss'`, `'atom'`, `'json'`, or any custom key
|
|
372
|
+
* the caller wired in {@link CreateFeedHandlerOptions.extensions}.
|
|
373
|
+
*
|
|
374
|
+
* Callers compute this from the request URL (e.g.
|
|
375
|
+
* `req.path.split('.').pop()` for Express).
|
|
376
|
+
*/
|
|
377
|
+
extension: string
|
|
378
|
+
|
|
379
|
+
/** Optional `If-None-Match` ETag value, for 304 short-circuiting. */
|
|
380
|
+
ifNoneMatch?: string
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### `FeedResponse`
|
|
385
|
+
|
|
386
|
+
Minimal response shape returned by the handler. Framework adapters map
|
|
387
|
+
this onto their native response API (`res.status().set().send()` for
|
|
388
|
+
Express, `new Response(body, { status, headers })` for fetch, etc.).
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
interface FeedResponse {
|
|
392
|
+
/** HTTP status code: `200` / `304` / `404` / `503`. */
|
|
393
|
+
status: number
|
|
394
|
+
|
|
395
|
+
/** Headers — always includes `Content-Type` on `200`. */
|
|
396
|
+
headers: Record<string, string>
|
|
397
|
+
|
|
398
|
+
/** Body — empty string on `304` / `404` / errors. */
|
|
399
|
+
body: string
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
#### `SerializeAtom1Options`
|
|
404
|
+
|
|
405
|
+
Options controlling {@link serializeAtom1}.
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
interface SerializeAtom1Options {
|
|
409
|
+
/** XML declaration. Defaults to `<?xml version="1.0" encoding="UTF-8"?>`. */
|
|
410
|
+
xmlDeclaration?: string
|
|
411
|
+
/** Pretty-print with 2-space indentation. Defaults to `true`. */
|
|
412
|
+
pretty?: boolean
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
#### `SerializeJsonFeedOptions`
|
|
417
|
+
|
|
418
|
+
Options controlling {@link serializeJsonFeed}.
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
interface SerializeJsonFeedOptions {
|
|
422
|
+
/** Pretty-print with 2-space indentation. Defaults to `false`. */
|
|
423
|
+
pretty?: boolean
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### `SerializeRss2Options`
|
|
428
|
+
|
|
429
|
+
Options controlling {@link serializeRss2}.
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
interface SerializeRss2Options {
|
|
433
|
+
/** XML declaration. Defaults to `<?xml version="1.0" encoding="UTF-8"?>`. */
|
|
434
|
+
xmlDeclaration?: string
|
|
435
|
+
/** Pretty-print with 2-space indentation. Defaults to `true`. */
|
|
436
|
+
pretty?: boolean
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Types
|
|
441
|
+
|
|
442
|
+
#### `FeedOutputFormat`
|
|
443
|
+
|
|
444
|
+
Output format discriminator for {@link serializeFeed} and the
|
|
445
|
+
{@link createFeedHandler} HTTP handler.
|
|
446
|
+
|
|
447
|
+
- `rss-2.0` — RSS 2.0 XML, served as `application/rss+xml`.
|
|
448
|
+
- `atom-1.0` — Atom 1.0 XML, served as `application/atom+xml`.
|
|
449
|
+
- `json-feed` — JSON Feed 1.1, served as `application/feed+json`.
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
type FeedOutputFormat = 'rss-2.0' | 'atom-1.0' | 'json-feed'
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Classes
|
|
456
|
+
|
|
457
|
+
#### `FeedSerializeError`
|
|
458
|
+
|
|
459
|
+
Error raised when the {@link Feed} input fails minimum-viability checks
|
|
460
|
+
(missing required `title` / `link` / `description`, missing item `id` or
|
|
461
|
+
`title`, etc.).
|
|
462
|
+
|
|
463
|
+
### Functions
|
|
464
|
+
|
|
465
|
+
#### `assertFeedShape(feed, format)`
|
|
466
|
+
|
|
467
|
+
Validate {@link Feed} input has the minimum required fields for the
|
|
468
|
+
given output format.
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
function assertFeedShape(feed: Feed, format: FeedOutputFormat): void
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
- `feed` — Feed input.
|
|
475
|
+
- `format` — Target serialization format.
|
|
476
|
+
|
|
477
|
+
#### `createFeedHandler(options)`
|
|
478
|
+
|
|
479
|
+
Create a framework-agnostic feed HTTP handler with built-in TTL caching.
|
|
480
|
+
|
|
481
|
+
The returned handler accepts a {@link FeedRequest} and returns a
|
|
482
|
+
{@link FeedResponse}. It serves three formats by default — RSS 2.0,
|
|
483
|
+
Atom 1.0, JSON Feed — keyed by URL extension.
|
|
484
|
+
|
|
485
|
+
**Caching** — feed payloads are cached per-format for `cacheTtlMs`
|
|
486
|
+
(default 1 minute). Each cache entry carries an ETag computed from the
|
|
487
|
+
payload hash; matching `If-None-Match` triggers a `304` response.
|
|
488
|
+
|
|
489
|
+
**Error handling** — if `loadFeed` throws, the handler returns `503`
|
|
490
|
+
with body `''`. The error is forwarded to `onError` if provided.
|
|
491
|
+
|
|
492
|
+
**No XSS surface** — all interpolation runs through the underlying
|
|
493
|
+
serializers (`serializeRss2` / `serializeAtom1` / `serializeJsonFeed`),
|
|
494
|
+
which escape every user-supplied field. The handler itself never
|
|
495
|
+
concatenates request data into the response body.
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
function createFeedHandler(
|
|
499
|
+
options: CreateFeedHandlerOptions,
|
|
500
|
+
): (req: FeedRequest) => Promise<FeedResponse>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
- `options` — See {@link CreateFeedHandlerOptions}.
|
|
504
|
+
|
|
505
|
+
**Returns:** Async handler function `(req: FeedRequest) => Promise<FeedResponse>`.
|
|
506
|
+
|
|
507
|
+
#### `escapeAttr(value)`
|
|
508
|
+
|
|
509
|
+
Escape an attribute value (alias of {@link escapeXml} kept distinct so
|
|
510
|
+
future tightening — e.g. enforcing quote style — has a single call site).
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
function escapeAttr(value: string | number | boolean | null | undefined): string
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
- `value` — Raw attribute value.
|
|
517
|
+
|
|
518
|
+
**Returns:** Escaped string suitable for `="..."` attribute syntax.
|
|
519
|
+
|
|
520
|
+
#### `escapeUrl(value)`
|
|
521
|
+
|
|
522
|
+
Validate + escape a URL for safe use as an `href` / `src` / RSS `<link>`
|
|
523
|
+
value. Strips `javascript:` and other dangerous schemes — those are
|
|
524
|
+
replaced with `about:blank`.
|
|
525
|
+
|
|
526
|
+
Allowed schemes (case-insensitive): `http`, `https`, `mailto`, `ftp`,
|
|
527
|
+
`tel`, plus protocol-relative (`//...`) and absolute paths (`/...`) and
|
|
528
|
+
relative paths (`./...`, `../...`, `name`, `name?...`, `name#...`).
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
function escapeUrl(value: string | null | undefined): string
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
- `value` — Raw URL. `undefined` / `null` collapse to `''`.
|
|
535
|
+
|
|
536
|
+
**Returns:** Escaped URL or `'about:blank'` for disallowed schemes.
|
|
537
|
+
|
|
538
|
+
#### `escapeXml(value)`
|
|
539
|
+
|
|
540
|
+
Escape a string for inclusion in an XML text node or attribute value.
|
|
541
|
+
|
|
542
|
+
Maps the five XML predefined entities:
|
|
543
|
+
|
|
544
|
+
- `&` → `&`
|
|
545
|
+
- `<` → `<`
|
|
546
|
+
- `>` → `>`
|
|
547
|
+
- `"` → `"`
|
|
548
|
+
- `'` → `'`
|
|
549
|
+
|
|
550
|
+
Also strips XML 1.0 illegal control characters (`U+0000`–`U+0008`,
|
|
551
|
+
`U+000B`, `U+000C`, `U+000E`–`U+001F`). `\t`, `\n`, `\r` are preserved.
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
function escapeXml(value: string | number | boolean | null | undefined): string
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
- `value` — Raw string. `undefined` / `null` collapse to `''`.
|
|
558
|
+
|
|
559
|
+
**Returns:** Escaped string safe for any XML position.
|
|
560
|
+
|
|
561
|
+
#### `indent(body, pad)`
|
|
562
|
+
|
|
563
|
+
Indent each newline of `body` by `pad` spaces. Used for pretty-printing.
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
function indent(body: string, pad: number): string
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
- `body` — Multi-line text. Empty input returns empty string.
|
|
570
|
+
- `pad` — Number of spaces to prepend to each line.
|
|
571
|
+
|
|
572
|
+
**Returns:** Indented text.
|
|
573
|
+
|
|
574
|
+
#### `looksLikeHttpUrl(value)`
|
|
575
|
+
|
|
576
|
+
Whether a string looks like an HTTP(S) URL — used to set
|
|
577
|
+
`<guid isPermaLink>` and to fall back to `link` for `<id>` synthesis.
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
function looksLikeHttpUrl(value: string | null | undefined): boolean
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
- `value` — Candidate string.
|
|
584
|
+
|
|
585
|
+
**Returns:** `true` if the value parses as `http:` or `https:` URL.
|
|
586
|
+
|
|
587
|
+
#### `serializeAtom1(feed, options)`
|
|
588
|
+
|
|
589
|
+
Serialize a {@link Feed} to a complete Atom 1.0 XML document.
|
|
590
|
+
|
|
591
|
+
Conforms to RFC 4287:
|
|
592
|
+
|
|
593
|
+
- Declares `xmlns="http://www.w3.org/2005/Atom"` on the root.
|
|
594
|
+
- Always emits required `<id>`, `<title>`, `<updated>` elements.
|
|
595
|
+
- Each entry has `<id>`, `<title>`, `<updated>`.
|
|
596
|
+
- HTML content is wrapped in CDATA inside `<content type="html">` /
|
|
597
|
+
`<summary type="html">`. Plain-text content uses `type="text"` with
|
|
598
|
+
raw escaping.
|
|
599
|
+
|
|
600
|
+
**All user-supplied content is escaped** — text fields run through
|
|
601
|
+
{@link escapeXml}, attribute values through {@link escapeAttr}, URLs
|
|
602
|
+
through {@link escapeUrl} (which neutralizes `javascript:` URIs), and
|
|
603
|
+
HTML-bearing fields are CDATA-wrapped via {@link wrapCdata}.
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
function serializeAtom1(feed: Feed, options?: SerializeAtom1Options): string
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
- `feed` — The feed structure to serialize.
|
|
610
|
+
- `options` — See {@link SerializeAtom1Options}.
|
|
611
|
+
|
|
612
|
+
**Returns:** Complete Atom 1.0 XML document string.
|
|
613
|
+
|
|
614
|
+
#### `serializeFeed(feed, format)`
|
|
615
|
+
|
|
616
|
+
Serialize a {@link Feed} to the body string for a given output format.
|
|
617
|
+
|
|
618
|
+
Delegates to {@link serializeRss2} / {@link serializeAtom1} /
|
|
619
|
+
{@link serializeJsonFeed}. Useful when callers want a single entry
|
|
620
|
+
point keyed by a runtime-determined format (e.g. inside a route
|
|
621
|
+
handler dispatching on the URL extension).
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
function serializeFeed(feed: Feed, format: FeedOutputFormat): string
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
- `feed` — The feed structure.
|
|
628
|
+
- `format` — Target output format.
|
|
629
|
+
|
|
630
|
+
**Returns:** Serialized body string.
|
|
631
|
+
|
|
632
|
+
#### `serializeJsonFeed(feed, options)`
|
|
633
|
+
|
|
634
|
+
Serialize a {@link Feed} to a complete JSON Feed 1.1 document.
|
|
635
|
+
|
|
636
|
+
The output conforms to https://www.jsonfeed.org/version/1.1/ — the
|
|
637
|
+
entire payload is built as plain JS objects then `JSON.stringify`-ed,
|
|
638
|
+
so all string content is naturally JSON-escaped. No CDATA / XML
|
|
639
|
+
escaping is applicable.
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
function serializeJsonFeed(feed: Feed, options?: SerializeJsonFeedOptions): string
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
- `feed` — The feed structure to serialize.
|
|
646
|
+
- `options` — See {@link SerializeJsonFeedOptions}.
|
|
647
|
+
|
|
648
|
+
**Returns:** Complete JSON Feed 1.1 document string.
|
|
649
|
+
|
|
650
|
+
#### `serializeRss2(feed, options)`
|
|
651
|
+
|
|
652
|
+
Serialize a {@link Feed} to a complete RSS 2.0 XML document.
|
|
653
|
+
|
|
654
|
+
The output is valid RSS 2.0 — declares the `dc`, `content`, and `atom`
|
|
655
|
+
namespaces always, plus `itunes` when {@link Feed.itunes} is present.
|
|
656
|
+
|
|
657
|
+
**All user-supplied content is escaped** — text fields run through
|
|
658
|
+
{@link escapeXml}, attribute values through {@link escapeAttr}, URLs
|
|
659
|
+
through {@link escapeUrl} (which neutralizes `javascript:` URIs), and
|
|
660
|
+
HTML-bearing fields (`description`, `content:encoded`,
|
|
661
|
+
`itunes:summary`) are CDATA-wrapped via {@link wrapCdata} with
|
|
662
|
+
`]]>` splitting.
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
function serializeRss2(feed: Feed, options?: SerializeRss2Options): string
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
- `feed` — The feed structure to serialize.
|
|
669
|
+
- `options` — See {@link SerializeRss2Options}.
|
|
670
|
+
|
|
671
|
+
**Returns:** Complete RSS 2.0 XML document string.
|
|
672
|
+
|
|
673
|
+
#### `toHhMmSs(seconds)`
|
|
674
|
+
|
|
675
|
+
Format a duration in seconds as `HH:MM:SS`, used for `<itunes:duration>`.
|
|
676
|
+
|
|
677
|
+
Returns `undefined` for non-finite or negative input.
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
function toHhMmSs(seconds: number | null | undefined): string | undefined
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
- `seconds` — Whole or fractional seconds.
|
|
684
|
+
|
|
685
|
+
**Returns:** `HH:MM:SS` string or `undefined`.
|
|
686
|
+
|
|
687
|
+
#### `toIso(value)`
|
|
688
|
+
|
|
689
|
+
Coerce a `Date` / ISO string / arbitrary string into an ISO 8601 string.
|
|
690
|
+
|
|
691
|
+
Returns `undefined` if the input is missing or unparseable. Unparseable
|
|
692
|
+
arbitrary strings fall back to `undefined` rather than throwing — the
|
|
693
|
+
serializer simply omits the timestamp element rather than emitting
|
|
694
|
+
`Invalid Date`.
|
|
695
|
+
|
|
696
|
+
```typescript
|
|
697
|
+
function toIso(value: string | Date | null | undefined): string | undefined
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
- `value` — Raw `Date`, ISO string, or other date-like string.
|
|
701
|
+
|
|
702
|
+
**Returns:** ISO 8601 timestamp (`2026-04-28T15:00:00.000Z`) or `undefined`.
|
|
703
|
+
|
|
704
|
+
#### `toRfc822(value)`
|
|
705
|
+
|
|
706
|
+
Coerce a `Date` / ISO string into an RFC 822 date string suitable for
|
|
707
|
+
RSS 2.0 `<pubDate>` / `<lastBuildDate>` fields.
|
|
708
|
+
|
|
709
|
+
Always returns GMT / `+0000`. Returns `undefined` for unparseable input.
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
function toRfc822(value: string | Date | null | undefined): string | undefined
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
- `value` — Raw `Date`, ISO string, or RFC 822 string.
|
|
716
|
+
|
|
717
|
+
**Returns:** RFC 822 timestamp (`Mon, 28 Apr 2026 15:00:00 GMT`) or `undefined`.
|
|
718
|
+
|
|
719
|
+
#### `wrapCdata(value)`
|
|
720
|
+
|
|
721
|
+
Wrap an HTML payload in a `<![CDATA[ ... ]]>` section, splitting any
|
|
722
|
+
accidental closing-marker (`]]>`) inside the payload across two CDATA
|
|
723
|
+
sections so the result remains valid XML.
|
|
724
|
+
|
|
725
|
+
Use for `<description>` / `<content:encoded>` / Atom `<content type="html">`
|
|
726
|
+
where the body is HTML and we want to avoid double-escaping.
|
|
727
|
+
|
|
728
|
+
Also strips XML 1.0 illegal control characters before wrapping (`\t`,
|
|
729
|
+
`\n`, `\r` preserved). The CDATA payload still cannot contain raw
|
|
730
|
+
`<![CDATA[` openers, but those are not produced by typical HTML and
|
|
731
|
+
any literal occurrence is treated as ordinary text by browsers.
|
|
732
|
+
|
|
733
|
+
```typescript
|
|
734
|
+
function wrapCdata(value: string | null | undefined): string
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
- `value` — Raw HTML / text. `undefined` / `null` collapse to `''`.
|
|
738
|
+
|
|
739
|
+
**Returns:** A complete CDATA section, e.g. `<![CDATA[<p>x</p>]]>`.
|
|
740
|
+
|
|
741
|
+
### Constants
|
|
742
|
+
|
|
743
|
+
#### `FEED_CONTENT_TYPES`
|
|
744
|
+
|
|
745
|
+
The MIME type each format is served with.
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
const FEED_CONTENT_TYPES: Record<FeedOutputFormat, string>
|
|
749
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-feed-rss",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "RSS/Atom/JSON Feed producer — pure-function serializers for RSS 2.0, Atom 1.0, JSON Feed 1.1; emits valid XML/JSON with full XML escaping and CDATA wrapping. Includes optional HTTP handler factory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"molecule",
|
|
@@ -30,8 +31,13 @@
|
|
|
30
31
|
"serializer"
|
|
31
32
|
],
|
|
32
33
|
"license": "Apache-2.0",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/molecule-dev/molecule.git",
|
|
37
|
+
"directory": "packages/api/utilities/feed-rss"
|
|
38
|
+
},
|
|
33
39
|
"devDependencies": {
|
|
34
|
-
"@molecule/api-feed-rss-parser": "1.0.
|
|
40
|
+
"@molecule/api-feed-rss-parser": "1.0.1",
|
|
35
41
|
"@types/node": "26.1.2",
|
|
36
42
|
"typescript": "6.0.3",
|
|
37
43
|
"vitest": "4.1.10"
|