diff --git a/package.json b/package.json index 0da1947..9cf9641 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "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.4", "pagefind": "^1.3.0", "photoswipe": "^5.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 054469c..9738c21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,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.4 version: 2.11.4 @@ -2353,10 +2356,6 @@ packages: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -2951,6 +2950,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==} @@ -3678,6 +3681,9 @@ packages: encoding: optional: true + node-html-parser@7.0.1: + resolution: {integrity: sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==} + node-mock-http@1.0.2: resolution: {integrity: sha512-zWaamgDUdo9SSLw47we78+zYw/bDr5gH8pH7oRRs8V3KmBtu8GLgGIbV2p/gRPd3LWpEOpjQj7X1FOU3VFMJ8g==} @@ -7628,7 +7634,7 @@ snapshots: dependencies: boolbase: 1.0.0 css-select: 5.1.0 - css-what: 6.1.0 + css-what: 6.2.2 domelementtype: 2.3.0 domhandler: 5.0.3 domutils: 3.2.2 @@ -7774,7 +7780,7 @@ snapshots: css-select@5.1.0: dependencies: boolbase: 1.0.0 - css-what: 6.1.0 + css-what: 6.2.2 domhandler: 5.0.3 domutils: 3.2.2 nth-check: 2.1.1 @@ -7801,8 +7807,6 @@ snapshots: mdn-data: 2.12.2 source-map-js: 1.2.1 - css-what@6.1.0: {} - css-what@6.2.2: {} cssesc@3.0.0: {} @@ -8582,6 +8586,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: {} @@ -9513,6 +9519,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.2: {} node-releases@2.0.19: {} @@ -10781,7 +10792,7 @@ snapshots: commander: 7.2.0 css-select: 5.1.0 css-tree: 2.3.1 - css-what: 6.1.0 + css-what: 6.2.2 csso: 5.0.5 picocolors: 1.1.1 diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index 2402ab9..72159fb 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -1,43 +1,84 @@ -import rss from "@astrojs/rss"; -import { getSortedPosts } from "@utils/content-utils"; -import { url } from "@utils/url-utils"; -import type { APIContext } from "astro"; -import MarkdownIt from "markdown-it"; -import sanitizeHtml from "sanitize-html"; -import { siteConfig } from "@/config"; +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, ImageMetadata } from 'astro'; +import type { RSSFeedItem } from '@astrojs/rss'; +import { getSortedPosts } from '@/utils/content-utils'; -const parser = new MarkdownIt(); +const markdownParser = new MarkdownIt(); -function stripInvalidXmlChars(str: string): string { - return str.replace( - // biome-ignore lint/suspicious/noControlCharactersInRegex: https://www.w3.org/TR/xml/#charsets - /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFDD0-\uFDEF\uFFFE\uFFFF]/g, - "", - ); -} +// 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'); + } + + // Use the same ordering as site listing (pinned first, then by published desc) + const posts = await getSortedPosts(); + 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 || ""); - const cleanedContent = stripInvalidXmlChars(content); - return { - title: post.data.title, - pubDate: post.data.published, - description: post.data.description || "", - link: url(`/posts/${post.slug}/`), - content: sanitizeHtml(parser.render(cleanedContent), { - allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]), - }), - }; - }), - + description: siteConfig.subtitle || 'No description', + site: context.site, + items: feed, // 准备迎接我的 Folo 之验证吧!——鲁迅 not 达摩(bushi customData: `${siteConfig.lang}17735037994913587283370505718413312`, });