mirror of
https://github.com/withastro/astro.git
synced 2024-12-23 21:53:55 -05:00
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}`);
|
debug(logging, 'build', `├── ${colors.bold(colors.red('✗'))} ${route.component}`);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
if (result.rss?.xml) {
|
if (result.rss?.length) {
|
||||||
const { url, content } = result.rss.xml;
|
for (let i = 0; i < result.rss.length; i++) {
|
||||||
if (content) {
|
const rss = result.rss[i];
|
||||||
const rssFile = new URL(url.replace(/^\/?/, './'), astroConfig.dist);
|
if (rss.xml) {
|
||||||
if (assets[fileURLToPath(rssFile)]) {
|
const { url, content } = rss.xml;
|
||||||
throw new Error(`[getStaticPaths] RSS feed ${url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`);
|
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] = {
|
allPages[route.component] = {
|
||||||
route,
|
route,
|
||||||
paths: result.paths,
|
paths: result.paths,
|
||||||
|
@ -119,7 +124,7 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
|
||||||
return { assets, allPages };
|
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;
|
const { astroConfig, logging, routeCache, viteServer } = opts;
|
||||||
if (!viteServer) throw new Error(`vite.createServer() not called!`);
|
if (!viteServer) throw new Error(`vite.createServer() not called!`);
|
||||||
const filePath = new URL(`./${route.component}`, astroConfig.projectRoot);
|
const filePath = new URL(`./${route.component}`, astroConfig.projectRoot);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { RSSFunction, RSS, RSSResult, FeedResult, RouteData } from '../../@types/astro';
|
import type { RSSFunction, RSS, RSSResult, FeedResult, RouteData } from '../../@types/astro';
|
||||||
|
|
||||||
import { XMLValidator } from 'fast-xml-parser';
|
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 */
|
/** Validates getStaticPaths.rss */
|
||||||
export function validateRSS(args: GenerateRSSArgs): void {
|
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.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)}"`);
|
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 += `<title><![CDATA[${result.title}]]></title>`;
|
||||||
xml += `<link>${canonicalURL(result.link, site).href}</link>`;
|
// If the item's link is already a valid URL, don't mess with it.
|
||||||
xml += `<guid>${canonicalURL(result.link, site).href}</guid>`;
|
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.description) xml += `<description><![CDATA[${result.description}]]></description>`;
|
||||||
if (result.pubDate) {
|
if (result.pubDate) {
|
||||||
// note: this should be a Date, but if user provided a string or number, we can work with that, too.
|
// 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 */
|
/** Generated function to be run */
|
||||||
export function generateRssFunction(site: string | undefined, route: RouteData): { generator: RSSFunction; rss?: RSSResult } {
|
export function generateRssFunction(site: string | undefined, route: RouteData): { generator: RSSFunction; rss?: RSSResult[] } {
|
||||||
let result: RSSResult = {} as any;
|
let results: RSSResult[] = [];
|
||||||
return {
|
return {
|
||||||
generator: function rssUtility(args: RSS) {
|
generator: function rssUtility(args: RSS) {
|
||||||
if (!site) {
|
if (!site) {
|
||||||
throw new Error(`[${route.component}] rss() tried to generate RSS but "buildOptions.site" missing in astro.config.mjs`);
|
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 { dest, ...rssData } = args;
|
||||||
const feedURL = dest || '/rss.xml';
|
const feedURL = dest || '/rss.xml';
|
||||||
if (rssData.stylesheet === true) {
|
if (rssData.stylesheet === true) {
|
||||||
|
@ -105,7 +108,8 @@ export function generateRssFunction(site: string | undefined, route: RouteData):
|
||||||
url: feedURL,
|
url: feedURL,
|
||||||
content: generateRSS({ rssData, site, srcFile: route.component, 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);
|
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? */
|
/** is a specifier an npm package? */
|
||||||
export function parseNpmName(spec: string): { scope?: string; name: string; subpath?: string } | undefined {
|
export function parseNpmName(spec: string): { scope?: string; name: string; subpath?: string } | undefined {
|
||||||
// not an npm package
|
// not an npm package
|
||||||
|
|
|
@ -22,11 +22,18 @@ describe('Sitemaps', () => {
|
||||||
const rss = await fixture.readFile('/custom/feed.xml');
|
const rss = await fixture.readFile('/custom/feed.xml');
|
||||||
expect(rss).to.equal(
|
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.
|
`<?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>`
|
]]></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', () => {
|
describe('Sitemap Generation', () => {
|
||||||
it('Generates Sitemap correctly', async () => {
|
it('Generates Sitemap correctly', async () => {
|
||||||
let sitemap = await fixture.readFile('/sitemap.xml');
|
let sitemap = await fixture.readFile('/sitemap.xml');
|
||||||
|
|
|
@ -21,6 +21,26 @@ export function getStaticPaths({paginate, rss}) {
|
||||||
})),
|
})),
|
||||||
dest: '/custom/feed.xml',
|
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);
|
return paginate(episodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue