Skip to content

Commit 5310781

Browse files
authored
refactor(plugin-rss)!: requires the ssg config, and remove _html field (#3003)
1 parent 9a8d833 commit 5310781

File tree

4 files changed

+145
-42
lines changed

4 files changed

+145
-42
lines changed

e2e/fixtures/plugin-rss/doc/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
link-rss: blog
33
---
44

5-
Nothing but should have rss <link>
5+
Nothing but should have rss `<link>`

packages/plugin-rss/src/createFeed.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ import type { FeedChannel, FeedItem } from './type';
1212
/**
1313
* @public
1414
* @param page Rspress Page Data
15-
* @param siteUrl
15+
* @param siteUrl Site URL for generating absolute links
16+
* @param htmlContent HTML content extracted from SSG output (optional)
1617
*/
17-
export function generateFeedItem(page: PageIndexInfo, siteUrl: string) {
18+
export function generateFeedItem(
19+
page: PageIndexInfo,
20+
siteUrl: string,
21+
htmlContent?: string | null,
22+
) {
1823
const { frontmatter: fm } = page;
1924
return {
2025
id: selectNonNullishProperty(fm.slug, fm.id, page.routePath) || '',
@@ -28,7 +33,9 @@ export function generateFeedItem(page: PageIndexInfo, siteUrl: string) {
2833
) || '',
2934
),
3035
description: selectNonNullishProperty(fm.description) || '',
31-
content: selectNonNullishProperty(fm.summary, page._html) || '',
36+
// Priority: frontmatter.summary > SSG HTML content > plain text content
37+
content:
38+
selectNonNullishProperty(fm.summary, htmlContent, page.content) || '',
3239
date: toDate((fm.date as string) || (fm.published_at as string))!,
3340
category: concatArray(fm.categories as string[], fm.category as string).map(
3441
cat => ({ name: cat }),

packages/plugin-rss/src/internals/node.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,52 @@ export async function writeFile(
99
await promises.mkdir(dir, { mode: 0o755, recursive: true });
1010
return promises.writeFile(path, content);
1111
}
12+
13+
export async function readFile(path: string): Promise<string | null> {
14+
try {
15+
return await promises.readFile(path, 'utf-8');
16+
} catch {
17+
return null;
18+
}
19+
}
20+
21+
/**
22+
* Convert routePath to HTML file path
23+
* e.g. '/blog/foo/' -> 'blog/foo/index.html'
24+
* e.g. '/blog/foo' -> 'blog/foo.html'
25+
*/
26+
export function routePathToHtmlPath(routePath: string): string {
27+
let fileName = routePath;
28+
if (fileName.endsWith('/')) {
29+
fileName = `${routePath}index.html`;
30+
} else {
31+
fileName = `${routePath}.html`;
32+
}
33+
return fileName.replace(/^\/+/, '');
34+
}
35+
36+
/**
37+
* Extract content from HTML using regex to find .rspress-doc container
38+
* This extracts the innerHTML of the element with class "rspress-doc"
39+
*/
40+
export function extractHtmlContent(html: string): string | null {
41+
// Match the content inside <div class="rp-doc rspress-doc" ...>...</div>
42+
// The content ends before </div><footer or </div></main
43+
const match = html.match(
44+
/<div[^>]*class="[^"]*rspress-doc[^"]*"[^>]*>([\s\S]*?)<\/div>(?=<footer|<\/main)/,
45+
);
46+
if (match) {
47+
return match[1].trim();
48+
}
49+
50+
// Fallback: try to find any element with rspress-doc class
51+
// Use a greedy approach but stop at common ending patterns
52+
const fallbackMatch = html.match(
53+
/<div[^>]*class="[^"]*rspress-doc[^"]*"[^>]*>([\s\S]*?)<\/div>/,
54+
);
55+
if (fallbackMatch) {
56+
return fallbackMatch[1].trim();
57+
}
58+
59+
return null;
60+
}

packages/plugin-rss/src/plugin-rss.ts

Lines changed: 85 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import { Feed } from 'feed';
88

99
import { createFeed, generateFeedItem } from './createFeed';
1010
import { PluginComponents, PluginName } from './exports';
11-
import { concatArray, type ResolvedOutput, writeFile } from './internals';
11+
import {
12+
concatArray,
13+
extractHtmlContent,
14+
type ResolvedOutput,
15+
readFile,
16+
routePathToHtmlPath,
17+
writeFile,
18+
} from './internals';
1219
import { getDefaultFeedOption, getOutputInfo, testPage } from './options';
1320
import type { FeedChannel, FeedItem, PluginRssOptions } from './type';
1421

@@ -47,17 +54,26 @@ class FeedsSet {
4754
}
4855
}
4956

50-
function getRssItems(
57+
interface PageRssInfo {
58+
page: PageIndexInfo;
59+
channels: string[];
60+
}
61+
62+
async function getRssItems(
5163
feeds: TransformedFeedChannel[],
5264
page: PageIndexInfo,
5365
siteUrl: string,
66+
htmlContent: string | null,
5467
): Promise<FeedItemWithChannel[]> {
5568
return Promise.all(
5669
feeds
5770
.filter(options => testPage(options.test, page))
5871
.map(async options => {
5972
const after = options.item || ((feed: FeedItem) => feed);
60-
const item = await after(generateFeedItem(page, siteUrl), page);
73+
const item = await after(
74+
generateFeedItem(page, siteUrl, htmlContent),
75+
page,
76+
);
6177
return { ...item, channel: options.id };
6278
}),
6379
);
@@ -67,77 +83,108 @@ export function pluginRss(pluginRssOptions: PluginRssOptions): RspressPlugin {
6783
const feedsSet = new FeedsSet();
6884

6985
/**
70-
* workaround for retrieving data of pages in `afterBuild`
71-
* TODO: get pageData list directly in `afterBuild`
72-
**/
73-
let _rssWorkaround: null | Record<
74-
string,
75-
PromiseLike<FeedItemWithChannel[]>
76-
> = null;
86+
* Store page data for generating RSS items in afterBuild
87+
* Key: routePath, Value: PageRssInfo
88+
*/
89+
let _pagesForRss: null | Map<string, PageRssInfo> = null;
7790

7891
return {
7992
name: PluginName,
8093
globalUIComponents: Object.values(PluginComponents),
8194
beforeBuild(config, isProd) {
8295
if (!isProd) {
83-
_rssWorkaround = null;
96+
_pagesForRss = null;
8497
return;
8598
}
86-
_rssWorkaround = {};
99+
100+
// RSS plugin requires SSG to be enabled
101+
const enableSSG = Boolean((config.ssg || config.llms) ?? true);
102+
if (!enableSSG) {
103+
throw new Error(
104+
'[plugin-rss] RSS plugin requires SSG to be enabled. ' +
105+
'Please set `ssg: true` in your rspress.config.ts or remove the RSS plugin.',
106+
);
107+
}
108+
109+
_pagesForRss = new Map();
87110
feedsSet.set(pluginRssOptions, config);
88111
},
89112
async extendPageData(pageData) {
90-
if (!_rssWorkaround) return;
113+
if (!_pagesForRss) return;
114+
115+
// Find which feeds this page belongs to
116+
const matchedChannels = feedsSet
117+
.get()
118+
.filter(options => testPage(options.test, pageData))
119+
.map(options => options.id);
91120

92-
// rspress run `extendPageData` for each page
93-
// - let's cache rss items within a complete rspress build
94-
_rssWorkaround[pageData.routePath] =
95-
_rssWorkaround[pageData.routePath] ||
96-
getRssItems(feedsSet.get(), pageData, pluginRssOptions.siteUrl);
121+
if (matchedChannels.length > 0) {
122+
_pagesForRss.set(pageData.routePath, {
123+
page: pageData,
124+
channels: matchedChannels,
125+
});
126+
}
97127

98-
const feeds = await _rssWorkaround[pageData.routePath];
128+
// Set up feed links for the page
99129
const showRssList = new Set(
100130
concatArray(pageData.frontmatter['link-rss'] as string[] | string),
101131
);
102-
for (const feed of feeds) {
103-
showRssList.add(feed.channel);
132+
for (const channel of matchedChannels) {
133+
showRssList.add(channel);
104134
}
105135

106136
pageData.feeds = Array.from(showRssList, id => {
107-
const { output, language } = feedsSet.get(id)!;
137+
const feedChannel = feedsSet.get(id);
138+
if (!feedChannel) return null;
139+
const { output, language } = feedChannel;
108140
return {
109141
url: output.url,
110142
mime: output.mime,
111143
language: language || pageData.lang,
112144
};
113-
});
145+
}).filter(Boolean) as typeof pageData.feeds;
114146
},
115147
async afterBuild(config) {
116-
if (!_rssWorkaround) return;
148+
if (!_pagesForRss) return;
117149

118-
const items = concatArray(
119-
...(await Promise.all(Object.values(_rssWorkaround))),
120-
);
150+
const outDir = config.outDir || 'doc_build';
121151
const feeds: Record<string, Feed> = Object.create(null);
122152

123-
for (const { channel, ...item } of items) {
124-
feeds[channel] =
125-
feeds[channel] ||
126-
new Feed(createFeed(feedsSet.get(channel)!, config));
127-
feeds[channel].addItem(item);
153+
// Process each page: read HTML from SSG output and generate feed items
154+
for (const [routePath, { page, channels }] of _pagesForRss) {
155+
// Read HTML content from SSG output
156+
const htmlPath = NodePath.resolve(
157+
outDir,
158+
routePathToHtmlPath(routePath),
159+
);
160+
const htmlFile = await readFile(htmlPath);
161+
const htmlContent = htmlFile ? extractHtmlContent(htmlFile) : null;
162+
163+
// Generate feed items for each channel
164+
const items = await getRssItems(
165+
channels.map(id => feedsSet.get(id)!),
166+
page,
167+
pluginRssOptions.siteUrl,
168+
htmlContent,
169+
);
170+
171+
for (const { channel, ...item } of items) {
172+
feeds[channel] =
173+
feeds[channel] ||
174+
new Feed(createFeed(feedsSet.get(channel)!, config));
175+
feeds[channel].addItem(item);
176+
}
128177
}
129178

179+
// Write feed files
130180
for (const [channel, feed] of Object.entries(feeds)) {
131181
const { output } = feedsSet.get(channel)!;
132182
feed.items.sort(output.sorting);
133-
const path = NodePath.resolve(
134-
config.outDir || 'doc_build',
135-
output.dir,
136-
output.filename,
137-
);
183+
const path = NodePath.resolve(outDir, output.dir, output.filename);
138184
await writeFile(path, output.getContent(feed));
139185
}
140-
_rssWorkaround = null;
186+
187+
_pagesForRss = null;
141188
},
142189
};
143190
}

0 commit comments

Comments
 (0)