183 lines
6.3 KiB
TypeScript
183 lines
6.3 KiB
TypeScript
import rss, { type RSSFeedItem } 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";
|
|
|
|
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)}`,
|
|
),
|
|
};
|
|
}
|
|
|
|
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);
|
|
const games = (
|
|
(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);
|
|
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: "Gallery | Bad Manners",
|
|
description: "Stories, games, and (possibly) more by Bad Manners",
|
|
site: site!,
|
|
items: await Promise.all(
|
|
[
|
|
stories.map(({ data, slug, body }) => ({
|
|
date: data.pubDate,
|
|
fn: () => storyFeedItem(site, data, slug, body),
|
|
})),
|
|
games.map(({ data, slug, body }) => ({
|
|
date: data.pubDate,
|
|
fn: () => gameFeedItem(site, data, slug, body),
|
|
})),
|
|
posts.map(({ data, slug, body }) => ({
|
|
date: data.pubDate,
|
|
fn: () => blogFeedItem(site, data, slug, body),
|
|
})),
|
|
]
|
|
.flat()
|
|
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
|
.slice(0, MAX_ITEMS)
|
|
.map((value) => value.fn()),
|
|
),
|
|
});
|
|
};
|