diff --git a/package-lock.json b/package-lock.json index b963f40..3bb95f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gallery.badmanners.xyz", - "version": "1.8.1", + "version": "1.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gallery.badmanners.xyz", - "version": "1.8.1", + "version": "1.8.2", "hasInstallScript": true, "dependencies": { "@astrojs/check": "^0.9.3", diff --git a/package.json b/package.json index 36ce21e..4108151 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gallery.badmanners.xyz", "type": "module", - "version": "1.8.1", + "version": "1.8.2", "scripts": { "postinstall": "astro sync", "dev": "astro dev", diff --git a/src/content/blog/taken-in-breakdown.mdx b/src/content/blog/taken-in-breakdown.mdx index 2d4b725..2475a10 100644 --- a/src/content/blog/taken-in-breakdown.mdx +++ b/src/content/blog/taken-in-breakdown.mdx @@ -26,15 +26,14 @@ relatedStories: import NoteTooltip from "@components/NoteTooltip.astro"; export let count = 0; -export function resetCount() { - count = 0; -} export function getCount() { count += 1; return count; } -{resetCount()} +{function () { +count = 0; +}()} Going over the story and breaking it down was a fun process, all in all. But this was originally a text document, which I had to completely manually refit into this post you're reading (oof...!). Still, for the sake of posteriority, I think it was worth the effort. diff --git a/src/layouts/GalleryLayout.astro b/src/layouts/GalleryLayout.astro index 3cabcbc..8ff39a0 100644 --- a/src/layouts/GalleryLayout.astro +++ b/src/layouts/GalleryLayout.astro @@ -84,7 +84,7 @@ const isCurrentRoute = (path: string) => <li> <a class="u-url text-link group" href="/blog" aria-current={isCurrentRoute("/blog") ? "page" : undefined}> <IconBlog width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" /> - <span class="order-3 group-hover:underline group-focus:underline">Blog posts</span> + <span class="order-3 group-hover:underline group-focus:underline">Blog</span> </a> </li> <li> @@ -104,12 +104,6 @@ const isCurrentRoute = (path: string) => <span class="order-3 group-hover:underline group-focus:underline">Search</span> </a> </li> - <li> - <a class="u-url text-link group" href="/feed.xml"> - <IconSquareRSS width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" /> - <span class="order-3 group-hover:underline group-focus:underline">RSS feed</span> - </a> - </li> <li> <button data-dark-mode @@ -144,7 +138,7 @@ const isCurrentRoute = (path: string) => </div> </nav> <main - class:list={[className, "ml-0 max-w-6xl px-2 pb-28 pt-4 md:px-4 lg:px-8 print:pb-0"]} + class:list={[className, "ml-0 w-full max-w-6xl px-2 pb-28 pt-4 md:px-4 lg:px-8 print:pb-0"]} data-pagefind-body={enablePagefind ? "" : undefined} > <slot /> diff --git a/src/pages/blog/feed.xml.ts b/src/pages/blog/feed.xml.ts new file mode 100644 index 0000000..18f4f0d --- /dev/null +++ b/src/pages/blog/feed.xml.ts @@ -0,0 +1,23 @@ +import rss from "@astrojs/rss"; +import type { APIRoute } from "astro"; +import { getCollection } from "astro:content"; +import { blogFeedItem, type EntryWithPubDate } from "@utils/feed"; + +const MAX_ITEMS = 8; + +export const GET: APIRoute = async ({ site }) => { + const posts = ( + (await getCollection("blog", (post) => !post.data.isDraft && post.data.pubDate)) as EntryWithPubDate<"blog">[] + ) + .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()) + .slice(0, MAX_ITEMS); + + return rss({ + title: "Blog | Bad Manners", + description: "Blog posts by Bad Manners.", + site: new URL("/blog", site!), + items: await Promise.all( + posts.map(async ({ data, slug, render }) => blogFeedItem(site, data, slug, (await render()).Content)), + ), + }); +}; diff --git a/src/pages/blog.astro b/src/pages/blog/index.astro similarity index 88% rename from src/pages/blog.astro rename to src/pages/blog/index.astro index 447684e..4e4190f 100644 --- a/src/pages/blog.astro +++ b/src/pages/blog/index.astro @@ -4,6 +4,7 @@ import { getCollection, getEntries, type CollectionEntry } from "astro:content"; import GalleryLayout from "@layouts/GalleryLayout.astro"; import UserComponent from "@components/UserComponent.astro"; import { markdownToPlaintext } from "@utils/markdown_to_plaintext"; +import { IconSquareRSS } from "@components/icons"; type PostWithPubDate = CollectionEntry<"blog"> & { data: { pubDate: Date } }; @@ -21,7 +22,13 @@ const posts = await Promise.all( <meta slot="head" property="og:description" content="Bad Manners || A game that I've gone and done." /> <h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Blog</h1> <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" /> - <p class="p-summary my-4">Posts on whatever has been rattling in my head as of late.</p> + <div class="my-4 flex w-full"> + <p class="p-summary grow">Posts on whatever has been rattling in my head as of late.</p> + <a class="u-url text-link ml-2 mr-10 p-2" href="/blog/feed.xml" rel="alternate" title="RSS feed" data-tooltip> + <IconSquareRSS width="2rem" height="2rem" /> + <span class="sr-only">RSS feed</span> + </a> + </div> <ul class="my-6 flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal"> { posts.map((post, i) => ( diff --git a/src/pages/feed.xml.ts b/src/pages/feed.xml.ts index 9db1738..ac0da5b 100644 --- a/src/pages/feed.xml.ts +++ b/src/pages/feed.xml.ts @@ -1,139 +1,9 @@ -import rss, { type RSSFeedItem } from "@astrojs/rss"; +import rss from "@astrojs/rss"; import type { APIRoute } from "astro"; -import { getCollection, getEntries, type CollectionEntry, type CollectionKey } from "astro:content"; -import { markdown } from "@astropub/md"; -import sanitizeHtml from "sanitize-html"; -import { t, type Lang } from "@i18n"; -import { markdownToPlaintext } from "@utils/markdown_to_plaintext"; -import { getUsernameForLang } from "@utils/get_username_for_lang"; -import { qualifyLocalURLsInMarkdown } from "@utils/qualify_local_urls_in_markdown"; +import { getCollection } from "astro:content"; +import { blogFeedItem, gameFeedItem, storyFeedItem, type EntryWithPubDate } from "@utils/feed"; -type FeedItem = RSSFeedItem & - Required<Pick<RSSFeedItem, "title" | "pubDate" | "link" | "description" | "categories" | "content">>; - -type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } }; - -const MAX_ITEMS = 6; - -function toNoonUTCDate(date: Date) { - const adjustedDate = new Date(date); - adjustedDate.setUTCHours(12); - return adjustedDate; -} - -const getLinkForUser = (user: CollectionEntry<"users">, lang: Lang) => { - const userName = getUsernameForLang(user, lang); - if (user.data.preferredLink) { - return `<a href="${user.data.links[user.data.preferredLink]!.link}">${userName}</a>`; - } - return userName; -}; - -async function storyFeedItem( - site: URL | undefined, - data: EntryWithPubDate<"stories">["data"], - slug: CollectionEntry<"stories">["slug"], - body: string, -): Promise<FeedItem> { - return { - title: `New story! "${data.title}"`, - pubDate: toNoonUTCDate(data.pubDate), - link: `/stories/${slug}`, - description: - `${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}\n\n${markdownToPlaintext(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`.replaceAll( - /[\n ]+/g, - " ", - ), - categories: ["story"], - commentsUrl: data.posts.mastodon?.link, - content: sanitizeHtml( - `<h1>${data.title}</h1>` + - `<p>${t( - data.lang, - "story/authors", - (await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)), - )}</p>` + - (data.requesters - ? `<p>${t( - data.lang, - "story/requested_by", - (await getEntries(data.requesters)).map((requester) => getLinkForUser(requester, data.lang)), - )}</p>` - : "") + - (data.commissioners - ? `<p>${t( - data.lang, - "story/commissioned_by", - (await getEntries(data.commissioners)).map((commissioner) => getLinkForUser(commissioner, data.lang)), - )}</p>` - : "") + - `<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}</em></p>` + - `<hr>${await markdown(body)}` + - `<hr>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`, - ), - }; -} - -async function gameFeedItem( - site: URL | undefined, - data: EntryWithPubDate<"games">["data"], - slug: CollectionEntry<"games">["slug"], - body: string, -): Promise<FeedItem> { - return { - title: `New game! "${data.title}"`, - pubDate: toNoonUTCDate(data.pubDate), - link: `/games/${slug}`, - description: - `${t(data.lang, "game/warnings", data.platforms, data.contentWarning)}\n\n${markdownToPlaintext(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`.replaceAll( - /[\n ]+/g, - " ", - ), - categories: ["game"], - commentsUrl: data.posts.mastodon?.link, - content: sanitizeHtml( - `<h1>${data.title}</h1>` + - `<p>${t( - data.lang, - "story/authors", - (await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)), - )}</p>` + - `<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` + - `<hr><p><em>${data.contentWarning}</em></p>` + - `<hr>${await markdown(body)}` + - `<hr>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`, - ), - }; -} - -async function blogFeedItem( - site: URL | undefined, - data: EntryWithPubDate<"blog">["data"], - slug: CollectionEntry<"blog">["slug"], - body: string, -): Promise<FeedItem> { - return { - title: `New blog post! "${data.title}"`, - pubDate: toNoonUTCDate(data.pubDate), - link: `/blog/${slug}`, - description: markdownToPlaintext(await qualifyLocalURLsInMarkdown(data.description, data.lang, site)).replaceAll( - /[\n ]+/g, - " ", - ), - categories: ["blog post"], - commentsUrl: data.posts.mastodon?.link, - content: sanitizeHtml( - `<h1>${data.title}</h1>` + - `<p>${t( - data.lang, - "story/authors", - (await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)), - )}</p>` + - `<hr>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}` + - `<hr>${await markdown(body)}`, - ), - }; -} +const MAX_ITEMS = 8; export const GET: APIRoute = async ({ site }) => { const stories = ( @@ -157,21 +27,21 @@ export const GET: APIRoute = async ({ site }) => { return rss({ title: "Gallery | Bad Manners", - description: "Stories, games, and (possibly) more by Bad Manners", + description: "Stories, games, and more by Bad Manners.", site: site!, items: await Promise.all( [ - stories.map(({ data, slug, body }) => ({ + stories.map(({ data, slug, render }) => ({ date: data.pubDate, - fn: () => storyFeedItem(site, data, slug, body), + fn: async () => storyFeedItem(site, data, slug, (await render()).Content), })), - games.map(({ data, slug, body }) => ({ + games.map(({ data, slug, render }) => ({ date: data.pubDate, - fn: () => gameFeedItem(site, data, slug, body), + fn: async () => gameFeedItem(site, data, slug, (await render()).Content), })), - posts.map(({ data, slug, body }) => ({ + posts.map(({ data, slug, render }) => ({ date: data.pubDate, - fn: () => blogFeedItem(site, data, slug, body), + fn: async () => blogFeedItem(site, data, slug, (await render()).Content), })), ] .flat() diff --git a/src/pages/games/feed.xml.ts b/src/pages/games/feed.xml.ts new file mode 100644 index 0000000..b637f94 --- /dev/null +++ b/src/pages/games/feed.xml.ts @@ -0,0 +1,23 @@ +import rss from "@astrojs/rss"; +import type { APIRoute } from "astro"; +import { getCollection } from "astro:content"; +import { gameFeedItem, type EntryWithPubDate } from "@utils/feed"; + +const MAX_ITEMS = 8; + +export const GET: APIRoute = async ({ site }) => { + const stories = ( + (await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)) as EntryWithPubDate<"games">[] + ) + .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()) + .slice(0, MAX_ITEMS); + + return rss({ + title: "Games | Bad Manners", + description: "Games by Bad Manners.", + site: new URL("/games", site!), + items: await Promise.all( + stories.map(async ({ data, slug, render }) => gameFeedItem(site, data, slug, (await render()).Content)), + ), + }); +}; diff --git a/src/pages/games.astro b/src/pages/games/index.astro similarity index 89% rename from src/pages/games.astro rename to src/pages/games/index.astro index 4375f6a..0df3c1b 100644 --- a/src/pages/games.astro +++ b/src/pages/games/index.astro @@ -4,6 +4,7 @@ import { getCollection, getEntries, type CollectionEntry } from "astro:content"; import GalleryLayout from "@layouts/GalleryLayout.astro"; import { t } from "@i18n"; import UserComponent from "@components/UserComponent.astro"; +import { IconSquareRSS } from "@components/icons"; type GameWithPubDate = CollectionEntry<"games"> & { data: { pubDate: Date } }; @@ -21,7 +22,13 @@ const games = await Promise.all( <meta slot="head" property="og:description" content="Bad Manners || A game that I've gone and done." /> <h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Games</h1> <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" /> - <p class="p-summary my-4">A game that I've gone and done.</p> + <div class="my-4 flex w-full"> + <p class="p-summary grow">A game that I've gone and done.</p> + <a class="u-url text-link ml-2 mr-10 p-2" href="/games/feed.xml" rel="alternate" title="RSS feed" data-tooltip> + <IconSquareRSS width="2rem" height="2rem" /> + <span class="sr-only">RSS feed</span> + </a> + </div> <ul class="my-6 flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal"> { games.map((game, i) => ( diff --git a/src/pages/index.astro b/src/pages/index.astro index cbe7ebd..2777fa5 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -6,6 +6,7 @@ import GalleryLayout from "@layouts/GalleryLayout.astro"; import { t, type Lang } from "@i18n"; import UserComponent from "@components/UserComponent.astro"; import { markdownToPlaintext } from "@utils/markdown_to_plaintext"; +import { IconSquareRSS } from "@components/icons"; const MAX_ITEMS = 10; @@ -98,22 +99,17 @@ const latestItems: LatestItemsEntry[] = await Promise.all( <h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Gallery</h1> <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" /> <div class="p-summary"> - <p class="my-4"> - Hey there, welcome to my corner of the Internet! You can expect lots of safe vore/endosoma ahead. - </p> - <ul class="list-disc pl-8"> - <li><a class="text-link underline" href="/stories/1">Read my stories!</a></li> - <li><a class="text-link underline" href="/games/crossing-over">Play my visual novel!</a></li> - <li><a class="text-link underline" href="/tags">Find all content with a certain tag!</a></li> - </ul> - <p class="my-4"> - For more information about me, please check out <a - class="text-link underline" - href="https://badmanners.xyz/" - data-age-restricted - rel="me">my main website</a - >. - </p> + <div class="my-4 flex"> + <p class="grow"> + Hey there, welcome to my corner of the Internet! This is where I'll share all of the safe vore and endosoma + content that I'll make. You can check the latest uploads below, or use the navigation bar to dig through all of + my content. + </p> + <a class="u-url text-link ml-2 mr-10 p-2" href="/feed.xml" rel="alternate" title="RSS feed" data-tooltip> + <IconSquareRSS width="2rem" height="2rem" /> + <span class="sr-only">RSS feed</span> + </a> + </div> </div> <section class="my-2" aria-labelledby="latest-uploads"> <h2 id="latest-uploads" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">Latest uploads</h2> diff --git a/src/pages/stories/[page].astro b/src/pages/stories/[page].astro index 58144c4..fa2823f 100644 --- a/src/pages/stories/[page].astro +++ b/src/pages/stories/[page].astro @@ -5,6 +5,7 @@ import { getCollection, getEntries, type CollectionEntry } from "astro:content"; import GalleryLayout from "@layouts/GalleryLayout.astro"; import { t } from "@i18n"; import UserComponent from "@components/UserComponent.astro"; +import { IconSquareRSS } from "@components/icons"; type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } }; @@ -30,18 +31,22 @@ const totalPages = Math.ceil(page.total / page.size); <GalleryLayout pageTitle="Stories" class="h-feed"> <meta slot="head" property="og:description" content={`Bad Manners || ${page.total} stories.`} /> - <h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Stories</h1> + <h1 class="p-name m-2 grow text-3xl font-semibold text-stone-800 dark:text-stone-100">Stories</h1> <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" /> - <div class="p-summary"> - <p class="my-4">The bulk of my content!</p> - <p class="text-center font-light text-stone-950 dark:text-white"> - { - page.start === page.end - ? `Displaying story #${page.start + 1}` - : `Displaying stories #${page.start + 1}–${page.end + 1}` - } / {page.total} - </p> + <div class="my-4 flex"> + <p class="p-summary grow">The bulk of my content!</p> + <a class="u-url text-link ml-2 mr-10 p-2" href="/stories/feed.xml" rel="alternate" title="RSS feed" data-tooltip> + <IconSquareRSS width="2rem" height="2rem" /> + <span class="sr-only">RSS feed</span> + </a> </div> + <p class="text-center font-light text-stone-950 dark:text-white"> + { + page.start === page.end + ? `Displaying story #${page.start + 1}` + : `Displaying stories #${page.start + 1}–${page.end + 1}` + } / {page.total} + </p> <div class="mx-auto mb-6 mt-2 flex w-fit rounded-lg border border-stone-400 dark:border-stone-500"> { page.url.prev && ( diff --git a/src/pages/stories/feed.xml.ts b/src/pages/stories/feed.xml.ts new file mode 100644 index 0000000..5388778 --- /dev/null +++ b/src/pages/stories/feed.xml.ts @@ -0,0 +1,26 @@ +import rss from "@astrojs/rss"; +import type { APIRoute } from "astro"; +import { getCollection } from "astro:content"; +import { storyFeedItem, type EntryWithPubDate } from "@utils/feed"; + +const MAX_ITEMS = 8; + +export const GET: APIRoute = async ({ site }) => { + const stories = ( + (await getCollection( + "stories", + (story) => !story.data.isDraft && story.data.pubDate, + )) as EntryWithPubDate<"stories">[] + ) + .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()) + .slice(0, MAX_ITEMS); + + return rss({ + title: "Stories | Bad Manners", + description: "Stories by Bad Manners.", + site: new URL("/stories/1", site!), + items: await Promise.all( + stories.map(async ({ data, slug, render }) => storyFeedItem(site, data, slug, (await render()).Content)), + ), + }); +}; diff --git a/src/utils/feed.ts b/src/utils/feed.ts new file mode 100644 index 0000000..c77b5e6 --- /dev/null +++ b/src/utils/feed.ts @@ -0,0 +1,139 @@ +import type { RSSFeedItem } from "@astrojs/rss"; +import { getEntries, getEntry, type CollectionEntry, type CollectionKey, type Render } from "astro:content"; +import { experimental_AstroContainer } from "astro/container"; +import sanitizeHtml from "sanitize-html"; +import { getUsernameForLang } from "./get_username_for_lang"; +import { markdownToPlaintext } from "./markdown_to_plaintext"; +import { qualifyLocalURLsInMarkdown } from "./qualify_local_urls_in_markdown"; +import { markdown } from "@astropub/md"; +import { t, type Lang } from "@i18n"; +import type { AstroComponentFactory } from "astro/runtime/server/index.js"; +import mdxRenderer from "astro/jsx/server.js"; + +export type FeedItem = RSSFeedItem & + Required<Pick<RSSFeedItem, "title" | "pubDate" | "link" | "description" | "categories" | "content">>; + +export type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } }; + +const container = await experimental_AstroContainer.create(); +container.addServerRenderer({ renderer: mdxRenderer } as any); + +function toNoonUTCDate(date: Date) { + const adjustedDate = new Date(date); + adjustedDate.setUTCHours(12); + return adjustedDate; +} + +const getLinkForUser = (user: CollectionEntry<"users">, lang: Lang) => { + const userName = getUsernameForLang(user, lang); + if (user.data.preferredLink) { + return `<a href="${user.data.links[user.data.preferredLink]!.link}">${userName}</a>`; + } + return userName; +}; + +export async function storyFeedItem( + site: URL | undefined, + data: EntryWithPubDate<"stories">["data"], + slug: CollectionEntry<"stories">["slug"], + content: AstroComponentFactory, +): Promise<FeedItem> { + return { + title: `New story! "${data.title}"`, + pubDate: toNoonUTCDate(data.pubDate), + link: `/stories/${slug}`, + description: + `${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}\n\n${markdownToPlaintext(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`.replaceAll( + /[\n ]+/g, + " ", + ), + categories: ["story"], + commentsUrl: data.posts.mastodon?.link, + content: sanitizeHtml( + `<h1>${data.title}</h1>` + + `<p>${t( + data.lang, + "story/authors", + (await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)), + )}</p>` + + (data.requesters + ? `<p>${t( + data.lang, + "story/requested_by", + (await getEntries(data.requesters)).map((requester) => getLinkForUser(requester, data.lang)), + )}</p>` + : "") + + (data.commissioners + ? `<p>${t( + data.lang, + "story/commissioned_by", + (await getEntries(data.commissioners)).map((commissioner) => getLinkForUser(commissioner, data.lang)), + )}</p>` + : "") + + `<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}</em></p>` + + `<hr>${await container.renderToString(content)}` + + `<hr>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`, + ), + }; +} + +export async function gameFeedItem( + site: URL | undefined, + data: EntryWithPubDate<"games">["data"], + slug: CollectionEntry<"games">["slug"], + content: AstroComponentFactory, +): Promise<FeedItem> { + return { + title: `New game! "${data.title}"`, + pubDate: toNoonUTCDate(data.pubDate), + link: `/games/${slug}`, + description: + `${t(data.lang, "game/warnings", data.platforms, data.contentWarning)}\n\n${markdownToPlaintext(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`.replaceAll( + /[\n ]+/g, + " ", + ), + categories: ["game"], + commentsUrl: data.posts.mastodon?.link, + content: sanitizeHtml( + `<h1>${data.title}</h1>` + + `<p>${t( + data.lang, + "story/authors", + (await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)), + )}</p>` + + `<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` + + `<hr><p><em>${data.contentWarning}</em></p>` + + `<hr>${await container.renderToString(content)}` + + `<hr>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`, + ), + }; +} + +export async function blogFeedItem( + site: URL | undefined, + data: EntryWithPubDate<"blog">["data"], + slug: CollectionEntry<"blog">["slug"], + content: AstroComponentFactory, +): Promise<FeedItem> { + return { + title: `New blog post! "${data.title}"`, + pubDate: toNoonUTCDate(data.pubDate), + link: `/blog/${slug}`, + description: markdownToPlaintext(await qualifyLocalURLsInMarkdown(data.description, data.lang, site)).replaceAll( + /[\n ]+/g, + " ", + ), + categories: ["blog post"], + commentsUrl: data.posts.mastodon?.link, + content: sanitizeHtml( + `<h1>${data.title}</h1>` + + `<p>${t( + data.lang, + "story/authors", + (await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)), + )}</p>` + + `<hr>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}` + + `<hr>${await container.renderToString(content)}`, + ), + }; +}