Add "Playing It Safe" draft and improve type-checking

- Remove most type assertions and improve types
- Validate wordCount property
- Add "Vore Day" tag
- Add licenses
This commit is contained in:
Bad Manners 2024-08-03 20:33:49 -03:00
parent 17ef8c652c
commit fe908a4989
37 changed files with 1309 additions and 841 deletions

View file

@ -84,7 +84,8 @@ function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website)
}
break;
default:
throw new Error(`Unhandled Website "${website}"`);
let _: never = website;
throw new Error(`Unhandled website "${website}"`);
}
} else {
return link[1].replace(/^@/, "");
@ -174,9 +175,9 @@ export const getStaticPaths: GetStaticPaths = async () => {
export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) => {
const { lang } = story.data;
const copyrightedCharacters = await formatCopyrightedCharacters(story.data.copyrightedCharacters);
const authorsList = await getEntries([story.data.authors].flat());
const commissioner = story.data.commissioner && (await getEntry(story.data.commissioner));
const requester = story.data.requester && (await getEntry(story.data.requester));
const authorsList = await getEntries(story.data.authors);
const commissionersList = story.data.commissioner && (await getEntries(story.data.commissioner));
const requestersList = story.data.requester && (await getEntries(story.data.requester));
const description = Object.fromEntries(
WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => {
@ -185,14 +186,24 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
const storyDescription = (
[
story.data.description,
`*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())}*`,
`*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}*`,
t(
lang,
"export_story/writing",
authorsList.map((author) => u(author)),
),
requester && t(lang, "export_story/request_for", u(requester)),
commissioner && t(lang, "export_story/commissioned_by", u(commissioner)),
requestersList &&
t(
lang,
"export_story/request_for",
requestersList.map((requester) => u(requester)),
),
commissionersList &&
t(
lang,
"export_story/commissioned_by",
commissionersList.map((commissioner) => u(commissioner)),
),
...copyrightedCharacters.map(([user, characterList]) =>
characterList[0] == ""
? t(lang, "characters/all_characters_are_copyrighted_by", u(user))
@ -206,12 +217,7 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
(_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
);
if (exportFormat === "bbcode") {
return [
website,
markdownToBbcode(storyDescription)
.replaceAll(/\n\n\n+/g, "\n\n")
.trim(),
];
return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n")];
} else if (exportFormat === "markdown") {
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
} else {
@ -227,8 +233,20 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
"story/authors",
authorsList.map((author) => getUsernameForLang(author, lang)),
)}\n` +
(commissioner ? `${t(lang, "story/commissioned_by", getUsernameForLang(commissioner, lang))}\n` : "") +
(requester ? `${t(lang, "story/requested_by", getUsernameForLang(requester, lang))}\n` : "");
(requestersList
? `${t(
lang,
"story/requested_by",
requestersList.map((requester) => getUsernameForLang(requester, lang)),
)}\n`
: "") +
(commissionersList
? `${t(
lang,
"story/commissioned_by",
commissionersList.map((commissioner) => getUsernameForLang(commissioner, lang)),
)}\n`
: "");
const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll(/\\([=*])/g, "$1")}`
.replaceAll(/\n\n\n+/g, "\n\n")

View file

@ -1,10 +1,12 @@
import type { APIRoute } from "astro";
const content = { isAlive: true };
const headers = { "Content-Type": "application/json; charset=utf-8" };
export const GET: APIRoute = () => {
if (import.meta.env.PROD) {
return new Response(null, { status: 404 });
}
return new Response(JSON.stringify({ isAlive: true }), {
headers: { "Content-Type": "application/json; charset=utf-8" },
});
return new Response(JSON.stringify(content), { headers });
};

View file

@ -1,6 +1,6 @@
import rss, { type RSSFeedItem } from "@astrojs/rss";
import type { APIRoute } from "astro";
import { getCollection, type CollectionEntry } from "astro:content";
import { getCollection, getEntries, type CollectionEntry, type CollectionKey } from "astro:content";
import { markdown } from "@astropub/md";
import sanitizeHtml from "sanitize-html";
import { t } from "../i18n";
@ -8,11 +8,12 @@ import type { Lang } from "../content/config";
import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
import { getUsernameForLang } from "../utils/get_username_for_lang";
type FeedItem = RSSFeedItem & {
pubDate: Date;
};
type FeedItem = RSSFeedItem &
Required<Pick<RSSFeedItem, "title" | "pubDate" | "link" | "description" | "categories" | "content">>;
const MAX_ITEMS = 8;
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
const MAX_ITEMS = 6;
function toNoonUTCDate(date: Date) {
const adjustedDate = new Date(date);
@ -30,13 +31,19 @@ const getLinkForUser = (user: CollectionEntry<"users">, lang: Lang) => {
};
export const GET: APIRoute = async ({ site }) => {
const stories = (await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate))
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime())
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))
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime())
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 users = await getCollection("users");
return rss({
title: "Gallery | Bad Manners",
@ -46,34 +53,38 @@ export const GET: APIRoute = async ({ site }) => {
await Promise.all(
stories.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({
title: `New story! "${data.title}"`,
pubDate: toNoonUTCDate(data.pubDate!),
pubDate: toNoonUTCDate(data.pubDate),
link: `/stories/${slug}`,
description:
`${t(data.lang, "story/warnings", data.wordCount, data.contentWarning.trim())}\n\n${markdownToPlaintext(data.description)}`
.replaceAll(/[\n ]+/g, " ")
.trim(),
`${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}\n\n${markdownToPlaintext(data.description)}`.replaceAll(
/[\n ]+/g,
" ",
),
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),
);
}),
(await getEntries(data.authors)).map((author) => getLinkForUser(author, 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>`
? `<p>${t(
data.lang,
"story/requested_by",
(await getEntries(data.requester)).map((requester) => getLinkForUser(requester, 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>`
? `<p>${t(
data.lang,
"story/commissioned_by",
(await getEntries(data.commissioner)).map((commissioner) =>
getLinkForUser(commissioner, data.lang),
),
)}</p>`
: "") +
`<hr><p><em>${t(data.lang, "story/warnings", data.wordCount || "???", data.contentWarning.trim())}</em></p>` +
`<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}</em></p>` +
`<hr>${await markdown(body)}` +
`<hr>${await markdown(data.description)}`,
),
@ -82,29 +93,23 @@ export const GET: APIRoute = async ({ site }) => {
await Promise.all(
games.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({
title: `New game! "${data.title}"`,
pubDate: toNoonUTCDate(data.pubDate!),
pubDate: toNoonUTCDate(data.pubDate),
link: `/games/${slug}`,
description:
`${t(data.lang, "game/warnings", data.platforms, data.contentWarning)}\n\n${markdownToPlaintext(data.description)}`
.replaceAll(/[\n ]+/g, " ")
.trim(),
`${t(data.lang, "game/warnings", data.platforms, data.contentWarning)}\n\n${markdownToPlaintext(data.description)}`.replaceAll(
/[\n ]+/g,
" ",
),
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),
);
}),
(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.trim()}</em></p>` +
`<hr><p><em>${data.contentWarning}</em></p>` +
`<hr>${await markdown(body)}` +
`<hr>${await markdown(data.description)}`,
),

View file

@ -1,12 +1,14 @@
---
import { Image } from "astro:assets";
import { getCollection } from "astro:content";
import { getCollection, type CollectionEntry } from "astro:content";
import GalleryLayout from "../layouts/GalleryLayout.astro";
import { t } from "../i18n";
const games = (await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)).sort(
(a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime(),
);
type GameWithPubDate = CollectionEntry<"games"> & { data: { pubDate: Date } };
const games = (
(await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)) as GameWithPubDate[]
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
---
<GalleryLayout pageTitle="Games">
@ -20,7 +22,7 @@ const games = (await getCollection("games", (game) => !game.data.isDraft && game
<a
class="text-link hover:underline focus:underline"
href={`/games/${game.slug}`}
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim())}
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning)}
>
{game.data.thumbnail ? (
<div class="flex aspect-[630/500] max-w-[288px] justify-center">
@ -32,7 +34,7 @@ const games = (await getCollection("games", (game) => !game.data.isDraft && game
<span>{game.data.title}</span>
<br />
<span class="italic">
{game.data.pubDate!.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
{game.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
</span>
</>
</div>

View file

@ -18,6 +18,17 @@ export const getStaticPaths: GetStaticPaths = async () => {
};
const game = Astro.props;
if (!game.data.isDraft) {
if (!game.data.pubDate) {
throw new Error(`Missing "pubDate" for published game ${game.data.title} ("${game.slug}")`);
}
if (!game.data.thumbnail) {
throw new Error(`Missing "thumbnail" for published game ${game.data.title} ("${game.slug}")`);
}
if (game.data.tags.length == 0) {
throw new Error(`Missing "tags" for published game ${game.data.title} ("${game.slug}")`);
}
}
const { Content } = await game.render();
---

View file

@ -1,5 +1,5 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import { type CollectionEntry, getCollection, type CollectionKey } from "astro:content";
import { Image } from "astro:assets";
import GalleryLayout from "../layouts/GalleryLayout.astro";
import { t } from "../i18n";
@ -15,11 +15,20 @@ interface LatestItemsEntry {
pubDate: Date;
}
const stories = (await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate))
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime())
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
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))
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime())
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 latestItems: LatestItemsEntry[] = [
@ -28,16 +37,16 @@ const latestItems: LatestItemsEntry[] = [
thumbnail: story.data.thumbnail,
href: `/stories/${story.slug}`,
title: story.data.title,
altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim()),
pubDate: story.data.pubDate!,
altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning),
pubDate: story.data.pubDate,
})),
games.map<LatestItemsEntry>((game) => ({
type: "Game",
thumbnail: game.data.thumbnail,
href: `/games/${game.slug}`,
title: game.data.title,
altText: t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim()),
pubDate: game.data.pubDate!,
altText: t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning),
pubDate: game.data.pubDate,
})),
]
.flat()
@ -48,11 +57,18 @@ const latestItems: LatestItemsEntry[] = [
<GalleryLayout pageTitle="Gallery">
<meta slot="head-description" property="og:description" content="Bad Manners || Welcome to my gallery!" />
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">My gallery</h1>
<p class="my-4">Welcome to my gallery! Use the navigation menu to navigate through my content.</p>
<p class="my-4 block md:hidden">
Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu above to navigate
through my content.
</p>
<p class="my-4 hidden md:block" aria-hidden>
Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu to the left to
navigate through my content.
</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>
<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

View file

@ -3,6 +3,8 @@ import type { APIRoute } from "astro";
const licenses = `
The briefcase logo and any unattributed characters are copyrighted and trademarked by me.
The source code of this website is licensed under the MIT License. The content hosted on it (i.e. stories, games, and respective thumbnails/images) is copyrighted by me and distributed under the CC BY-NC-ND 4.0 license.
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.
@ -10,8 +12,8 @@ The generic SVG icons were created by Font Awesome and are distributed under the
All third-party trademarks and attributed characters belong to their respective owners, and I'm not affiliated with any of them.
`.trim();
const headers = { "Content-Type": "text/plain; charset=utf-8" };
export const GET: APIRoute = () => {
return new Response(licenses, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
return new Response(licenses, { headers });
};

View file

@ -1,6 +1,7 @@
---
import type { GetStaticPaths } from "astro";
import { type CollectionEntry, getCollection } from "astro:content";
import getReadingTime from "reading-time";
import StoryLayout from "../../layouts/StoryLayout.astro";
type Props = CollectionEntry<"stories">;
@ -18,6 +19,28 @@ export const getStaticPaths: GetStaticPaths = async () => {
};
const story = Astro.props;
const readingTime = getReadingTime(story.body);
if (!story.data.isDraft) {
if (!story.data.wordCount) {
throw new Error(`Missing "wordCount" for published story ${story.data.title} ("${story.slug}")`);
}
if (!story.data.pubDate) {
throw new Error(`Missing "pubDate" for published story ${story.data.title} ("${story.slug}")`);
}
if (!story.data.thumbnail) {
throw new Error(`Missing "thumbnail" for published story ${story.data.title} ("${story.slug}")`);
}
if (story.data.tags.length == 0) {
throw new Error(`Missing "tags" for published story ${story.data.title} ("${story.slug}")`);
}
}
if (story.data.wordCount && Math.abs(story.data.wordCount - readingTime.words) >= 135) {
console.warn(
`"wordCount" differs greatly from actual word count for published story ${story.data.title} ("${story.slug}") ` +
`(expected ~${story.data.wordCount}, found ${readingTime.words})`,
);
}
const { Content } = await story.render();
---

View file

@ -6,14 +6,16 @@ import GalleryLayout from "../../layouts/GalleryLayout.astro";
import type { CollectionEntry } from "astro:content";
import { t } from "../../i18n";
type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } };
type Props = {
page: Page<CollectionEntry<"stories">>;
page: Page<StoryWithPubDate>;
};
export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
const stories = (await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate)).sort(
(a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime(),
);
const stories = (
(await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate)) as StoryWithPubDate[]
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
return paginate(stories, { pageSize: 30 }) satisfies { props: Props }[];
};
@ -71,7 +73,7 @@ const totalPages = Math.ceil(page.total / page.size);
<a
class="text-link hover:underline focus:underline"
href={`/stories/${story.slug}`}
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())}
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}
>
{story.data.thumbnail ? (
<div class="flex aspect-square max-w-[192px] justify-center">
@ -87,7 +89,7 @@ const totalPages = Math.ceil(page.total / page.size);
<span>{story.data.title}</span>
<br />
<span class="italic">
{story.data.pubDate!.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
{story.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
</span>
</div>
</a>

View file

@ -1,20 +1,22 @@
---
import { getCollection, getEntry } from "astro:content";
import { getCollection, getEntry, type CollectionEntry } from "astro:content";
import { Image } from "astro:assets";
import GalleryLayout from "../../layouts/GalleryLayout.astro";
import mapImage from "../../assets/images/tlotm_map.jpg";
type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } };
const series = await getEntry("series", "the-lost-of-the-marshes");
const stories = await getCollection(
const stories = (await getCollection(
"stories",
(story) => !story.data.isDraft && story.data.pubDate && story.data.series?.id === series.id,
);
)) as StoryWithPubDate[];
const mainChapters = stories
.filter((story) => story.slug.startsWith("the-lost-of-the-marshes/chapter-"))
.sort((a, b) => a.data.pubDate!.getTime() - b.data.pubDate!.getTime());
.sort((a, b) => a.data.pubDate.getTime() - b.data.pubDate.getTime());
const bonusChapters = stories
.filter((story) => story.slug.startsWith("the-lost-of-the-marshes/bonus-"))
.sort((a, b) => a.data.pubDate!.getTime() - b.data.pubDate!.getTime());
.sort((a, b) => a.data.pubDate.getTime() - b.data.pubDate.getTime());
const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summary);
---

View file

@ -48,7 +48,7 @@ const categorizedTags = tagCategories
})
.map((category) => {
const tagList = category.data.tags.map<Tag>(({ name, description }) => {
description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ").trim();
description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ");
const tag = typeof name === "string" ? name : name["eng"];
const id = slug(tag);
return { id, name: tag, description };

View file

@ -0,0 +1,28 @@
---
import type { GetStaticPaths } from "astro";
import { slug } from "github-slugger";
import GalleryLayout from "../../layouts/GalleryLayout.astro";
type Props = {
badTag: string;
};
type Params = {
badSlug: string;
};
export const getStaticPaths: GetStaticPaths = () => {
return ["digestion", "fatal vore"].map((badTag) => ({
params: { badSlug: slug(badTag) } satisfies Params,
props: { badTag } satisfies Props,
}));
};
const { badTag } = Astro.props;
---
<GalleryLayout pageTitle={`Works tagged "${badTag}"`}>
<meta slot="head-description" content="No." property="og:description" />
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{badTag}"</h1>
<p class="my-4">No.</p>
</GalleryLayout>

View file

@ -1,7 +1,7 @@
---
import type { GetStaticPaths } from "astro";
import { Image } from "astro:assets";
import { type CollectionEntry, getCollection } from "astro:content";
import { type CollectionEntry, getCollection, type CollectionKey } from "astro:content";
import { Markdown } from "@astropub/md";
import { slug } from "github-slugger";
import GalleryLayout from "../../layouts/GalleryLayout.astro";
@ -9,12 +9,14 @@ import Prose from "../../components/Prose.astro";
import { t } from "../../i18n";
import { DEFAULT_LANG } from "../../i18n";
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
type Props = {
tag: string;
description?: string;
related?: string[];
stories: CollectionEntry<"stories">[];
games: CollectionEntry<"games">[];
stories: EntryWithPubDate<"stories">[];
games: EntryWithPubDate<"games">[];
};
type Params = {
@ -74,12 +76,16 @@ export const getStaticPaths: GetStaticPaths = async () => {
tag,
description: tagDescriptions[tag]?.description,
related: tagDescriptions[tag]?.related,
stories: stories
.filter((story) => !story.data.isDraft && story.data.pubDate && story.data.tags.includes(tag))
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime()),
games: games
.filter((game) => !game.data.isDraft && game.data.pubDate && game.data.tags.includes(tag))
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime()),
stories: (
stories.filter(
(story) => !story.data.isDraft && story.data.pubDate && story.data.tags.includes(tag),
) as EntryWithPubDate<"stories">[]
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()),
games: (
games.filter(
(game) => !game.data.isDraft && game.data.pubDate && game.data.tags.includes(tag),
) as EntryWithPubDate<"games">[]
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()),
} satisfies Props,
}));
};
@ -119,7 +125,7 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
<a
class="text-link hover:underline focus:underline"
href={`/stories/${story.slug}`}
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())}
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}
>
{story.data.thumbnail ? (
<div class="flex aspect-square max-w-[192px] justify-center">
@ -151,7 +157,7 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
<a
class="text-link hover:underline focus:underline"
href={`/games/${game.slug}`}
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim())}
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning)}
>
{game.data.thumbnail ? (
<div class="flex aspect-[630/500] max-w-[192px] justify-center">

View file

@ -1,11 +0,0 @@
---
import GalleryLayout from "../../layouts/GalleryLayout.astro";
const tag = "Digestion";
---
<GalleryLayout pageTitle={`Works tagged "${tag}"`}>
<meta slot="head-description" content="No." property="og:description" />
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{tag}"</h1>
<p class="my-4">No.</p>
</GalleryLayout>