summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/quiet-cougars-fix.md5
-rw-r--r--packages/astro-rss/README.md154
-rw-r--r--packages/astro-rss/src/index.ts37
-rw-r--r--packages/astro-rss/src/schema.ts11
-rw-r--r--packages/astro-rss/test/rss.test.js22
-rw-r--r--packages/astro-rss/test/test-utils.js15
6 files changed, 221 insertions, 23 deletions
diff --git a/.changeset/quiet-cougars-fix.md b/.changeset/quiet-cougars-fix.md
new file mode 100644
index 000000000..b5a99c0f5
--- /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
diff --git a/packages/astro-rss/README.md b/packages/astro-rss/README.md
index 5a0011d1f..43adaad92 100644
--- a/packages/astro-rss/README.md
+++ b/packages/astro-rss/README.md
@@ -112,24 +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;
-};
-```
+When providing a formatted RSS item list, see the [`RSSFeedItem` type reference](#rssfeeditem).
### drafts
@@ -202,6 +185,141 @@ 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 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 title of the item in the feed.
+
+### `link`
+
+Type: `string (required)`
+
+The URL of the item on the web.
+
+### `pubDate`
+
+Type: `Date (required)`
+
+Indicates when the item was published.
+
+### `description`
+
+Type: `string (optional)`
+
+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)`
+
+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)`
+
+A list of any tags or categories to categorize your content. They will be output as multiple `<category>` elements.
+
+### `author`
+
+Type: `string (optional)`
+
+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 URL of a web page that contains comments on the item.
+
+### `source`
+
+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 properties 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"
+ }
+}
+```
+
+#### `source.title`
+
+Type: `string (required)`
+
+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`
+
+Type: `string (required)`
+
+The URL of the original feed in which the item was published.
+
+### `enclosure`
+
+Type: `object (optional)`
+
+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"
+ }
+}
+```
+
+#### `enclosure.url`
+
+Type: `string (required)`
+
+The URL where the media can be found. If the media is hosted outside of your own domain you must provide a full URL.
+
+#### `enclosure.length`
+
+Type: `number (required)`
+
+The size of the file found at the `url` in bytes.
+
+#### `enclosure.type`
+
+Type: `string (required)`
+
+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`
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 9c5908b93..2a35bd573 100644
--- a/packages/astro-rss/src/index.ts
+++ b/packages/astro-rss/src/index.ts
@@ -47,6 +47,16 @@ type RSSFeedItem = {
customData?: z.infer<typeof rssSchema>['customData'];
/** Whether draft or not */
draft?: z.infer<typeof rssSchema>['draft'];
+ /** Categories or tags related to the item */
+ categories?: z.infer<typeof rssSchema>['categories'];
+ /** The item author's email address */
+ author?: z.infer<typeof rssSchema>['author'];
+ /** A URL of a page for comments related to the item */
+ 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 */
+ enclosure?: z.infer<typeof rssSchema>['enclosure'];
};
type ValidatedRSSFeedItem = z.infer<typeof rssFeedItemValidator>;
@@ -148,6 +158,7 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<string> {
// 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 +207,7 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<string> {
const item: any = {
title: result.title,
link: itemLink,
- guid: itemLink,
+ guid: { '#text': itemLink, '@_isPermaLink': 'true' },
};
if (result.description) {
item.description = result.description;
@@ -211,6 +222,30 @@ async function generateRSS(rssOptions: ValidatedRSSOptions): Promise<string> {
if (typeof result.customData === 'string') {
Object.assign(item, parser.parse(`<item>${result.customData}</item>`).item);
}
+ if (Array.isArray(result.categories)) {
+ item.category = result.categories;
+ }
+ if (typeof result.author === 'string') {
+ item.author = result.author;
+ }
+ if (typeof result.commentsUrl === 'string') {
+ item.comments = isValidURL(result.commentsUrl)
+ ? result.commentsUrl
+ : createCanonicalURL(result.commentsUrl, rssOptions.trailingSlash, site).href;
+ }
+ if (result.source) {
+ item.source = parser.parse(
+ `<source url="${result.source.url}">${result.source.title}</source>`
+ ).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="${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 b24a1441f..829a4da1e 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(),
+ commentsUrl: z.string().optional(),
+ source: z.object({ url: z.string().url(), title: z.string() }).optional(),
+ enclosure: z
+ .object({
+ url: z.string(),
+ 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 be4362a34..2a7106c07 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);
@@ -19,13 +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 = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItem.title}]]></title><link>${site}${phpFeedItem.link}/</link><guid>${site}${phpFeedItem.link}/</guid><description><![CDATA[${phpFeedItem.description}]]></description><pubDate>${new Date(phpFeedItem.pubDate).toUTCString()}</pubDate></item><item><title><![CDATA[${web1FeedItem.title}]]></title><link>${site}${web1FeedItem.link}/</link><guid>${site}${web1FeedItem.link}/</guid><description><![CDATA[${web1FeedItem.description}]]></description><pubDate>${new Date(web1FeedItem.pubDate).toUTCString()}</pubDate></item></channel></rss>`;
+const validXmlResult = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItem.title}]]></title><link>${site}${phpFeedItem.link}/</link><guid isPermaLink="true">${site}${phpFeedItem.link}/</guid><description><![CDATA[${phpFeedItem.description}]]></description><pubDate>${new Date(phpFeedItem.pubDate).toUTCString()}</pubDate></item><item><title><![CDATA[${web1FeedItem.title}]]></title><link>${site}${web1FeedItem.link}/</link><guid isPermaLink="true">${site}${web1FeedItem.link}/</guid><description><![CDATA[${web1FeedItem.description}]]></description><pubDate>${new Date(web1FeedItem.pubDate).toUTCString()}</pubDate></item></channel></rss>`;
// prettier-ignore
-const validXmlWithoutWeb1FeedResult = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItem.title}]]></title><link>${site}${phpFeedItem.link}/</link><guid>${site}${phpFeedItem.link}/</guid><description><![CDATA[${phpFeedItem.description}]]></description><pubDate>${new Date(phpFeedItem.pubDate).toUTCString()}</pubDate></item></channel></rss>`;
+const validXmlWithoutWeb1FeedResult = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItem.title}]]></title><link>${site}${phpFeedItem.link}/</link><guid isPermaLink="true">${site}${phpFeedItem.link}/</guid><description><![CDATA[${phpFeedItem.description}]]></description><pubDate>${new Date(phpFeedItem.pubDate).toUTCString()}</pubDate></item></channel></rss>`;
// 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}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItemWithContent.title}]]></title><link>${site}${phpFeedItemWithContent.link}/</link><guid>${site}${phpFeedItemWithContent.link}/</guid><description><![CDATA[${phpFeedItemWithContent.description}]]></description><pubDate>${new Date(phpFeedItemWithContent.pubDate).toUTCString()}</pubDate><content:encoded><![CDATA[${phpFeedItemWithContent.content}]]></content:encoded></item><item><title><![CDATA[${web1FeedItemWithContent.title}]]></title><link>${site}${web1FeedItemWithContent.link}/</link><guid>${site}${web1FeedItemWithContent.link}/</guid><description><![CDATA[${web1FeedItemWithContent.description}]]></description><pubDate>${new Date(web1FeedItemWithContent.pubDate).toUTCString()}</pubDate><content:encoded><![CDATA[${web1FeedItemWithContent.content}]]></content:encoded></item></channel></rss>`;
+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}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItemWithContent.title}]]></title><link>${site}${phpFeedItemWithContent.link}/</link><guid isPermaLink="true">${site}${phpFeedItemWithContent.link}/</guid><description><![CDATA[${phpFeedItemWithContent.description}]]></description><pubDate>${new Date(phpFeedItemWithContent.pubDate).toUTCString()}</pubDate><content:encoded><![CDATA[${phpFeedItemWithContent.content}]]></content:encoded></item><item><title><![CDATA[${web1FeedItemWithContent.title}]]></title><link>${site}${web1FeedItemWithContent.link}/</link><guid isPermaLink="true">${site}${web1FeedItemWithContent.link}/</guid><description><![CDATA[${web1FeedItemWithContent.description}]]></description><pubDate>${new Date(web1FeedItemWithContent.pubDate).toUTCString()}</pubDate><content:encoded><![CDATA[${web1FeedItemWithContent.content}]]></content:encoded></item></channel></rss>`;
// prettier-ignore
-const validXmlWithCustomDataResult = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItemWithCustomData.title}]]></title><link>${site}${phpFeedItemWithCustomData.link}/</link><guid>${site}${phpFeedItemWithCustomData.link}/</guid><description><![CDATA[${phpFeedItemWithCustomData.description}]]></description><pubDate>${new Date(phpFeedItemWithCustomData.pubDate).toUTCString()}</pubDate>${phpFeedItemWithCustomData.customData}</item><item><title><![CDATA[${web1FeedItemWithContent.title}]]></title><link>${site}${web1FeedItemWithContent.link}/</link><guid>${site}${web1FeedItemWithContent.link}/</guid><description><![CDATA[${web1FeedItemWithContent.description}]]></description><pubDate>${new Date(web1FeedItemWithContent.pubDate).toUTCString()}</pubDate><content:encoded><![CDATA[${web1FeedItemWithContent.content}]]></content:encoded></item></channel></rss>`;
+const validXmlResultWithAllData = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItem.title}]]></title><link>${site}${phpFeedItem.link}/</link><guid isPermaLink="true">${site}${phpFeedItem.link}/</guid><description><![CDATA[${phpFeedItem.description}]]></description><pubDate>${new Date(phpFeedItem.pubDate).toUTCString()}</pubDate></item><item><title><![CDATA[${web1FeedItemWithAllData.title}]]></title><link>${site}${web1FeedItemWithAllData.link}/</link><guid isPermaLink="true">${site}${web1FeedItemWithAllData.link}/</guid><description><![CDATA[${web1FeedItemWithAllData.description}]]></description><pubDate>${new Date(web1FeedItemWithAllData.pubDate).toUTCString()}</pubDate><category>${web1FeedItemWithAllData.categories[0]}</category><category>${web1FeedItemWithAllData.categories[1]}</category><author>${web1FeedItemWithAllData.author}</author><comments>${web1FeedItemWithAllData.commentsUrl}</comments><source url="${web1FeedItemWithAllData.source.url}">${web1FeedItemWithAllData.source.title}</source><enclosure url="${site}${web1FeedItemWithAllData.enclosure.url}" length="${web1FeedItemWithAllData.enclosure.length}" type="${web1FeedItemWithAllData.enclosure.type}"/></item></channel></rss>`;
+// prettier-ignore
+const validXmlWithCustomDataResult = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItemWithCustomData.title}]]></title><link>${site}${phpFeedItemWithCustomData.link}/</link><guid isPermaLink="true">${site}${phpFeedItemWithCustomData.link}/</guid><description><![CDATA[${phpFeedItemWithCustomData.description}]]></description><pubDate>${new Date(phpFeedItemWithCustomData.pubDate).toUTCString()}</pubDate>${phpFeedItemWithCustomData.customData}</item><item><title><![CDATA[${web1FeedItemWithContent.title}]]></title><link>${site}${web1FeedItemWithContent.link}/</link><guid isPermaLink="true">${site}${web1FeedItemWithContent.link}/</guid><description><![CDATA[${web1FeedItemWithContent.description}]]></description><pubDate>${new Date(web1FeedItemWithContent.pubDate).toUTCString()}</pubDate><content:encoded><![CDATA[${web1FeedItemWithContent.content}]]></content:encoded></item></channel></rss>`;
// prettier-ignore
const validXmlWithStylesheet = `<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/feedstylesheet.css"?><rss version="2.0"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link></channel></rss>`;
// prettier-ignore
@@ -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 37f95214b..4f0340333 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: `<h1>${web1FeedItem.title}</h1><p>${web1FeedItem.description}</p>`,
};
+export const web1FeedItemWithAllData = {
+ ...web1FeedItem,
+ categories: ['web1', 'history'],
+ author: 'test@example.com',
+ commentsUrl: 'http://example.com/comments',
+ source: {
+ url: 'http://example.com/source',
+ title: 'The Web 1.0 blog',
+ },
+ enclosure: {
+ url: '/podcast.mp3',
+ length: 256,
+ type: 'audio/mpeg',
+ },
+};