feat: add support for styled RSS feeds (#2371)
This commit is contained in:
parent
04c2e2e4cd
commit
85ad1aab67
6 changed files with 170 additions and 12 deletions
5
.changeset/five-crabs-sparkle.md
Normal file
5
.changeset/five-crabs-sparkle.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for styled RSS feeds using the new `stylesheet` option
|
|
@ -4,7 +4,7 @@ title: RSS
|
||||||
description: An intro to RSS in Astro
|
description: An intro to RSS in Astro
|
||||||
---
|
---
|
||||||
|
|
||||||
Astro supports fast, automatic RSS feed generation for blogs and other content websites.
|
Astro supports fast, automatic RSS feed generation for blogs and other content websites. For more information about RSS feeds in general, see [aboutfeeds.com](https://aboutfeeds.com/).
|
||||||
|
|
||||||
You can create an RSS feed from any Astro page that uses a `getStaticPaths()` function for routing. Only dynamic routes can use `getStaticPaths()` today (see [Routing](/en/core-concepts/routing)).
|
You can create an RSS feed from any Astro page that uses a `getStaticPaths()` function for routing. Only dynamic routes can use `getStaticPaths()` today (see [Routing](/en/core-concepts/routing)).
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ export async function getStaticPaths({rss}) {
|
||||||
rss({
|
rss({
|
||||||
// The RSS Feed title, description, and custom metadata.
|
// The RSS Feed title, description, and custom metadata.
|
||||||
title: 'Don\'s Blog',
|
title: 'Don\'s Blog',
|
||||||
|
// See "Styling" section below
|
||||||
|
stylesheet: true,
|
||||||
description: 'An example blog on Astro',
|
description: 'An example blog on Astro',
|
||||||
customData: `<language>en-us</language>`,
|
customData: `<language>en-us</language>`,
|
||||||
// The list of items for your RSS feed, sorted.
|
// The list of items for your RSS feed, sorted.
|
||||||
|
@ -41,3 +43,11 @@ export async function getStaticPaths({rss}) {
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: RSS feeds will **not** be built during development. Currently, RSS feeds are only generated during your final build.
|
Note: RSS feeds will **not** be built during development. Currently, RSS feeds are only generated during your final build.
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
RSS Feeds can be styled with an XSL stylesheet for a more pleasant user experience when they are opened directly in a browser. By default, Astro does not set a stylesheet for RSS feeds, but it can be enabled by setting the `stylesheet` option.
|
||||||
|
|
||||||
|
Astro can automatically use [Pretty Feed](https://github.com/genmon/aboutfeeds/blob/main/tools/pretty-feed-v3.xsl), a popular open-source XSL stylesheet. To enable this behavior, pass `stylesheet: true`.
|
||||||
|
|
||||||
|
If you'd like to use a custom XSL stylesheet, you can pass a string value like `stylesheet: '/my-custom-stylesheet.xsl'`. This file should be in your `public/` directory (in this case, `public/my-custom-stylesheet.xsl`).
|
||||||
|
|
|
@ -339,6 +339,12 @@ export interface RSS {
|
||||||
description: string;
|
description: string;
|
||||||
/** Specify arbitrary metadata on opening <xml> tag */
|
/** Specify arbitrary metadata on opening <xml> tag */
|
||||||
xmlns?: Record<string, string>;
|
xmlns?: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* If false (default), does not include XSL stylesheet.
|
||||||
|
* If true, automatically includes 'pretty-feed-v3'.
|
||||||
|
* If a string value, specifies a local custom XSL stylesheet, for example '/custom-feed.xsl'.
|
||||||
|
*/
|
||||||
|
stylesheet?: string | boolean;
|
||||||
/** Specify custom data in opening of file */
|
/** Specify custom data in opening of file */
|
||||||
customData?: string;
|
customData?: string;
|
||||||
/**
|
/**
|
||||||
|
@ -364,7 +370,8 @@ export interface RSS {
|
||||||
|
|
||||||
export type RSSFunction = (args: RSS) => void;
|
export type RSSFunction = (args: RSS) => void;
|
||||||
|
|
||||||
export type RSSResult = { url: string; xml?: string };
|
export type FeedResult = { url: string; content?: string; };
|
||||||
|
export type RSSResult = { xml: FeedResult; xsl?: FeedResult };
|
||||||
|
|
||||||
export type SSRError = Error & vite.ErrorPayload['err'];
|
export type SSRError = Error & vite.ErrorPayload['err'];
|
||||||
|
|
||||||
|
|
|
@ -79,11 +79,22 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
if (result.rss?.xml) {
|
if (result.rss?.xml) {
|
||||||
const rssFile = new URL(result.rss.url.replace(/^\/?/, './'), astroConfig.dist);
|
const { url, content } = result.rss.xml;
|
||||||
if (assets[fileURLToPath(rssFile)]) {
|
if (content) {
|
||||||
throw new Error(`[getStaticPaths] RSS feed ${result.rss.url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`);
|
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)] = result.rss.xml;
|
}
|
||||||
|
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,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { RSSFunction, RSS, RSSResult, 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 } from '../util.js';
|
import { canonicalURL, PRETTY_FEED_V3 } from '../util.js';
|
||||||
|
|
||||||
/** Validates getStaticPaths.rss */
|
/** Validates getStaticPaths.rss */
|
||||||
export function validateRSS(args: GenerateRSSArgs): void {
|
export function validateRSS(args: GenerateRSSArgs): void {
|
||||||
|
@ -20,7 +20,11 @@ export function generateRSS(args: GenerateRSSArgs): string {
|
||||||
const { srcFile, feedURL, rssData, site } = args;
|
const { srcFile, feedURL, rssData, site } = args;
|
||||||
if ((rssData as any).item) throw new Error(`[${srcFile}] rss() \`item()\` function was deprecated, and is now \`items: object[]\`.`);
|
if ((rssData as any).item) throw new Error(`[${srcFile}] rss() \`item()\` function was deprecated, and is now \`items: object[]\`.`);
|
||||||
|
|
||||||
let xml = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"`;
|
let xml = `<?xml version="1.0" encoding="UTF-8"?>`;
|
||||||
|
if (typeof rssData.stylesheet === 'string') {
|
||||||
|
xml += `<?xml-stylesheet href="${rssData.stylesheet}" type="text/xsl"?>`;
|
||||||
|
}
|
||||||
|
xml += `<rss version="2.0"`;
|
||||||
|
|
||||||
// xmlns
|
// xmlns
|
||||||
if (rssData.xmlns) {
|
if (rssData.xmlns) {
|
||||||
|
@ -72,18 +76,35 @@ export function generateRSS(args: GenerateRSSArgs): string {
|
||||||
return xml;
|
return xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateRSSStylesheet() {
|
||||||
|
return PRETTY_FEED_V3;
|
||||||
|
}
|
||||||
|
|
||||||
/** 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 result: RSSResult = {} as any;
|
||||||
return {
|
return {
|
||||||
generator: function rssUtility(args: any) {
|
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`);
|
||||||
}
|
}
|
||||||
const { dest, ...rssData } = args;
|
const { dest, ...rssData } = args;
|
||||||
const feedURL = dest || '/rss.xml';
|
const feedURL = dest || '/rss.xml';
|
||||||
result.url = feedURL;
|
if (rssData.stylesheet === true) {
|
||||||
result.xml = generateRSS({ rssData, site, srcFile: route.component, feedURL });
|
rssData.stylesheet = feedURL.replace(/\.xml$/, '.xsl');
|
||||||
|
result.xsl = {
|
||||||
|
url: rssData.stylesheet,
|
||||||
|
content: generateRSSStylesheet(),
|
||||||
|
}
|
||||||
|
} else if (typeof rssData.stylesheet === 'string') {
|
||||||
|
result.xsl = {
|
||||||
|
url: rssData.stylesheet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.xml = {
|
||||||
|
url: feedURL,
|
||||||
|
content: generateRSS({ rssData, site, srcFile: route.component, feedURL })
|
||||||
|
};
|
||||||
},
|
},
|
||||||
rss: result,
|
rss: result,
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue