From 42662ae3db2a2f2366942ab7d68f9441e03b29bb Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Tue, 13 Jan 2026 19:06:55 +0100 Subject: Add full content render for rss feed --- package.json | 2 ++ pnpm-lock.yaml | 58 +++++++++++++++++++++++++++++++++++++++++ src/loaders/pleroma.ts | 16 +----------- src/pages/rss.xml.ts | 23 +++++++++++----- src/pages/tags/[tag]/rss.xml.ts | 23 +++++++++++----- src/utils/markdown.ts | 11 ++++++++ 6 files changed, 104 insertions(+), 29 deletions(-) create mode 100644 src/utils/markdown.ts diff --git a/package.json b/package.json index df7f25c..4f25b06 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "rehype-external-links": "^3.0.0", "rehype-unwrap-images": "^1.0.0", "remark-directive": "^4.0.0", + "sanitize-html": "^2.17.0", "satori": "0.15.2", "satori-html": "^0.3.2", "sharp": "^0.34.2", @@ -54,6 +55,7 @@ "@tailwindcss/typography": "^0.5.16", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", + "@types/sanitize-html": "^2.16.0", "@types/turndown": "^5.0.5", "autoprefixer": "^10.4.21", "husky": "^9.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db2b694..e7df8c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: remark-directive: specifier: ^4.0.0 version: 4.0.0 + sanitize-html: + specifier: ^2.17.0 + version: 2.17.0 satori: specifier: 0.15.2 version: 0.15.2 @@ -114,6 +117,9 @@ importers: '@types/mdast': specifier: ^4.0.4 version: 4.0.4 + '@types/sanitize-html': + specifier: ^2.16.0 + version: 2.16.0 '@types/turndown': specifier: ^5.0.5 version: 5.0.5 @@ -1265,6 +1271,9 @@ packages: '@types/node@24.0.7': resolution: {integrity: sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==} + '@types/sanitize-html@2.16.0': + resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} + '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} @@ -1671,6 +1680,10 @@ packages: decode-named-character-reference@1.1.0: resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} @@ -1813,6 +1826,10 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -2056,6 +2073,9 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} @@ -2129,6 +2149,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -2617,6 +2641,9 @@ packages: parse-latin@7.0.0: resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -3071,6 +3098,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sanitize-html@2.17.0: + resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} + sass-formatter@0.7.9: resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} @@ -4663,6 +4693,10 @@ snapshots: dependencies: undici-types: 7.8.0 + '@types/sanitize-html@2.16.0': + dependencies: + htmlparser2: 8.0.2 + '@types/sax@1.2.7': dependencies: '@types/node': 17.0.45 @@ -5200,6 +5234,8 @@ snapshots: dependencies: character-entities: 2.0.2 + deepmerge@4.3.1: {} + defu@6.1.4: {} delayed-stream@1.0.0: {} @@ -5379,6 +5415,8 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} estree-util-attach-comments@3.0.0: @@ -5747,6 +5785,13 @@ snapshots: html-void-elements@3.0.0: {} + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + htmlparser2@9.1.0: dependencies: domelementtype: 2.3.0 @@ -5801,6 +5846,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} + is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -6545,6 +6592,8 @@ snapshots: unist-util-visit-children: 3.0.0 vfile: 6.0.3 + parse-srcset@1.0.2: {} + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -7044,6 +7093,15 @@ snapshots: safer-buffer@2.1.2: {} + sanitize-html@2.17.0: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.5.6 + sass-formatter@0.7.9: dependencies: suf-log: 2.5.3 diff --git a/src/loaders/pleroma.ts b/src/loaders/pleroma.ts index 36ea4d6..83f60c2 100644 --- a/src/loaders/pleroma.ts +++ b/src/loaders/pleroma.ts @@ -1,6 +1,6 @@ import type { Loader } from "astro/loaders"; -import { marked } from "marked"; import TurndownService from "turndown"; +import { markdownToHtml } from "@/utils/markdown"; interface Logger { info: (message: string) => void; @@ -606,20 +606,6 @@ function replacePleromaLinks( return modifiedContent; } -function markdownToHtml(markdown: string): string { - // Configure marked options for safe rendering - marked.setOptions({ - breaks: true, // Convert line breaks to
- gfm: true, // GitHub flavored markdown - }); - - // Convert markdown to HTML - const html = marked.parse(markdown); - - // Return as string (marked.parse can return string or Promise) - return typeof html === "string" ? html : ""; -} - function extractTitle(content: string): string { // Extract first line or first sentence as title const firstLine = content.split("\n")[0]; diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index d428cc9..61adea7 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -1,7 +1,9 @@ import rss from "@astrojs/rss"; import type { APIContext } from "astro"; +import sanitizeHtml from "sanitize-html"; import { getAllPosts } from "@/data/post"; import { siteConfig } from "@/site.config"; +import { markdownToHtml } from "@/utils/markdown"; export const GET = async (context: APIContext) => { const posts = await getAllPosts(); @@ -10,13 +12,20 @@ export const GET = async (context: APIContext) => { title: siteConfig.title, description: siteConfig.description, site: context.site || import.meta.env.SITE, - items: posts.map((post) => ({ - title: post.data.title, - description: post.data.description, - pubDate: post.data.publishDate, - link: `posts/${post.id}/`, - author: post.data.author, - })), + items: posts.map((post) => { + const htmlContent = post.rendered?.html || markdownToHtml(post.body || ""); + + return { + title: post.data.title, + description: post.data.description, + pubDate: post.data.publishDate, + link: `posts/${post.id}/`, + author: post.data.author, + content: sanitizeHtml(htmlContent, { + allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]), + }), + }; + }), customData: ``, }); }; diff --git a/src/pages/tags/[tag]/rss.xml.ts b/src/pages/tags/[tag]/rss.xml.ts index 3a61414..89e158c 100644 --- a/src/pages/tags/[tag]/rss.xml.ts +++ b/src/pages/tags/[tag]/rss.xml.ts @@ -1,7 +1,9 @@ import rss from "@astrojs/rss"; import type { APIContext } from "astro"; +import sanitizeHtml from "sanitize-html"; import { getAllPosts, getUniqueTags } from "@/data/post"; import { siteConfig } from "@/site.config"; +import { markdownToHtml } from "@/utils/markdown"; export async function getStaticPaths() { // Get all posts (including archived, now includes pleroma too) @@ -37,13 +39,20 @@ export const GET = async (context: APIContext) => { title: `${siteConfig.title} - ${tag}`, description: `Posts tagged with ${tag}`, site, - items: sortedPosts.map((post) => ({ - title: post.data.title, - description: post.data.description, - pubDate: post.data.publishDate, - link: `posts/${post.id}/`, - author: post.data.author, - })), + items: sortedPosts.map((post) => { + const htmlContent = post.rendered?.html || markdownToHtml(post.body || ""); + + return { + title: post.data.title, + description: post.data.description, + pubDate: post.data.publishDate, + link: `posts/${post.id}/`, + author: post.data.author, + content: sanitizeHtml(htmlContent, { + allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]), + }), + }; + }), customData: ``, }); }; diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts new file mode 100644 index 0000000..946d8d8 --- /dev/null +++ b/src/utils/markdown.ts @@ -0,0 +1,11 @@ +import { marked } from "marked"; + +marked.setOptions({ + breaks: true, + gfm: true, +}); + +export function markdownToHtml(markdown: string): string { + const html = marked.parse(markdown); + return typeof html === "string" ? html : ""; +} -- cgit v1.2.3