@molecule/api-feed-rss 1.0.0 → 1.0.2

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.
Files changed (2) hide show
  1. package/README.md +749 -0
  2. package/package.json +11 -4
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
+ - `&` → `&amp;`
545
+ - `<` → `&lt;`
546
+ - `>` → `&gt;`
547
+ - `"` → `&quot;`
548
+ - `'` → `&apos;`
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,7 +1,8 @@
1
1
  {
2
2
  "name": "@molecule/api-feed-rss",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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
+ "homepage": "https://www.molecule.dev/packages/api-feed-rss",
5
6
  "type": "module",
6
7
  "main": "dist/index.js",
7
8
  "types": "dist/index.d.ts",
@@ -17,7 +18,8 @@
17
18
  }
18
19
  },
19
20
  "files": [
20
- "dist"
21
+ "dist",
22
+ "README.md"
21
23
  ],
22
24
  "keywords": [
23
25
  "molecule",
@@ -30,10 +32,15 @@
30
32
  "serializer"
31
33
  ],
32
34
  "license": "Apache-2.0",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/molecule-dev/molecule.git",
38
+ "directory": "packages/api/utilities/feed-rss"
39
+ },
33
40
  "devDependencies": {
34
- "@molecule/api-feed-rss-parser": "1.0.0",
41
+ "@molecule/api-feed-rss-parser": "1.0.2",
35
42
  "@types/node": "26.1.2",
36
43
  "typescript": "6.0.3",
37
- "vitest": "4.1.10"
44
+ "vitest": "4.1.11"
38
45
  }
39
46
  }