From bb1bf77ae52193501de559dde1caa271b11c1a9b Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Thu, 30 Mar 2023 14:50:42 +1100 Subject: [PATCH 1/8] Adds extra elements to RSS items. I found I wanted to add categories to my RSS feed, but it wasn't supported, aside from through custom data. I thought it would be better for the RSS generator to have types for the available elements than creating my own XML by hand. --- packages/astro-rss/src/index.ts | 19 +++++++++++++++++++ packages/astro-rss/src/schema.ts | 11 +++++++++++ packages/astro-rss/test/rss.test.js | 14 ++++++++++++++ packages/astro-rss/test/test-utils.js | 15 +++++++++++++++ 4 files changed, 59 insertions(+) diff --git a/packages/astro-rss/src/index.ts b/packages/astro-rss/src/index.ts index 7764b5d132c5..edc902a55828 100644 --- a/packages/astro-rss/src/index.ts +++ b/packages/astro-rss/src/index.ts @@ -211,6 +211,25 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise { if (typeof result.customData === 'string') { Object.assign(item, parser.parse(`${result.customData}`).item); } + if (Array.isArray(result.categories)) { + item.category = result.categories; + } + if (typeof result.author === 'string') { + item.author = result.author; + } + if (typeof result.comments === 'string') { + item.comments = result.comments; + } + if (result.source) { + item.source = parser.parse( + `${result.source.title}` + ).source; + } + if (result.enclosure) { + item.enclosure = parser.parse( + `` + ).enclosure; + } return item; }); diff --git a/packages/astro-rss/src/schema.ts b/packages/astro-rss/src/schema.ts index b24a1441f28c..973ae9d7e5af 100644 --- a/packages/astro-rss/src/schema.ts +++ b/packages/astro-rss/src/schema.ts @@ -6,4 +6,15 @@ export const rssSchema = z.object({ description: z.string().optional(), customData: z.string().optional(), draft: z.boolean().optional(), + categories: z.array(z.string()).optional(), + author: z.string().optional(), + comments: z.string().url().optional(), + source: z.object({ url: z.string().url(), title: z.string() }).optional(), + enclosure: z + .object({ + url: z.string().url(), + length: z.number().positive().int().finite(), + type: z.string(), + }) + .optional(), }); diff --git a/packages/astro-rss/test/rss.test.js b/packages/astro-rss/test/rss.test.js index be4362a34531..2e2caed49407 100644 --- a/packages/astro-rss/test/rss.test.js +++ b/packages/astro-rss/test/rss.test.js @@ -11,6 +11,7 @@ import { phpFeedItemWithCustomData, web1FeedItem, web1FeedItemWithContent, + web1FeedItemWithAllData, } from './test-utils.js'; chai.use(chaiPromises); @@ -25,6 +26,8 @@ const validXmlWithoutWeb1FeedResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithContent.title}]]>${site}${phpFeedItemWithContent.link}/${site}${phpFeedItemWithContent.link}/${new Date(phpFeedItemWithContent.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; // prettier-ignore +const validXmlResultWithAllData = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithAllData.title}]]>${site}${web1FeedItemWithAllData.link}/${site}${web1FeedItemWithAllData.link}/${new Date(web1FeedItemWithAllData.pubDate).toUTCString()}${web1FeedItemWithAllData.categories[0]}${web1FeedItemWithAllData.categories[1]}${web1FeedItemWithAllData.author}${web1FeedItemWithAllData.comments}${web1FeedItemWithAllData.source.title}`; +// prettier-ignore const validXmlWithCustomDataResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithCustomData.title}]]>${site}${phpFeedItemWithCustomData.link}/${site}${phpFeedItemWithCustomData.link}/${new Date(phpFeedItemWithCustomData.pubDate).toUTCString()}${phpFeedItemWithCustomData.customData}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; // prettier-ignore const validXmlWithStylesheet = `<![CDATA[${title}]]>${site}/`; @@ -54,6 +57,17 @@ describe('rss', () => { chai.expect(body).xml.to.equal(validXmlWithContentResult); }); + it('should generate on valid RSSFeedItem array with all RSS content included', async () => { + const { body } = await rss({ + title, + description, + items: [phpFeedItem, web1FeedItemWithAllData], + site, + }); + + chai.expect(body).xml.to.equal(validXmlResultWithAllData); + }); + it('should generate on valid RSSFeedItem array with custom data included', async () => { const { body } = await rss({ xmlns: { diff --git a/packages/astro-rss/test/test-utils.js b/packages/astro-rss/test/test-utils.js index 37f95214bb0e..f9bf868ac7d1 100644 --- a/packages/astro-rss/test/test-utils.js +++ b/packages/astro-rss/test/test-utils.js @@ -30,3 +30,18 @@ export const web1FeedItemWithContent = { ...web1FeedItem, content: `

${web1FeedItem.title}

${web1FeedItem.description}

`, }; +export const web1FeedItemWithAllData = { + ...web1FeedItem, + categories: ['web1', 'history'], + author: 'test@example.com', + comments: 'http://example.com/comments', + source: { + url: 'http://example.com/source', + title: 'The Web 1.0 blog', + }, + enclosure: { + url: 'http://example.com/podcast.mp3', + length: 256, + type: 'audio/mpeg', + }, +}; From 8c2ae61526e67f1d13bedfa1ce3a56b79b46380b Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Sat, 1 Apr 2023 16:32:28 +1100 Subject: [PATCH 2/8] Set isPermaLink='true' for RSS guid. Astro always sets the <guid> element to the URL of the post. For that reason we can also set the isPermaLink attribute to true for all <guid> elements. --- packages/astro-rss/src/index.ts | 3 ++- packages/astro-rss/test/rss.test.js | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/astro-rss/src/index.ts b/packages/astro-rss/src/index.ts index edc902a55828..3548ff6c61d6 100644 --- a/packages/astro-rss/src/index.ts +++ b/packages/astro-rss/src/index.ts @@ -148,6 +148,7 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise { // when using `customData` // https://github.com/withastro/astro/issues/5794 suppressEmptyNode: true, + suppressBooleanAttributes: false, }; const parser = new XMLParser(xmlOptions); const root: any = { '?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' } }; @@ -196,7 +197,7 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise { const item: any = { title: result.title, link: itemLink, - guid: itemLink, + guid: { '#text': itemLink, '@_isPermaLink': 'true' }, }; if (result.description) { item.description = result.description; diff --git a/packages/astro-rss/test/rss.test.js b/packages/astro-rss/test/rss.test.js index 2e2caed49407..ba6a8055d876 100644 --- a/packages/astro-rss/test/rss.test.js +++ b/packages/astro-rss/test/rss.test.js @@ -20,15 +20,15 @@ chai.use(chaiXml); // note: I spent 30 minutes looking for a nice node-based snapshot tool // ...and I gave up. Enjoy big strings! // prettier-ignore -const validXmlResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}<![CDATA[${web1FeedItem.title}]]>${site}${web1FeedItem.link}/${site}${web1FeedItem.link}/${new Date(web1FeedItem.pubDate).toUTCString()}`; +const validXmlResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}<![CDATA[${web1FeedItem.title}]]>${site}${web1FeedItem.link}/${site}${web1FeedItem.link}/${new Date(web1FeedItem.pubDate).toUTCString()}`; // prettier-ignore -const validXmlWithoutWeb1FeedResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}`; +const validXmlWithoutWeb1FeedResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}`; // prettier-ignore -const validXmlWithContentResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithContent.title}]]>${site}${phpFeedItemWithContent.link}/${site}${phpFeedItemWithContent.link}/${new Date(phpFeedItemWithContent.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; +const validXmlWithContentResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithContent.title}]]>${site}${phpFeedItemWithContent.link}/${site}${phpFeedItemWithContent.link}/${new Date(phpFeedItemWithContent.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; // prettier-ignore -const validXmlResultWithAllData = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithAllData.title}]]>${site}${web1FeedItemWithAllData.link}/${site}${web1FeedItemWithAllData.link}/${new Date(web1FeedItemWithAllData.pubDate).toUTCString()}${web1FeedItemWithAllData.categories[0]}${web1FeedItemWithAllData.categories[1]}${web1FeedItemWithAllData.author}${web1FeedItemWithAllData.comments}${web1FeedItemWithAllData.source.title}`; +const validXmlResultWithAllData = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithAllData.title}]]>${site}${web1FeedItemWithAllData.link}/${site}${web1FeedItemWithAllData.link}/${new Date(web1FeedItemWithAllData.pubDate).toUTCString()}${web1FeedItemWithAllData.categories[0]}${web1FeedItemWithAllData.categories[1]}${web1FeedItemWithAllData.author}${web1FeedItemWithAllData.comments}${web1FeedItemWithAllData.source.title}`; // prettier-ignore -const validXmlWithCustomDataResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithCustomData.title}]]>${site}${phpFeedItemWithCustomData.link}/${site}${phpFeedItemWithCustomData.link}/${new Date(phpFeedItemWithCustomData.pubDate).toUTCString()}${phpFeedItemWithCustomData.customData}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; +const validXmlWithCustomDataResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithCustomData.title}]]>${site}${phpFeedItemWithCustomData.link}/${site}${phpFeedItemWithCustomData.link}/${new Date(phpFeedItemWithCustomData.pubDate).toUTCString()}${phpFeedItemWithCustomData.customData}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; // prettier-ignore const validXmlWithStylesheet = `<![CDATA[${title}]]>${site}/`; // prettier-ignore From bbc4171e9dec709b1d9ceec464f2ae7e7f568ea7 Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Tue, 4 Apr 2023 21:25:19 +1000 Subject: [PATCH 3/8] Adds new fields to the RSSFeedItem type and docs to the README --- packages/astro-rss/README.md | 10 ++++++++++ packages/astro-rss/src/index.ts | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/astro-rss/README.md b/packages/astro-rss/README.md index 5a0011d1f6b7..cba11a95a0f7 100644 --- a/packages/astro-rss/README.md +++ b/packages/astro-rss/README.md @@ -128,6 +128,16 @@ type RSSFeedItem = { content?: string; /** Append some other XML-valid data to this item */ customData?: string; + /** Categories or tags related to the item */ + categories?: string[]; + /** The item author's email address */ + author?: string; + /** A URL of a page for comments relating to the item */ + comments?: string; + /** The RSS channel that the item came from */ + source?: { url: string, title: string } + /** A media object that belongs to the item */ + enclosure?: { url: string, length: number, type: string } }; ``` diff --git a/packages/astro-rss/src/index.ts b/packages/astro-rss/src/index.ts index 3548ff6c61d6..9f4603286e21 100644 --- a/packages/astro-rss/src/index.ts +++ b/packages/astro-rss/src/index.ts @@ -47,6 +47,16 @@ type RSSFeedItem = { customData?: z.infer['customData']; /** Whether draft or not */ draft?: z.infer['draft']; + /** Categories or tags related to the item */ + categories?: z.infer['categories']; + /** The item author's email address */ + author?: z.infer['author']; + /** A URL of a page for comments related to the item */ + comments?: z.infer['comments']; + /** The RSS channel that the item came from */ + source?: z.infer['source']; + /** A media object that belongs to the item */ + enclosure?: z.infer['enclosure']; }; type ValidatedRSSFeedItem = z.infer; From 0171fe1bbd0e184a8a61baceaa0b86a28168d72c Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Thu, 6 Apr 2023 22:13:43 +1000 Subject: [PATCH 4/8] Improved documentation of RSSFeedItem. Renames comments => commentsUrl --- packages/astro-rss/README.md | 135 ++++++++++++++++++++------ packages/astro-rss/src/index.ts | 6 +- packages/astro-rss/src/schema.ts | 2 +- packages/astro-rss/test/rss.test.js | 2 +- packages/astro-rss/test/test-utils.js | 2 +- 5 files changed, 113 insertions(+), 34 deletions(-) diff --git a/packages/astro-rss/README.md b/packages/astro-rss/README.md index cba11a95a0f7..171b4663b04b 100644 --- a/packages/astro-rss/README.md +++ b/packages/astro-rss/README.md @@ -112,34 +112,7 @@ Type: `RSSFeedItem[] (required)` A list of formatted RSS feed items. See [Astro's RSS items documentation](https://docs.astro.build/en/guides/rss/#generating-items) for usage examples to choose the best option for you. -When providing a formatted RSS item list, see the `RSSFeedItem` type reference below: - -```ts -type RSSFeedItem = { - /** Link to item */ - link: string; - /** Title of item */ - title: string; - /** Publication date of item */ - pubDate: Date; - /** Item description */ - description?: string; - /** Full content of the item, should be valid HTML */ - content?: string; - /** Append some other XML-valid data to this item */ - customData?: string; - /** Categories or tags related to the item */ - categories?: string[]; - /** The item author's email address */ - author?: string; - /** A URL of a page for comments relating to the item */ - comments?: string; - /** The RSS channel that the item came from */ - source?: { url: string, title: string } - /** A media object that belongs to the item */ - enclosure?: { url: string, length: number, type: string } -}; -``` +When providing a formatted RSS item list, see the [`RSSFeedItem` type reference below](#rssfeeditem). ### drafts @@ -212,6 +185,112 @@ export const get = () => rss({ }); ``` +## `RSSFeedItem` + +An `RSSFeedItem` is a single item in the list of items in your feed. It represents a story, with `link`, `title` and `pubDate` fields. There are further optional fields defined below. + +An example feed item might look like: + +```js +const item = { + title: "Alpha Centauri: so close you can touch it", + link: "/blog/alpha-centuari", + pubDate: new Date("2023-06-04"), + description: "Alpha Centauri is a triple star system, containing Proxima Centauri, the closest star to our sun at only 4.24 light-years away.", + categories: ["stars", "space"] +} +``` + +### `title` + +Type: `string (required)` + +The `` attribute of the item in the feed. + +### `link` + +Type: `string (required)` + +The `<link>` attribute of the item in the feed containing the URL of the item on the web. + +### `pubDate` + +Type: `Date (required)` + +Indicates when the item was published. + +### `description` + +Type: `string (optional)` + +If the item is complete within itself, that is you are publishing the full content of the item in the feed, the `description` field may contain the full text (entity-encoded HTML is permitted). If the item is a stub, then the `description` may contain a synopsis of the item. + +### `content` + +Type: `string (optional)` + +If you want to supply both a short description and also the full content in an item, set the `content` field to the full, encoded text. See the [recommendations from the RSS spec for how to use and differentiate between `description` and `content`](https://www.rssboard.org/rss-profile#namespace-elements-content-encoded). + +### `categories` + +Type: `string[] (optional)` + +If you use tags or categories to categorize your content, you can add them as the `categories` field. They will be output as multiple `<category>` elements. + +### `author` + +Type: `string (optional)` + +Useful for multi-author blogs, the `author` field provides the email address of the person who wrote the item. + +### `commentsUrl` + +Type: `string (optional)` + +The `commentsUrl` defines a URL of a web page that contains comments on the item. + +### `source` + +Type: `object (optional)` + +Items that are republished from other publications may define a `source` which defines the `title` and `url` of the original feed in which it was published. + +#### `title` + +Type: `string (required)` + +If you define a `source` you must define that source's `title`. It is the name of the original feed in which the item was published. + +#### `url` + +Type: `string (required)` + +If you define a `source` you must also define that source's `url` which identifies the URL of the original feed in which the item was published. + +### `enclosure` + +Type: `object (optional)` + +Items that include media as part of the feed, like a podcast, can define an `enclosure` which is made of three required fields, a `url`, `length`, and `type`. + +#### `url` + +Type: `string (required)` + +The `url` field for the `enclosure` defines a URL where the media can be found. + +#### `length` + +Type: `number (required)` + +The `length` field defines the size of the file found at the `url` in bytes. + +#### `type` + +Type: `string (required)` + +The `type` field defines the MIME type for the media item found at the `url`. + ## `rssSchema` When using content collections, you can configure your collection schema to enforce expected [`RSSFeedItem`](#items) properties. Import and apply `rssSchema` to ensure that each collection entry produces a valid RSS feed item: diff --git a/packages/astro-rss/src/index.ts b/packages/astro-rss/src/index.ts index 9f4603286e21..b048d6986332 100644 --- a/packages/astro-rss/src/index.ts +++ b/packages/astro-rss/src/index.ts @@ -52,7 +52,7 @@ type RSSFeedItem = { /** The item author's email address */ author?: z.infer<typeof rssSchema>['author']; /** A URL of a page for comments related to the item */ - comments?: z.infer<typeof rssSchema>['comments']; + commentsUrl?: z.infer<typeof rssSchema>['commentsUrl']; /** The RSS channel that the item came from */ source?: z.infer<typeof rssSchema>['source']; /** A media object that belongs to the item */ @@ -228,8 +228,8 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<string> { if (typeof result.author === 'string') { item.author = result.author; } - if (typeof result.comments === 'string') { - item.comments = result.comments; + if (typeof result.commentsUrl === 'string') { + item.comments = result.commentsUrl; } if (result.source) { item.source = parser.parse( diff --git a/packages/astro-rss/src/schema.ts b/packages/astro-rss/src/schema.ts index 973ae9d7e5af..66b9d41281fe 100644 --- a/packages/astro-rss/src/schema.ts +++ b/packages/astro-rss/src/schema.ts @@ -8,7 +8,7 @@ export const rssSchema = z.object({ draft: z.boolean().optional(), categories: z.array(z.string()).optional(), author: z.string().optional(), - comments: z.string().url().optional(), + commentsUrl: z.string().url().optional(), source: z.object({ url: z.string().url(), title: z.string() }).optional(), enclosure: z .object({ diff --git a/packages/astro-rss/test/rss.test.js b/packages/astro-rss/test/rss.test.js index ba6a8055d876..6258e63d1c6c 100644 --- a/packages/astro-rss/test/rss.test.js +++ b/packages/astro-rss/test/rss.test.js @@ -26,7 +26,7 @@ const validXmlWithoutWeb1FeedResult = `<?xml version="1.0" encoding="UTF-8"?><rs // prettier-ignore const validXmlWithContentResult = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title><![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithContent.title}]]>${site}${phpFeedItemWithContent.link}/${site}${phpFeedItemWithContent.link}/${new Date(phpFeedItemWithContent.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; // prettier-ignore -const validXmlResultWithAllData = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithAllData.title}]]>${site}${web1FeedItemWithAllData.link}/${site}${web1FeedItemWithAllData.link}/${new Date(web1FeedItemWithAllData.pubDate).toUTCString()}${web1FeedItemWithAllData.categories[0]}${web1FeedItemWithAllData.categories[1]}${web1FeedItemWithAllData.author}${web1FeedItemWithAllData.comments}${web1FeedItemWithAllData.source.title}`; +const validXmlResultWithAllData = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithAllData.title}]]>${site}${web1FeedItemWithAllData.link}/${site}${web1FeedItemWithAllData.link}/${new Date(web1FeedItemWithAllData.pubDate).toUTCString()}${web1FeedItemWithAllData.categories[0]}${web1FeedItemWithAllData.categories[1]}${web1FeedItemWithAllData.author}${web1FeedItemWithAllData.commentsUrl}${web1FeedItemWithAllData.source.title}`; // prettier-ignore const validXmlWithCustomDataResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithCustomData.title}]]>${site}${phpFeedItemWithCustomData.link}/${site}${phpFeedItemWithCustomData.link}/${new Date(phpFeedItemWithCustomData.pubDate).toUTCString()}${phpFeedItemWithCustomData.customData}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; // prettier-ignore diff --git a/packages/astro-rss/test/test-utils.js b/packages/astro-rss/test/test-utils.js index f9bf868ac7d1..79d2a992077d 100644 --- a/packages/astro-rss/test/test-utils.js +++ b/packages/astro-rss/test/test-utils.js @@ -34,7 +34,7 @@ export const web1FeedItemWithAllData = { ...web1FeedItem, categories: ['web1', 'history'], author: 'test@example.com', - comments: 'http://example.com/comments', + commentsUrl: 'http://example.com/comments', source: { url: 'http://example.com/source', title: 'The Web 1.0 blog', From 21b1dcad7b8942f0b2c19df040c22273b6243dcf Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Thu, 6 Apr 2023 22:15:03 +1000 Subject: [PATCH 5/8] Adds link to RSS spec --- packages/astro-rss/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro-rss/README.md b/packages/astro-rss/README.md index 171b4663b04b..f6d54e89d96a 100644 --- a/packages/astro-rss/README.md +++ b/packages/astro-rss/README.md @@ -187,7 +187,7 @@ export const get = () => rss({ ## `RSSFeedItem` -An `RSSFeedItem` is a single item in the list of items in your feed. It represents a story, with `link`, `title` and `pubDate` fields. There are further optional fields defined below. +An `RSSFeedItem` is a single item in the list of items in your feed. It represents a story, with `link`, `title` and `pubDate` fields. There are further optional fields defined below. You can also check the definitions for the fields in the [RSS spec](https://validator.w3.org/feed/docs/rss2.html#ltpubdategtSubelementOfLtitemgt). An example feed item might look like: From e5d24c78d7feee814f24de0acf5e9a0364c4cc02 Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Tue, 18 Apr 2023 17:04:45 +1000 Subject: [PATCH 6/8] Updates docs for astro-rss after feedback Also allows for result.commentsUrl and result.enclosure.url to be relative, using createCanonicalURL function. --- packages/astro-rss/README.md | 67 +++++++++++++++++++-------- packages/astro-rss/src/index.ts | 9 +++- packages/astro-rss/src/schema.ts | 4 +- packages/astro-rss/test/rss.test.js | 2 +- packages/astro-rss/test/test-utils.js | 2 +- 5 files changed, 59 insertions(+), 25 deletions(-) diff --git a/packages/astro-rss/README.md b/packages/astro-rss/README.md index f6d54e89d96a..719f7e25ab5a 100644 --- a/packages/astro-rss/README.md +++ b/packages/astro-rss/README.md @@ -205,13 +205,13 @@ const item = { Type: `string (required)` -The `` attribute of the item in the feed. +The title of the item in the feed. ### `link` Type: `string (required)` -The `<link>` attribute of the item in the feed containing the URL of the item on the web. +The URL of the item on the web. ### `pubDate` @@ -223,73 +223,102 @@ Indicates when the item was published. Type: `string (optional)` -If the item is complete within itself, that is you are publishing the full content of the item in the feed, the `description` field may contain the full text (entity-encoded HTML is permitted). If the item is a stub, then the `description` may contain a synopsis of the item. +A synopsis of your item when you are publishing the full content of the item in the `content` field. The `description` may alternatively be the full content of the item in the feed if you are not using the `content` field (entity-coded HTML is permitted). ### `content` Type: `string (optional)` -If you want to supply both a short description and also the full content in an item, set the `content` field to the full, encoded text. See the [recommendations from the RSS spec for how to use and differentiate between `description` and `content`](https://www.rssboard.org/rss-profile#namespace-elements-content-encoded). +The full text content of the item suitable for presentation as HTML. If used, you should also provide a short article summary in the `description` field. + +See the [recommendations from the RSS spec for how to use and differentiate between `description` and `content`](https://www.rssboard.org/rss-profile#namespace-elements-content-encoded). ### `categories` Type: `string[] (optional)` -If you use tags or categories to categorize your content, you can add them as the `categories` field. They will be output as multiple `<category>` elements. +A list of any tags or categories to categorize your content. They will be output as multiple `<category>` elements. ### `author` Type: `string (optional)` -Useful for multi-author blogs, the `author` field provides the email address of the person who wrote the item. +The email address of the item author. This is useful for indicating the author of a post on multi-author blogs. ### `commentsUrl` Type: `string (optional)` -The `commentsUrl` defines a URL of a web page that contains comments on the item. +The URL of a web page that contains comments on the item. ### `source` Type: `object (optional)` -Items that are republished from other publications may define a `source` which defines the `title` and `url` of the original feed in which it was published. +An object that defines the `title` and `url` of the original feed for items that have been republished from another source. Both are required propeties of `source` for proper attribution. + +```js +const item = { + title: "Alpha Centauri: so close you can touch it", + link: "/blog/alpha-centuari", + pubDate: new Date("2023-06-04"), + description: "Alpha Centauri is a triple star system, containing Proxima Centauri, the closest star to our sun at only 4.24 light-years away.", + source: { + title: "The Galactic Times", + url: "https://galactictimes.space/feed.xml" + } +} +``` -#### `title` +#### `source.title` Type: `string (required)` -If you define a `source` you must define that source's `title`. It is the name of the original feed in which the item was published. +The name of the original feed in which the item was published. (Note that this is the the feed's title, not the individual article title.) -#### `url` +#### `source.url` Type: `string (required)` -If you define a `source` you must also define that source's `url` which identifies the URL of the original feed in which the item was published. +The URL of the original feed in which the item was published. ### `enclosure` Type: `object (optional)` -Items that include media as part of the feed, like a podcast, can define an `enclosure` which is made of three required fields, a `url`, `length`, and `type`. +An object to specify properties for an included media source (e.g. a podcast) with three required values: `url`, `length`, and `type`. + +```js +const item = { + title: "Alpha Centauri: so close you can touch it", + link: "/blog/alpha-centuari", + pubDate: new Date("2023-06-04"), + description: "Alpha Centauri is a triple star system, containing Proxima Centauri, the closest star to our sun at only 4.24 light-years away.", + enclosure: { + url: "/media/alpha-centauri.aac", + length: 124568, + type: "audio/aac" + } +} +``` -#### `url` +#### `enclosure.url` Type: `string (required)` -The `url` field for the `enclosure` defines a URL where the media can be found. +The URL where the media can be found. If the media is hosted outside of your own domain you must provide a full URL. -#### `length` +#### `enclosure.length` Type: `number (required)` -The `length` field defines the size of the file found at the `url` in bytes. +The size of the file found at the `url` in bytes. -#### `type` +#### `enclosure.type` Type: `string (required)` -The `type` field defines the MIME type for the media item found at the `url`. +The [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) for the media item found at the `url`. ## `rssSchema` diff --git a/packages/astro-rss/src/index.ts b/packages/astro-rss/src/index.ts index b048d6986332..c6ce05462c54 100644 --- a/packages/astro-rss/src/index.ts +++ b/packages/astro-rss/src/index.ts @@ -229,7 +229,9 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<string> { item.author = result.author; } if (typeof result.commentsUrl === 'string') { - item.comments = result.commentsUrl; + item.comments = isValidURL(result.commentsUrl) + ? result.commentsUrl + : createCanonicalURL(result.commentsUrl, rssOptions.trailingSlash, site).href; } if (result.source) { item.source = parser.parse( @@ -237,8 +239,11 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<string> { ).source; } if (result.enclosure) { + const enclosureURL = isValidURL(result.enclosure.url) + ? result.enclosure.url + : createCanonicalURL(result.enclosure.url, rssOptions.trailingSlash, site).href; item.enclosure = parser.parse( - `<enclosure url="${result.enclosure.url}" length="${result.enclosure.length}" type="${result.enclosure.type}"/>` + `<enclosure url="${enclosureURL}" length="${result.enclosure.length}" type="${result.enclosure.type}"/>` ).enclosure; } return item; diff --git a/packages/astro-rss/src/schema.ts b/packages/astro-rss/src/schema.ts index 66b9d41281fe..829a4da1e459 100644 --- a/packages/astro-rss/src/schema.ts +++ b/packages/astro-rss/src/schema.ts @@ -8,11 +8,11 @@ export const rssSchema = z.object({ draft: z.boolean().optional(), categories: z.array(z.string()).optional(), author: z.string().optional(), - commentsUrl: z.string().url().optional(), + commentsUrl: z.string().optional(), source: z.object({ url: z.string().url(), title: z.string() }).optional(), enclosure: z .object({ - url: z.string().url(), + url: z.string(), length: z.number().positive().int().finite(), type: z.string(), }) diff --git a/packages/astro-rss/test/rss.test.js b/packages/astro-rss/test/rss.test.js index 6258e63d1c6c..2a7106c0766f 100644 --- a/packages/astro-rss/test/rss.test.js +++ b/packages/astro-rss/test/rss.test.js @@ -26,7 +26,7 @@ const validXmlWithoutWeb1FeedResult = `<?xml version="1.0" encoding="UTF-8"?><rs // prettier-ignore const validXmlWithContentResult = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title><![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithContent.title}]]>${site}${phpFeedItemWithContent.link}/${site}${phpFeedItemWithContent.link}/${new Date(phpFeedItemWithContent.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; // prettier-ignore -const validXmlResultWithAllData = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithAllData.title}]]>${site}${web1FeedItemWithAllData.link}/${site}${web1FeedItemWithAllData.link}/${new Date(web1FeedItemWithAllData.pubDate).toUTCString()}${web1FeedItemWithAllData.categories[0]}${web1FeedItemWithAllData.categories[1]}${web1FeedItemWithAllData.author}${web1FeedItemWithAllData.commentsUrl}${web1FeedItemWithAllData.source.title}`; +const validXmlResultWithAllData = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithAllData.title}]]>${site}${web1FeedItemWithAllData.link}/${site}${web1FeedItemWithAllData.link}/${new Date(web1FeedItemWithAllData.pubDate).toUTCString()}${web1FeedItemWithAllData.categories[0]}${web1FeedItemWithAllData.categories[1]}${web1FeedItemWithAllData.author}${web1FeedItemWithAllData.commentsUrl}${web1FeedItemWithAllData.source.title}`; // prettier-ignore const validXmlWithCustomDataResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithCustomData.title}]]>${site}${phpFeedItemWithCustomData.link}/${site}${phpFeedItemWithCustomData.link}/${new Date(phpFeedItemWithCustomData.pubDate).toUTCString()}${phpFeedItemWithCustomData.customData}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; // prettier-ignore diff --git a/packages/astro-rss/test/test-utils.js b/packages/astro-rss/test/test-utils.js index 79d2a992077d..4f0340333659 100644 --- a/packages/astro-rss/test/test-utils.js +++ b/packages/astro-rss/test/test-utils.js @@ -40,7 +40,7 @@ export const web1FeedItemWithAllData = { title: 'The Web 1.0 blog', }, enclosure: { - url: 'http://example.com/podcast.mp3', + url: '/podcast.mp3', length: 256, type: 'audio/mpeg', }, From 0ea5df3427cea1f70de83178be06e3ff048619f9 Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Wed, 19 Apr 2023 09:03:06 +1000 Subject: [PATCH 7/8] Spelling and grammar fixes --- packages/astro-rss/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/astro-rss/README.md b/packages/astro-rss/README.md index 719f7e25ab5a..43adaad92b6e 100644 --- a/packages/astro-rss/README.md +++ b/packages/astro-rss/README.md @@ -112,7 +112,7 @@ Type: `RSSFeedItem[] (required)` A list of formatted RSS feed items. See [Astro's RSS items documentation](https://docs.astro.build/en/guides/rss/#generating-items) for usage examples to choose the best option for you. -When providing a formatted RSS item list, see the [`RSSFeedItem` type reference below](#rssfeeditem). +When providing a formatted RSS item list, see the [`RSSFeedItem` type reference](#rssfeeditem). ### drafts @@ -187,7 +187,7 @@ export const get = () => rss({ ## `RSSFeedItem` -An `RSSFeedItem` is a single item in the list of items in your feed. It represents a story, with `link`, `title` and `pubDate` fields. There are further optional fields defined below. You can also check the definitions for the fields in the [RSS spec](https://validator.w3.org/feed/docs/rss2.html#ltpubdategtSubelementOfLtitemgt). +An `RSSFeedItem` is a single item in the list of items in your feed. It represents a story, with `link`, `title`, and `pubDate` fields. There are further optional fields defined below. You can also check the definitions for the fields in the [RSS spec](https://validator.w3.org/feed/docs/rss2.html#ltpubdategtSubelementOfLtitemgt). An example feed item might look like: @@ -255,7 +255,7 @@ The URL of a web page that contains comments on the item. Type: `object (optional)` -An object that defines the `title` and `url` of the original feed for items that have been republished from another source. Both are required propeties of `source` for proper attribution. +An object that defines the `title` and `url` of the original feed for items that have been republished from another source. Both are required properties of `source` for proper attribution. ```js const item = { @@ -274,7 +274,7 @@ const item = { Type: `string (required)` -The name of the original feed in which the item was published. (Note that this is the the feed's title, not the individual article title.) +The name of the original feed in which the item was published. (Note that this is the feed's title, not the individual article title.) #### `source.url` From b4daee6e5d462b8c821fab78c3d554bd63cb61d9 Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Wed, 26 Apr 2023 22:21:28 +1000 Subject: [PATCH 8/8] Adds changeset for RSS Items update --- .changeset/quiet-cougars-fix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quiet-cougars-fix.md diff --git a/.changeset/quiet-cougars-fix.md b/.changeset/quiet-cougars-fix.md new file mode 100644 index 000000000000..b5a99c0f536a --- /dev/null +++ b/.changeset/quiet-cougars-fix.md @@ -0,0 +1,5 @@ +--- +'@astrojs/rss': minor +--- + +Added extra elements to the RSS items, including categories and enclosure