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
|
||||
---
|
||||
|
||||
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)).
|
||||
|
||||
|
@ -22,6 +22,8 @@ export async function getStaticPaths({rss}) {
|
|||
rss({
|
||||
// The RSS Feed title, description, and custom metadata.
|
||||
title: 'Don\'s Blog',
|
||||
// See "Styling" section below
|
||||
stylesheet: true,
|
||||
description: 'An example blog on Astro',
|
||||
customData: `<language>en-us</language>`,
|
||||
// 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.
|
||||
|
||||
### 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;
|
||||
/** Specify arbitrary metadata on opening <xml> tag */
|
||||
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 */
|
||||
customData?: string;
|
||||
/**
|
||||
|
@ -364,7 +370,8 @@ export interface RSS {
|
|||
|
||||
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'];
|
||||
|
||||
|
|
|
@ -79,11 +79,22 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
|
|||
throw err;
|
||||
});
|
||||
if (result.rss?.xml) {
|
||||
const rssFile = new URL(result.rss.url.replace(/^\/?/, './'), astroConfig.dist);
|
||||
if (assets[fileURLToPath(rssFile)]) {
|
||||
throw new Error(`[getStaticPaths] RSS feed ${result.rss.url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`);
|
||||
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})`);
|
||||
}
|
||||
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] = {
|
||||
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 { canonicalURL } from '../util.js';
|
||||
import { canonicalURL, PRETTY_FEED_V3 } from '../util.js';
|
||||
|
||||
/** Validates getStaticPaths.rss */
|
||||
export function validateRSS(args: GenerateRSSArgs): void {
|
||||
|
@ -20,7 +20,11 @@ export function generateRSS(args: GenerateRSSArgs): string {
|
|||
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[]\`.`);
|
||||
|
||||
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
|
||||
if (rssData.xmlns) {
|
||||
|
@ -72,18 +76,35 @@ export function generateRSS(args: GenerateRSSArgs): string {
|
|||
return xml;
|
||||
}
|
||||
|
||||
export function generateRSSStylesheet() {
|
||||
return PRETTY_FEED_V3;
|
||||
}
|
||||
|
||||
/** Generated function to be run */
|
||||
export function generateRssFunction(site: string | undefined, route: RouteData): { generator: RSSFunction; rss?: RSSResult } {
|
||||
let result: RSSResult = {} as any;
|
||||
return {
|
||||
generator: function rssUtility(args: any) {
|
||||
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`);
|
||||
}
|
||||
const { dest, ...rssData } = args;
|
||||
const feedURL = dest || '/rss.xml';
|
||||
result.url = feedURL;
|
||||
result.xml = generateRSS({ rssData, site, srcFile: route.component, feedURL });
|
||||
if (rssData.stylesheet === true) {
|
||||
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,
|
||||
};
|
||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue