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:
parent
31b16fcac1
commit
ed0b46f96f
6 changed files with 75 additions and 25 deletions
5
.changeset/flat-mayflies-ring.md
Normal file
5
.changeset/flat-mayflies-ring.md
Normal 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.
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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-Hop’s 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-Hop’s 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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue