Fix pre-generated RSS URLs (#2443)

* Allow pre-generated urls to be passed in rss feeds

* Fix variable name

* Add isValidURL helper function

* Remove scary RegEx and tidy up code

* add test for using pregenerated urls

* fix: allow rss to be called multiple times

* test: normalize rss feed in test

* chore: add changeset

Co-authored-by: Zade Viggers <74938858+zadeviggers@users.noreply.github.com>
Co-authored-by: zadeviggers <zade.viggers@gmail.com>
This commit is contained in:
Nate Moore 2022-01-21 16:38:48 -06:00 committed by GitHub
parent 31b16fcac1
commit ed0b46f96f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 75 additions and 25 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix bug with RSS feed generation. `rss()` can now be called multiple times and URLs can now be fully qualified.

View file

@ -78,26 +78,31 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
debug(logging, 'build', `├── ${colors.bold(colors.red('✗'))} ${route.component}`);
throw err;
});
if (result.rss?.xml) {
const { url, content } = result.rss.xml;
if (content) {
const rssFile = new URL(url.replace(/^\/?/, './'), astroConfig.dist);
if (assets[fileURLToPath(rssFile)]) {
throw new Error(`[getStaticPaths] RSS feed ${url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`);
if (result.rss?.length) {
for (let i = 0; i < result.rss.length; i++) {
const rss = result.rss[i];
if (rss.xml) {
const { url, content } = rss.xml;
if (content) {
const rssFile = new URL(url.replace(/^\/?/, './'), astroConfig.dist);
if (assets[fileURLToPath(rssFile)]) {
throw new Error(`[getStaticPaths] RSS feed ${url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`);
}
assets[fileURLToPath(rssFile)] = content;
}
}
assets[fileURLToPath(rssFile)] = content;
if (rss.xsl?.content) {
const { url, content } = rss.xsl;
const stylesheetFile = new URL(url.replace(/^\/?/, './'), astroConfig.dist);
if (assets[fileURLToPath(stylesheetFile)]) {
throw new Error(
`[getStaticPaths] RSS feed stylesheet ${url} already exists.\nUse \`rss(data, {stylesheet: '...'})\` to choose a unique, custom URL. (${route.component})`
);
}
assets[fileURLToPath(stylesheetFile)] = content;
}
}
}
if (result.rss?.xsl?.content) {
const { url, content } = result.rss.xsl;
const stylesheetFile = new URL(url.replace(/^\/?/, './'), astroConfig.dist);
if (assets[fileURLToPath(stylesheetFile)]) {
throw new Error(
`[getStaticPaths] RSS feed stylesheet ${url} already exists.\nUse \`rss(data, {stylesheet: '...'})\` to choose a unique, custom URL. (${route.component})`
);
}
assets[fileURLToPath(stylesheetFile)] = content;
}
allPages[route.component] = {
route,
paths: result.paths,
@ -119,7 +124,7 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
return { assets, allPages };
}
async function getStaticPathsForRoute(opts: CollectPagesDataOptions, route: RouteData): Promise<{ paths: string[]; rss?: RSSResult }> {
async function getStaticPathsForRoute(opts: CollectPagesDataOptions, route: RouteData): Promise<{ paths: string[]; rss?: RSSResult[] }> {
const { astroConfig, logging, routeCache, viteServer } = opts;
if (!viteServer) throw new Error(`vite.createServer() not called!`);
const filePath = new URL(`./${route.component}`, astroConfig.projectRoot);

View file

@ -1,7 +1,7 @@
import type { RSSFunction, RSS, RSSResult, FeedResult, RouteData } from '../../@types/astro';
import { XMLValidator } from 'fast-xml-parser';
import { canonicalURL, PRETTY_FEED_V3 } from '../util.js';
import { canonicalURL, isValidURL, PRETTY_FEED_V3 } from '../util.js';
/** Validates getStaticPaths.rss */
export function validateRSS(args: GenerateRSSArgs): void {
@ -48,8 +48,10 @@ export function generateRSS(args: GenerateRSSArgs): string {
if (!result.title) throw new Error(`[${srcFile}] rss.items required "title" property is missing. got: "${JSON.stringify(result)}"`);
if (!result.link) throw new Error(`[${srcFile}] rss.items required "link" property is missing. got: "${JSON.stringify(result)}"`);
xml += `<title><![CDATA[${result.title}]]></title>`;
xml += `<link>${canonicalURL(result.link, site).href}</link>`;
xml += `<guid>${canonicalURL(result.link, site).href}</guid>`;
// If the item's link is already a valid URL, don't mess with it.
const itemLink = isValidURL(result.link) ? result.link : canonicalURL(result.link, site).href;
xml += `<link>${itemLink}</link>`;
xml += `<guid>${itemLink}</guid>`;
if (result.description) xml += `<description><![CDATA[${result.description}]]></description>`;
if (result.pubDate) {
// note: this should be a Date, but if user provided a string or number, we can work with that, too.
@ -81,13 +83,14 @@ export function generateRSSStylesheet() {
}
/** Generated function to be run */
export function generateRssFunction(site: string | undefined, route: RouteData): { generator: RSSFunction; rss?: RSSResult } {
let result: RSSResult = {} as any;
export function generateRssFunction(site: string | undefined, route: RouteData): { generator: RSSFunction; rss?: RSSResult[] } {
let results: RSSResult[] = [];
return {
generator: function rssUtility(args: RSS) {
if (!site) {
throw new Error(`[${route.component}] rss() tried to generate RSS but "buildOptions.site" missing in astro.config.mjs`);
}
let result: RSSResult = {} as any;
const { dest, ...rssData } = args;
const feedURL = dest || '/rss.xml';
if (rssData.stylesheet === true) {
@ -105,7 +108,8 @@ export function generateRssFunction(site: string | undefined, route: RouteData):
url: feedURL,
content: generateRSS({ rssData, site, srcFile: route.component, feedURL }),
};
results.push(result);
},
rss: result,
rss: results,
};
}

View file

@ -15,6 +15,15 @@ export function canonicalURL(url: string, base?: string): URL {
return new URL(pathname, base);
}
/** Check if a URL is already valid */
export function isValidURL(url: string):boolean {
try {
new URL(url)
return true;
} catch (e) {}
return false;
}
/** is a specifier an npm package? */
export function parseNpmName(spec: string): { scope?: string; name: string; subpath?: string } | undefined {
// not an npm package

View file

@ -22,11 +22,18 @@ describe('Sitemaps', () => {
const rss = await fixture.readFile('/custom/feed.xml');
expect(rss).to.equal(
`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title><![CDATA[MF Doomcast]]></title><description><![CDATA[The podcast about the things you find on a picnic, or at a picnic table]]></description><link>https://astro.build/custom/feed.xml</link><language>en-us</language><itunes:author>MF Doom</itunes:author><item><title><![CDATA[Rap Snitch Knishes (feat. Mr. Fantastik)]]></title><link>https://astro.build/episode/rap-snitch-knishes/</link><guid>https://astro.build/episode/rap-snitch-knishes/</guid><description><![CDATA[Complex named this song the “22nd funniest rap song of all time.”]]></description><pubDate>Tue, 16 Nov 2004 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>172</itunes:duration><itunes:explicit>true</itunes:explicit></item><item><title><![CDATA[Fazers]]></title><link>https://astro.build/episode/fazers/</link><guid>https://astro.build/episode/fazers/</guid><description><![CDATA[Rhapsody ranked Take Me to Your Leader 17th on its list “Hip-Hops Best Albums of the Decade”]]></description><pubDate>Thu, 03 Jul 2003 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>197</itunes:duration><itunes:explicit>true</itunes:explicit></item><item><title><![CDATA[Rhymes Like Dimes (feat. Cucumber Slice)]]></title><link>https://astro.build/episode/rhymes-like-dimes/</link><guid>https://astro.build/episode/rhymes-like-dimes/</guid><description><![CDATA[Operation: Doomsday has been heralded as an underground classic that established MF Doom's rank within the underground hip-hop scene during the early to mid-2000s.
]]></description><pubDate>Tue, 19 Oct 1999 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>259</itunes:duration><itunes:explicit>true</itunes:explicit></item></channel></rss>`
);
});
it('generates RSS with pregenerated URLs correctly', async () => {
const rss = await fixture.readFile('/custom/feed-pregenerated-urls.xml');
expect(rss).to.equal(
`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title><![CDATA[MF Doomcast]]></title><description><![CDATA[The podcast about the things you find on a picnic, or at a picnic table]]></description><link>https://astro.build/custom/feed-pregenerated-urls.xml</link><language>en-us</language><itunes:author>MF Doom</itunes:author><item><title><![CDATA[Rap Snitch Knishes (feat. Mr. Fantastik)]]></title><link>https://example.com/episode/rap-snitch-knishes/</link><guid>https://example.com/episode/rap-snitch-knishes/</guid><description><![CDATA[Complex named this song the “22nd funniest rap song of all time.”]]></description><pubDate>Tue, 16 Nov 2004 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>172</itunes:duration><itunes:explicit>true</itunes:explicit></item><item><title><![CDATA[Fazers]]></title><link>https://example.com/episode/fazers/</link><guid>https://example.com/episode/fazers/</guid><description><![CDATA[Rhapsody ranked Take Me to Your Leader 17th on its list “Hip-Hops Best Albums of the Decade”]]></description><pubDate>Thu, 03 Jul 2003 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>197</itunes:duration><itunes:explicit>true</itunes:explicit></item><item><title><![CDATA[Rhymes Like Dimes (feat. Cucumber Slice)]]></title><link>https://example.com/episode/rhymes-like-dimes/</link><guid>https://example.com/episode/rhymes-like-dimes/</guid><description><![CDATA[Operation: Doomsday has been heralded as an underground classic that established MF Doom's rank within the underground hip-hop scene during the early to mid-2000s.
]]></description><pubDate>Tue, 19 Oct 1999 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>259</itunes:duration><itunes:explicit>true</itunes:explicit></item></channel></rss>`
);
});
});
describe('Sitemap Generation', () => {
it('Generates Sitemap correctly', async () => {
let sitemap = await fixture.readFile('/sitemap.xml');

View file

@ -21,6 +21,26 @@ export function getStaticPaths({paginate, rss}) {
})),
dest: '/custom/feed.xml',
});
rss({
title: 'MF Doomcast',
description: 'The podcast about the things you find on a picnic, or at a picnic table',
xmlns: {
itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd',
content: 'http://purl.org/rss/1.0/modules/content/',
},
customData: `<language>en-us</language>` +
`<itunes:author>MF Doom</itunes:author>`,
items: episodes.map((episode) => ({
title: episode.title,
link: `https://example.com${episode.url}/`,
description: episode.description,
pubDate: episode.pubDate + 'Z',
customData: `<itunes:episodeType>${episode.type}</itunes:episodeType>` +
`<itunes:duration>${episode.duration}</itunes:duration>` +
`<itunes:explicit>${episode.explicit || false}</itunes:explicit>`,
})),
dest: '/custom/feed-pregenerated-urls.xml',
});
return paginate(episodes);
}