diff options
Diffstat (limited to 'packages/astro-rss/src')
-rw-r--r-- | packages/astro-rss/src/index.ts | 67 |
1 files changed, 34 insertions, 33 deletions
diff --git a/packages/astro-rss/src/index.ts b/packages/astro-rss/src/index.ts index bb6afd562..8779ac007 100644 --- a/packages/astro-rss/src/index.ts +++ b/packages/astro-rss/src/index.ts @@ -1,4 +1,4 @@ -import { XMLValidator } from 'fast-xml-parser'; +import { XMLBuilder, XMLParser } from 'fast-xml-parser'; import { createCanonicalURL, isValidURL } from './util.js'; type GlobResult = Record<string, () => Promise<{ [key: string]: any }>>; @@ -100,15 +100,17 @@ export default async function getRSS(rssOptions: RSSOptions) { /** Generate RSS 2.0 feed */ export async function generateRSS({ rssOptions, items }: GenerateRSSArgs): Promise<string> { const { site } = rssOptions; - let xml = `<?xml version="1.0" encoding="UTF-8"?>`; + const xmlOptions = { ignoreAttributes: false }; + const parser = new XMLParser(xmlOptions); + const root: any = { '?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' } }; if (typeof rssOptions.stylesheet === 'string') { - xml += `<?xml-stylesheet href="${rssOptions.stylesheet}" type="text/xsl"?>`; + root['?xml-stylesheet'] = { '@_href': rssOptions.stylesheet, '@_encoding': 'UTF-8' }; } - xml += `<rss version="2.0"`; + root.rss = { '@_version': '2.0' }; if (items.find((result) => result.content)) { // the namespace to be added to the xmlns:content attribute to enable the <content> RSS feature const XMLContentNamespace = 'http://purl.org/rss/1.0/modules/content/'; - xml += ` xmlns:content="${XMLContentNamespace}"`; + root.rss['@_xmlns:content'] = XMLContentNamespace; // Ensure that the user hasn't tried to manually include the necessary namespace themselves if (rssOptions.xmlns?.content && rssOptions.xmlns.content === XMLContentNamespace) { delete rssOptions.xmlns.content; @@ -118,29 +120,36 @@ export async function generateRSS({ rssOptions, items }: GenerateRSSArgs): Promi // xmlns if (rssOptions.xmlns) { for (const [k, v] of Object.entries(rssOptions.xmlns)) { - xml += ` xmlns:${k}="${v}"`; + root.rss[`@_xmlns:${k}`] = v; } } - xml += `>`; - xml += `<channel>`; // title, description, customData - xml += `<title><![CDATA[${rssOptions.title}]]></title>`; - xml += `<description><![CDATA[${rssOptions.description}]]></description>`; - xml += `<link>${createCanonicalURL(site).href}</link>`; - if (typeof rssOptions.customData === 'string') xml += rssOptions.customData; + root.rss.channel = { + title: rssOptions.title, + description: rssOptions.description, + link: createCanonicalURL(site).href, + }; + if (typeof rssOptions.customData === 'string') + Object.assign( + root.rss.channel, + parser.parse(`<channel>${rssOptions.customData}</channel>`).channel + ); // items - for (const result of items) { + root.rss.channel.item = items.map((result) => { validate(result); - xml += `<item>`; - xml += `<title><![CDATA[${result.title}]]></title>`; // If the item's link is already a valid URL, don't mess with it. const itemLink = isValidURL(result.link) ? result.link : createCanonicalURL(result.link, site).href; - xml += `<link>${itemLink}</link>`; - xml += `<guid>${itemLink}</guid>`; - if (result.description) xml += `<description><![CDATA[${result.description}]]></description>`; + const item: any = { + title: result.title, + link: itemLink, + guid: itemLink, + }; + if (result.description) { + item.description = result.description; + } if (result.pubDate) { // note: this should be a Date, but if user provided a string or number, we can work with that, too. if (typeof result.pubDate === 'number' || typeof result.pubDate === 'string') { @@ -148,26 +157,18 @@ export async function generateRSS({ rssOptions, items }: GenerateRSSArgs): Promi } else if (result.pubDate instanceof Date === false) { throw new Error('[${filename}] rss.item().pubDate must be a Date'); } - xml += `<pubDate>${result.pubDate.toUTCString()}</pubDate>`; + item.pubDate = result.pubDate.toUTCString(); } // include the full content of the post if the user supplies it if (typeof result.content === 'string') { - xml += `<content:encoded><![CDATA[${result.content}]]></content:encoded>`; + item['content:encoded'] = result.content; } - if (typeof result.customData === 'string') xml += result.customData; - xml += `</item>`; - } - - xml += `</channel></rss>`; - - // validate user’s inputs to see if it’s valid XML - const isValid = XMLValidator.validate(xml); - if (isValid !== true) { - // If valid XML, isValid will be `true`. Otherwise, this will be an error object. Throw. - throw new Error(isValid as any); - } + if (typeof rssOptions.customData === 'string') + Object.assign(item, parser.parse(`<item>${rssOptions.customData}</item>`).item); + return item; + }); - return xml; + return new XMLBuilder(xmlOptions).build(root); } const requiredFields = Object.freeze(['link', 'title']); |