From 568b7709ec14868c70f6c643848266fa0d1a3652 Mon Sep 17 00:00:00 2001
From: Bad Manners <me@badmanners.xyz>
Date: Wed, 3 Apr 2024 20:05:25 -0300
Subject: [PATCH] Improve feed items

---
 src/i18n/index.ts                       | 12 ++--
 src/layouts/StoryLayout.astro           |  3 +-
 src/pages/api/export-story/[...slug].ts |  2 +-
 src/pages/feed.xml.ts                   | 96 +++++++++++++++++--------
 src/pages/licenses.txt.ts               |  5 +-
 5 files changed, 74 insertions(+), 44 deletions(-)

diff --git a/src/i18n/index.ts b/src/i18n/index.ts
index 51eed70..bb029f9 100644
--- a/src/i18n/index.ts
+++ b/src/i18n/index.ts
@@ -16,10 +16,6 @@ export const UI_STRINGS: Record<string, TranslationRecord> = {
           : `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`,
     tok: (names: string[]) => names.join(" en "),
   },
-  "export_story/warnings": {
-    eng: (wordCount: number | string, contentWarning: string) => `*Word count: ${wordCount}. ${contentWarning}*`,
-    tok: (_wordCount: number | string, contentWarning: string) => `*${contentWarning}*`,
-  },
   "export_story/writing": {
     eng: (authorsList: string[]) => `Writing: ${authorsList.join(" ")}`,
     tok: (authorsList: string[]) => `lipu ni li tan jan ni: ${authorsList.join(" en ")}`,
@@ -45,9 +41,9 @@ export const UI_STRINGS: Record<string, TranslationRecord> = {
     eng: "Toggle dark mode",
     tok: "o ante e kule lipu",
   },
-  "story/word_count": {
-    eng: (wordCount: string | number) => `Word count: ${wordCount}.`,
-    tok: "",
+  "story/warnings": {
+    eng: (wordCount: number | string, contentWarning: string) => `Word count: ${wordCount}. ${contentWarning}`,
+    tok: (_wordCount: number | string, contentWarning: string) => `${contentWarning}`,
   },
   "story/publish_date": {
     eng: (date: string) => date,
@@ -99,7 +95,7 @@ export const UI_STRINGS: Record<string, TranslationRecord> = {
     eng: (owner: string, charactersList: string[]) =>
       charactersList.length == 1
         ? `${charactersList[0]} is © ${owner}`
-        : `${(UI_STRINGS["util/join_names"]!["eng"] as (arg: string[]) => string)(charactersList)} are © ${owner}`,
+        : `${(UI_STRINGS["util/join_names"]!.eng as (arg: string[]) => string)(charactersList)} are © ${owner}`,
   },
   "characters/all_characters_are_copyrighted_by": {
     eng: (owner: string) => `All characters are © ${owner}`,
diff --git a/src/layouts/StoryLayout.astro b/src/layouts/StoryLayout.astro
index 3dd5829..449e11a 100644
--- a/src/layouts/StoryLayout.astro
+++ b/src/layouts/StoryLayout.astro
@@ -199,8 +199,7 @@ const thumbnail =
         }
         <div id="content-warning">
           <p>
-            {t(props.lang, "story/word_count", props.wordCount)}
-            {props.contentWarning}
+            {t(props.lang, "story/warnings", props.wordCount, props.contentWarning.trim())}
           </p>
         </div>
       </section>
diff --git a/src/pages/api/export-story/[...slug].ts b/src/pages/api/export-story/[...slug].ts
index e80e460..aff8832 100644
--- a/src/pages/api/export-story/[...slug].ts
+++ b/src/pages/api/export-story/[...slug].ts
@@ -235,7 +235,7 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
         const storyDescription = (
           [
             story.data.description,
-            t(lang, "export_story/warnings", story.data.wordCount, story.data.contentWarning.trim()),
+            `*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())}*`,
             t(
               lang,
               "export_story/writing",
diff --git a/src/pages/feed.xml.ts b/src/pages/feed.xml.ts
index 9e0cb0f..9539c75 100644
--- a/src/pages/feed.xml.ts
+++ b/src/pages/feed.xml.ts
@@ -23,10 +23,10 @@ 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 `<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))
@@ -42,37 +42,71 @@ export const GET: APIRoute = async ({ site }) => {
     description: "Stories, games, and (possibly) more by Bad Manners",
     site: site as URL,
     items: [
-      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: ${data.wordCount}. ${data.contentWarning} ${data.descriptionPlaintext || data.description}`
+      await Promise.all(
+        stories.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({
+          title: `New story! "${data.title}"`,
+          pubDate: toNoonUTCDate(data.pubDate),
+          link: `/stories/${slug}`,
+          description:
+            `${t(data.lang, "story/warnings", data.wordCount, data.contentWarning.trim())} ${data.descriptionPlaintext || data.description}`
+              .replaceAll(/[\n ]+/g, " ")
+              .trim(),
+          categories: ["story"],
+          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><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning.trim())}</em></p>` +
+              `<hr>${tinyDecode(await marked(body))}` +
+              `<hr>${tinyDecode(await marked(data.description))}`,
+          ),
+        })),
+      ),
+      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: ["story"],
-        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))}`),
-      }))),
+          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><p><em>${data.contentWarning.trim()}</em></p>` +
+              `<hr>${tinyDecode(await marked(body))}` +
+              `<hr>${tinyDecode(await marked(data.description))}`,
+          ),
+        })),
+      ),
     ]
       .flat()
       .sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime())
diff --git a/src/pages/licenses.txt.ts b/src/pages/licenses.txt.ts
index c5beff8..c7a467b 100644
--- a/src/pages/licenses.txt.ts
+++ b/src/pages/licenses.txt.ts
@@ -1,13 +1,14 @@
 import type { APIRoute } from "astro";
 
-const licenses = `The briefcase logo and any unattributed characters are copyrighted and trademarked by me.
+const licenses = `
+The briefcase logo and any unattributed characters are copyrighted and trademarked by me.
 
 The Noto Sans and Noto Serif typefaces are copyrighted to the Noto Project Authors and distributed under the SIL Open Font License v1.1.
 
 The generic SVG icons were created by Font Awesome and are distributed under the CC BY 4.0 license.
 
 All third-party trademarks belong to their respective owners, and I'm not affiliated with any of them.
-`;
+`.trim();
 
 export const GET: APIRoute = () => {
   return new Response(licenses, {