From e859109ade497d4906a3310c3e6d8a205fd8fc1b Mon Sep 17 00:00:00 2001 From: Bad Manners <me@badmanners.xyz> Date: Tue, 2 Apr 2024 22:48:48 -0300 Subject: [PATCH] Add content to RSS feed items --- package-lock.json | 128 +++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- src/pages/feed.xml.ts | 56 +++++++++++++----- src/pages/index.astro | 2 +- 4 files changed, 172 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d41de1..5a79535 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,23 @@ { "name": "gallery-badmanners-xyz", - "version": "1.2.1", + "version": "1.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gallery-badmanners-xyz", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@astrojs/check": "^0.5.9", "@astrojs/rss": "^4.0.5", "@astrojs/tailwind": "^5.1.0", "@astropub/md": "^0.4.0", "@tailwindcss/typography": "^0.5.10", + "@types/sanitize-html": "^2.11.0", "astro": "^4.5.4", "github-slugger": "^2.0.0", "marked": "^12.0.1", + "sanitize-html": "^2.13.0", "tailwindcss": "^3.4.1", "tiny-decode": "^0.1.3", "typescript": "^5.4.2" @@ -1370,6 +1372,14 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==", + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, "node_modules/@types/unist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", @@ -2402,6 +2412,14 @@ "node": ">=4.0.0" } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2465,6 +2483,57 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dset": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz", @@ -3147,6 +3216,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -3345,6 +3432,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -4791,6 +4886,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -6076,6 +6176,30 @@ } ] }, + "node_modules/sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sass-formatter": { "version": "0.7.9", "resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.9.tgz", diff --git a/package.json b/package.json index 26082b3..1b754a1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gallery-badmanners-xyz", "type": "module", - "version": "1.2.1", + "version": "1.2.2", "scripts": { "dev": "astro dev", "start": "astro dev", @@ -17,9 +17,11 @@ "@astrojs/tailwind": "^5.1.0", "@astropub/md": "^0.4.0", "@tailwindcss/typography": "^0.5.10", + "@types/sanitize-html": "^2.11.0", "astro": "^4.5.4", "github-slugger": "^2.0.0", "marked": "^12.0.1", + "sanitize-html": "^2.13.0", "tailwindcss": "^3.4.1", "tiny-decode": "^0.1.3", "typescript": "^5.4.2" diff --git a/src/pages/feed.xml.ts b/src/pages/feed.xml.ts index 8956900..9e0cb0f 100644 --- a/src/pages/feed.xml.ts +++ b/src/pages/feed.xml.ts @@ -1,12 +1,17 @@ import rss, { type RSSFeedItem } from "@astrojs/rss"; import type { APIRoute } from "astro"; -import { getCollection } from "astro:content"; +import { getCollection, type CollectionEntry } from "astro:content"; +import sanitizeHtml from "sanitize-html"; +import { marked } from "marked"; +import { decode as tinyDecode } from "tiny-decode"; +import { t } from "../i18n"; +import type { Lang } from "../content/config"; type FeedItem = RSSFeedItem & { pubDate: Date; }; -const MAX_ITEMS = 10; +const MAX_ITEMS = 8; function toNoonUTCDate(date: Date) { const adjustedDate = new Date(date); @@ -14,6 +19,15 @@ function toNoonUTCDate(date: Date) { return adjustedDate; } +const getLinkForUser = (user: CollectionEntry<"users">, lang: Lang) => { + const userName = user.data.nameLang[lang] || user.data.name; + if (user.data.preferredLink) { + const link = user.data.links[user.data.preferredLink]!; + return `<a href="${typeof link === "string" ? link : link[0]}">${userName}</a>` + } + return userName; +} + export const GET: APIRoute = async ({ site }) => { const stories = (await getCollection("stories", (story) => !story.data.isDraft)) .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()) @@ -21,30 +35,44 @@ export const GET: APIRoute = async ({ site }) => { const games = (await getCollection("games", (game) => !game.data.isDraft)) .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()) .slice(0, MAX_ITEMS); + const users = await getCollection("users"); + return rss({ title: "Gallery | Bad Manners", description: "Stories, games, and (possibly) more by Bad Manners", site: site as URL, items: [ - stories.map<FeedItem>((story) => ({ - title: `New story! "${story.data.title}"`, - pubDate: toNoonUTCDate(story.data.pubDate), - link: `/stories/${story.slug}`, + await Promise.all(stories.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({ + title: `New story! "${data.title}"`, + pubDate: toNoonUTCDate(data.pubDate), + link: `/stories/${slug}`, description: - `Word count: ${story.data.wordCount}. ${story.data.contentWarning} ${story.data.descriptionPlaintext || story.data.description}` + `Word count: ${data.wordCount}. ${data.contentWarning} ${data.descriptionPlaintext || data.description}` .replaceAll(/[\n ]+/g, " ") .trim(), categories: ["story"], - })), - games.map<FeedItem>((game) => ({ - title: `New game! "${game.data.title}"`, - pubDate: toNoonUTCDate(game.data.pubDate), - link: `/games/${game.slug}`, - description: `${game.data.contentWarning} ${game.data.descriptionPlaintext || game.data.description}` + content: sanitizeHtml(`<h1>${data.title}</h1><p>${t(data.lang, "story/authors", [data.authors].flatMap((authorArray => { + if (!Array.isArray(authorArray)) { + authorArray = [authorArray] + } + return authorArray.map(author => getLinkForUser(users.find(user => user.id === author.id)!, data.lang)); + })))}</p>${data.requester ? `<p>${t(data.lang, "export_story/request_for", getLinkForUser(users.find(user => user.id === data.requester!.id)!, data.lang))}</p>` : ""}${data.commissioner ? `<p>${t(data.lang, "export_story/commissioned_by", getLinkForUser(users.find(user => user.id === data.commissioner!.id)!, data.lang))}</p>` : ""}<hr>${tinyDecode(await marked(body))}`), + }))), + await Promise.all(games.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({ + title: `New game! "${data.title}"`, + pubDate: toNoonUTCDate(data.pubDate), + link: `/games/${slug}`, + description: `${data.contentWarning} ${data.descriptionPlaintext || data.description}` .replaceAll(/[\n ]+/g, " ") .trim(), categories: ["game"], - })), + content: sanitizeHtml(`<h1>${data.title}</h1><p>${t(data.lang, "story/authors", [data.authors].flatMap((authorArray => { + if (!Array.isArray(authorArray)) { + authorArray = [authorArray] + } + return authorArray.map(author => getLinkForUser(users.find(user => user.id === author.id)!, data.lang)); + })))}</p><hr>${tinyDecode(await marked(body))}`), + }))), ] .flat() .sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime()) diff --git a/src/pages/index.astro b/src/pages/index.astro index 192b12c..8b44d83 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,7 +3,7 @@ import { type CollectionEntry, getCollection } from "astro:content"; import { Image } from "astro:assets"; import GalleryLayout from "../layouts/GalleryLayout.astro"; -const MAX_ITEMS = 5; +const MAX_ITEMS = 8; interface LatestItemsEntry { type: string;