feat(rss): 添加对内容相对路径图片的支持并优化RSS生成

在RSS生成器中添加node-html-parser依赖,用于解析和处理HTML内容中的图片路径。主要修改包括:
1. 添加对内容相对路径图片的支持,自动转换为正确的_astro路径
2. 优化RSS项生成逻辑,使用Astro的内容集合API
3. 改进图片路径处理,支持公共目录图片
This commit is contained in:
afoim
2025-08-10 22:37:55 +08:00
parent bd99261090
commit 4667fb2032
3 changed files with 91 additions and 25 deletions

View File

@@ -36,8 +36,8 @@
"katex": "^0.16.22",
"markdown-it": "^14.1.0",
"mdast-util-to-string": "^4.0.0",
"node-html-parser": "^7.0.1",
"overlayscrollbars": "^2.11.1",
"photoswipe": "^5.4.4",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",

17
pnpm-lock.yaml generated
View File

@@ -74,6 +74,9 @@ importers:
mdast-util-to-string:
specifier: ^4.0.0
version: 4.0.0
node-html-parser:
specifier: ^7.0.1
version: 7.0.1
overlayscrollbars:
specifier: ^2.11.1
version: 2.11.1
@@ -2863,6 +2866,10 @@ packages:
hastscript@9.0.1:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
html-escaper@3.0.3:
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
@@ -3590,6 +3597,9 @@ packages:
encoding:
optional: true
node-html-parser@7.0.1:
resolution: {integrity: sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==}
node-mock-http@1.0.0:
resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==}
@@ -8321,6 +8331,8 @@ snapshots:
property-information: 7.0.0
space-separated-tokens: 2.0.2
he@1.2.0: {}
html-escaper@3.0.3: {}
html-void-elements@3.0.0: {}
@@ -9250,6 +9262,11 @@ snapshots:
dependencies:
whatwg-url: 5.0.0
node-html-parser@7.0.1:
dependencies:
css-select: 5.1.0
he: 1.2.0
node-mock-http@1.0.0: {}
node-releases@2.0.19: {}

View File

@@ -1,33 +1,82 @@
import { siteConfig } from "@/config";
import rss from "@astrojs/rss";
import { getSortedPosts } from "@utils/content-utils";
import type { APIContext } from "astro";
import MarkdownIt from "markdown-it";
import sanitizeHtml from "sanitize-html";
import rss from '@astrojs/rss';
import sanitizeHtml from 'sanitize-html';
import MarkdownIt from 'markdown-it';
import { getCollection } from 'astro:content';
import { siteConfig } from '@/config';
import { parse as htmlParser } from 'node-html-parser';
import { getImage } from 'astro:assets';
import type { APIContext } from 'astro';
import type { RSSFeedItem } from '@astrojs/rss';
const parser = new MarkdownIt();
const markdownParser = new MarkdownIt();
// get dynamic import of images as a map collection
const imagesGlob = import.meta.glob<{ default: ImageMetadata }>(
'/src/content/**/*.{jpeg,jpg,png,gif,webp}', // include posts and assets
);
export async function GET(context: APIContext) {
const blog = await getSortedPosts();
if (!context.site) {
throw Error('site not set');
}
const posts = await getCollection('posts');
const feed: RSSFeedItem[] = [];
for (const post of posts) {
// convert markdown to html string
const body = markdownParser.render(post.body);
// convert html string to DOM-like structure
const html = htmlParser.parse(body);
// hold all img tags in variable images
const images = html.querySelectorAll('img');
for (const img of images) {
const src = img.getAttribute('src');
if (!src) continue;
// Handle content-relative images and convert them to built _astro paths
if (src.startsWith('./') || src.startsWith('../')) {
let importPath: string | null = null;
if (src.startsWith('./')) {
// Path relative to the post file directory
const prefixRemoved = src.slice(2);
importPath = `/src/content/posts/${prefixRemoved}`;
} else {
// Path like ../assets/images/xxx -> relative to /src/content/
const cleaned = src.replace(/^\.\.\//, '');
importPath = `/src/content/${cleaned}`;
}
const imageMod = await imagesGlob[importPath]?.()?.then((res) => res.default);
if (imageMod) {
const optimizedImg = await getImage({ src: imageMod });
img.setAttribute('src', new URL(optimizedImg.src, context.site).href);
}
} else if (src.startsWith('/')) {
// images starting with `/` are in public dir
img.setAttribute('src', new URL(src, context.site).href);
}
}
feed.push({
title: post.data.title,
description: post.data.description,
pubDate: post.data.published,
link: `/posts/${post.slug}/`,
// sanitize the new html string with corrected image paths
content: sanitizeHtml(html.toString(), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
}),
});
}
return rss({
title: siteConfig.title,
description: siteConfig.subtitle || "No description",
site: context.site ?? "https://fuwari.vercel.app",
items: blog.map((post) => {
const content =
typeof post.body === "string" ? post.body : String(post.body || "");
return {
title: post.data.title,
pubDate: post.data.published,
description: post.data.description || "",
link: `/posts/${post.slug}/`,
content: sanitizeHtml(parser.render(content), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
}),
};
}),
description: siteConfig.subtitle || 'No description',
site: context.site,
items: feed,
customData: `<language>${siteConfig.lang}</language>`,
});
}