From 0e5e3c0a5082f655bd7d3318dda4d8d049c14244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Milanovi=C4=87?= Date: Tue, 7 Jul 2026 14:05:37 +0000 Subject: [PATCH] feat(rss2): allow explicit override of guid isPermaLink attribute Currently the isPermaLink attribute on RSS 2.0 is inferred purely from which fields are set (guid/id/link), with no way to override it: - guid or id present -> isPermaLink="false" - only link present -> isPermaLink="true" This breaks down in two common cases: - id/link is a real, permanent URL, but because id is also set, isPermaLink is forced to "false" (see e.g. discussion in #191). - id is an opaque, non-URL identifier, but a link is also present, so there's no way to force isPermaLink="false" without dropping link. This adds an optional isPermaLink?: boolean field on Item that, when set, takes precedence over the existing inference. When omitted, behavior is unchanged (fully backwards compatible). Related: #191, #89, #213 --- src/__tests__/rss2.spec.ts | 50 ++++++++++++++++++++++++++++++++++++++ src/rss2.ts | 9 ++++--- src/typings/index.ts | 10 ++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/__tests__/rss2.spec.ts b/src/__tests__/rss2.spec.ts index dc520da..f94fe2c 100644 --- a/src/__tests__/rss2.spec.ts +++ b/src/__tests__/rss2.spec.ts @@ -1,6 +1,56 @@ import { Feed } from "../feed"; import { createSampleFeed, published, sampleFeed, updated } from "./setup"; +describe("rss 2.0 isPermaLink override", () => { + it("should specify isPermaLink=true when feed item specifies an id and explicitly sets isPermaLink to true", () => { + const feed = createSampleFeed(); + feed.addItem({ + title: "Hello World", + id: "http://example.org/id-that-is-also-a-real-url", + link: "http://example.org/id-that-is-also-a-real-url", + isPermaLink: true, + date: published, + }); + const actual = feed.rss2(); + expect(actual).toContain('http://example.org/id-that-is-also-a-real-url'); + }); + + it("should specify isPermaLink=false when feed item specifies only a link but explicitly sets isPermaLink to false", () => { + const feed = createSampleFeed(); + feed.addItem({ + title: "Hello World", + link: "http://example.org/link-that-is-not-a-stable-id", + isPermaLink: false, + date: published, + }); + const actual = feed.rss2(); + expect(actual).toContain('http://example.org/link-that-is-not-a-stable-id'); + }); + + it("should still default to isPermaLink=false for a guid when isPermaLink is not set (no regression)", () => { + const feed = createSampleFeed(); + feed.addItem({ + title: "Hello World", + guid: "50e14f43-dd4e-412f-864d-78943ea28d91", + link: "http://example.org/guid", + date: published, + }); + const actual = feed.rss2(); + expect(actual).toContain('50e14f43-dd4e-412f-864d-78943ea28d91'); + }); + + it("should still default to isPermaLink=true for a link-only item when isPermaLink is not set (no regression)", () => { + const feed = createSampleFeed(); + feed.addItem({ + title: "Hello World", + link: "http://example.org/link", + date: published, + }); + const actual = feed.rss2(); + expect(actual).toContain('http://example.org/link'); + }); +}); + describe("rss 2.0", () => { it("should generate a valid feed", () => { const actual = sampleFeed.rss2(); diff --git a/src/rss2.ts b/src/rss2.ts index d527fa9..da585db 100644 --- a/src/rss2.ts +++ b/src/rss2.ts @@ -107,11 +107,14 @@ export default (ins: Feed) => { } if (entry.guid) { - item.guid = { _text: entry.guid, _attributes: { isPermaLink: false } }; + item.guid = { _text: entry.guid, _attributes: { isPermaLink: entry.isPermaLink ?? false } }; } else if (entry.id) { - item.guid = { _text: entry.id, _attributes: { isPermaLink: false } }; + item.guid = { _text: entry.id, _attributes: { isPermaLink: entry.isPermaLink ?? false } }; } else if (entry.link) { - item.guid = { _text: sanitizeUrl(entry.link), _attributes: { isPermaLink: true } }; + item.guid = { + _text: sanitizeUrl(entry.link), + _attributes: { isPermaLink: entry.isPermaLink ?? true }, + }; } if (entry.date) { diff --git a/src/typings/index.ts b/src/typings/index.ts index fd69e05..3f93069 100644 --- a/src/typings/index.ts +++ b/src/typings/index.ts @@ -9,6 +9,16 @@ export interface Item { category?: Category[]; guid?: string; + /** + * Explicitly control the `isPermaLink` attribute on the RSS 2.0 `` element. + * + * If omitted, the existing inference is used: `false` when `guid` or `id` + * is set, `true` when only `link` is set. Pass `true` or `false` here to + * override that inference, e.g. when `id`/`guid` happens to be a real, + * permanent URL (`true`) or when `link` is a permanent URL but `id`/`guid` + * is an opaque, non-URL identifier that should stay `false`. + */ + isPermaLink?: boolean; image?: string | Enclosure; audio?: string | Enclosure;