Add Mastodon links to new stories, add title texts, and improve tags
- Added Mastodon links to "Woofer Exploration" and "Rose's Binge" - Add title texts to stories and games thumbnails on index, stories, games, and tag pages - Add descriptions and related tags to tag pages
This commit is contained in:
parent
efcfce1e06
commit
a713adc1ec
34 changed files with 493 additions and 239 deletions
|
|
@ -1,13 +1,12 @@
|
|||
import type { APIRoute, GetStaticPaths } from "astro";
|
||||
import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro:content";
|
||||
import { marked, type RendererApi } from "marked";
|
||||
import { decode as tinyDecode } from "tiny-decode";
|
||||
import type { Lang, Website } from "../../../content/config";
|
||||
import { t } from "../../../i18n";
|
||||
import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
|
||||
import { markdownToBbcode } from "../../../utils/markdown_to_bbcode";
|
||||
|
||||
interface ExportWebsiteInfo {
|
||||
website: string;
|
||||
website: Website;
|
||||
exportFormat: "bbcode" | "markdown";
|
||||
}
|
||||
|
||||
|
|
@ -21,37 +20,6 @@ const WEBSITE_LIST = [
|
|||
|
||||
type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: infer K }> ? K : never;
|
||||
|
||||
//type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<[infer K, DescriptionExportFormat]> ? K : never;
|
||||
|
||||
const bbcodeRenderer: RendererApi = {
|
||||
strong: (text) => `[b]${text}[/b]`,
|
||||
em: (text) => `[i]${text}[/i]`,
|
||||
codespan: (code) => code,
|
||||
br: () => `\n\n`,
|
||||
link: (href, _, text) => `[url=${href}]${text}[/url]`,
|
||||
image: (href) => `[img]${href}[/img]`,
|
||||
text: (text) => text,
|
||||
paragraph: (text) => `\n${text}\n`,
|
||||
list: (body, ordered) => (ordered ? `\n[ol]\n${body}[/ol]\n` : `\n[ul]\n${body}[/ul]\n`),
|
||||
listitem: (text) => `[li]${text}[/li]\n`,
|
||||
blockquote: (quote) => `\n[quote]${quote}[/quote]\n`,
|
||||
code: (code) => `\n[code]${code}[/code]\n`,
|
||||
heading: (heading) => `\n${heading}\n`,
|
||||
table: (header, body) => `\n[table]\n${header}${body}[/table]\n`,
|
||||
tablerow: (content) => `[tr]\n${content}[/tr]\n`,
|
||||
tablecell: (content, { header }) => (header ? `[th]${content}[/th]\n` : `[td]${content}[/td]\n`),
|
||||
hr: () => `\n===\n`,
|
||||
del: () => {
|
||||
throw new Error("Not supported by bbcodeRenderer: del");
|
||||
},
|
||||
html: () => {
|
||||
throw new Error("Not supported by bbcodeRenderer: html");
|
||||
},
|
||||
checkbox: () => {
|
||||
throw new Error("Not supported by bbcodeRenderer: checkbox");
|
||||
},
|
||||
};
|
||||
|
||||
function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website): string {
|
||||
const link = user.data.links[website];
|
||||
if (link) {
|
||||
|
|
@ -147,7 +115,7 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
|
|||
if ("weasyl" in user.data.links) {
|
||||
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
|
||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||
return `<fa:${getUsernameForWebsite(user, "furaffinity")}>`;
|
||||
return `<fa:${getUsernameForWebsite(user, "furaffinity").replaceAll("_", "")}>`;
|
||||
} else if (isPreferredWebsite(user, "inkbunny")) {
|
||||
return `<ib:${getUsernameForWebsite(user, "inkbunny")}>`;
|
||||
} else if (isPreferredWebsite(user, "sofurry")) {
|
||||
|
|
@ -220,47 +188,40 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
|||
const anonymousUser = await getEntry("users", "anonymous");
|
||||
const anonymousFallback = getNameForUser(anonymousUser, anonymousUser, lang);
|
||||
|
||||
const description: Record<ExportWebsiteName, string> = Object.fromEntries(
|
||||
await Promise.all(
|
||||
WEBSITE_LIST.map(async ({ website, exportFormat }) => {
|
||||
const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website, anonymousFallback);
|
||||
const storyDescription = (
|
||||
[
|
||||
story.data.description,
|
||||
`*${t(lang, "story/warnings", story.data.wordCount || "???", story.data.contentWarning.trim())}*`,
|
||||
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)),
|
||||
...copyrightedCharacters.map(([user, characterList]) =>
|
||||
characterList[0] == ""
|
||||
? t(lang, "characters/all_characters_are_copyrighted_by", u(user))
|
||||
: t(lang, "characters/characters_are_copyrighted_by", u(user), characterList),
|
||||
),
|
||||
].filter((data) => data) as string[]
|
||||
)
|
||||
.join("\n\n")
|
||||
.replaceAll(
|
||||
/\[([^\]]+)\]\((\/[^\)]+)\)/g,
|
||||
(_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
|
||||
);
|
||||
if (exportFormat === "bbcode") {
|
||||
return [
|
||||
website,
|
||||
tinyDecode(await marked.use({ renderer: bbcodeRenderer }).parse(storyDescription))
|
||||
.replaceAll(/\n\n\n+/g, "\n\n")
|
||||
.trim(),
|
||||
];
|
||||
}
|
||||
if (exportFormat === "markdown") {
|
||||
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
||||
}
|
||||
throw new Error(`Unhandled ExportFormat "${exportFormat}"`);
|
||||
}),
|
||||
),
|
||||
const description = Object.fromEntries(
|
||||
WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => {
|
||||
const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website, anonymousFallback);
|
||||
const storyDescription = (
|
||||
[
|
||||
story.data.description,
|
||||
`*${t(lang, "story/warnings", story.data.wordCount || "???", story.data.contentWarning.trim())}*`,
|
||||
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)),
|
||||
...copyrightedCharacters.map(([user, characterList]) =>
|
||||
characterList[0] == ""
|
||||
? t(lang, "characters/all_characters_are_copyrighted_by", u(user))
|
||||
: t(lang, "characters/characters_are_copyrighted_by", u(user), characterList),
|
||||
),
|
||||
].filter((data) => data) as string[]
|
||||
)
|
||||
.join("\n\n")
|
||||
.replaceAll(
|
||||
/\[([^\]]+)\]\((\/[^\)]+)\)/g,
|
||||
(_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
|
||||
);
|
||||
if (exportFormat === "bbcode") {
|
||||
return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
||||
} else if (exportFormat === "markdown") {
|
||||
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
||||
} else {
|
||||
throw new Error(`Unhandled export format "${exportFormat}"`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const storyHeader =
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const GET: APIRoute = async ({ site }) => {
|
|||
pubDate: toNoonUTCDate(data.pubDate!),
|
||||
link: `/games/${slug}`,
|
||||
description:
|
||||
`${t(data.lang, "game/platforms", data.platforms)}. ${data.contentWarning} ${data.descriptionPlaintext || data.description}`
|
||||
`${t(data.lang, "game/warnings", data.platforms, data.contentWarning)} ${data.descriptionPlaintext || data.description}`
|
||||
.replaceAll(/[\n ]+/g, " ")
|
||||
.trim(),
|
||||
categories: ["game"],
|
||||
|
|
@ -104,8 +104,8 @@ export const GET: APIRoute = async ({ site }) => {
|
|||
)}</p>` +
|
||||
`<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` +
|
||||
`<hr><p><em>${data.contentWarning.trim()}</em></p>` +
|
||||
`<hr>${tinyDecode(await marked(body))}` +
|
||||
`<hr>${tinyDecode(await marked(data.description))}`,
|
||||
`<hr>${tinyDecode(marked.parse(body) as string)}` +
|
||||
`<hr>${tinyDecode(marked.parse(data.description) as string)}`,
|
||||
),
|
||||
})),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { Image } from "astro:assets";
|
||||
import { getCollection } 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(),
|
||||
|
|
@ -16,7 +17,11 @@ const games = (await getCollection("games", (game) => !game.data.isDraft && game
|
|||
{
|
||||
games.map((game) => (
|
||||
<li>
|
||||
<a class="text-link hover:underline focus:underline" href={`/games/${game.slug}`}>
|
||||
<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())}
|
||||
>
|
||||
{game.data.thumbnail ? (
|
||||
<div class="flex aspect-[630/500] max-w-[288px] justify-center">
|
||||
<Image class="m-auto" src={game.data.thumbnail} alt={`Thumbnail for ${game.data.title}`} width={288} />
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import { Image } from "astro:assets";
|
||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
import { t } from "../i18n";
|
||||
|
||||
const MAX_ITEMS = 8;
|
||||
|
||||
|
|
@ -10,6 +11,7 @@ interface LatestItemsEntry {
|
|||
thumbnail: CollectionEntry<"stories">["data"]["thumbnail"];
|
||||
href: string;
|
||||
title: string;
|
||||
altText: string;
|
||||
pubDate: Date;
|
||||
}
|
||||
|
||||
|
|
@ -26,6 +28,7 @@ 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!,
|
||||
})),
|
||||
games.map<LatestItemsEntry>((game) => ({
|
||||
|
|
@ -33,6 +36,7 @@ const latestItems: LatestItemsEntry[] = [
|
|||
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!,
|
||||
})),
|
||||
]
|
||||
|
|
@ -63,7 +67,7 @@ const latestItems: LatestItemsEntry[] = [
|
|||
{
|
||||
latestItems.map((entry) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={entry.href}>
|
||||
<a class="text-link hover:underline focus:underline" href={entry.href} title={entry.altText}>
|
||||
{entry.thumbnail ? (
|
||||
<div class="flex aspect-square max-w-[192px] justify-center">
|
||||
<Image class="m-auto" src={entry.thumbnail} alt={`Thumbnail for ${entry.title}`} width={192} />
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Image } from "astro:assets";
|
|||
import { getCollection } from "astro:content";
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { t } from "../../i18n";
|
||||
|
||||
type Props = {
|
||||
page: Page<CollectionEntry<"stories">>;
|
||||
|
|
@ -67,7 +68,11 @@ const totalPages = Math.ceil(page.total / page.size);
|
|||
{
|
||||
page.data.map((story) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
||||
<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())}
|
||||
>
|
||||
{story.data.thumbnail ? (
|
||||
<div class="flex aspect-square max-w-[192px] justify-center">
|
||||
<Image
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { getCollection } from "astro:content";
|
||||
import { slug } from "github-slugger";
|
||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
|
||||
|
||||
const [stories, games, tagCategories] = await Promise.all([
|
||||
getCollection("stories"),
|
||||
|
|
@ -30,35 +31,43 @@ const seriesCollection = await getCollection("series");
|
|||
});
|
||||
|
||||
const uncategorizedTagsSet = new Set(tagsSet);
|
||||
const categorizedTags: Array<[string, string, string[]]> = tagCategories
|
||||
.sort((a, b) => a.data.index - b.data.index)
|
||||
.map((category) => {
|
||||
const tagList = category.data.tags.map((tag) => (typeof tag === "string" ? tag : tag["eng"]!));
|
||||
tagList.forEach((tag, index) => {
|
||||
if (index !== tagList.findLastIndex((val) => tag == val)) {
|
||||
throw new Error(`Duplicated tag "${tag}" found in multiple tag-categories`);
|
||||
}
|
||||
});
|
||||
return [
|
||||
category.data.name,
|
||||
category.id,
|
||||
tagList.filter((tag) => {
|
||||
if (draftOnlyTagsSet.has(tag)) {
|
||||
console.log(`Omitting draft-only tag "${tag}"`);
|
||||
return false;
|
||||
const categorizedTags: Array<[string, string, [string, string | undefined][]]> = await Promise.all(
|
||||
tagCategories
|
||||
.sort((a, b) => a.data.index - b.data.index)
|
||||
.map(async (category) => {
|
||||
const tagList = category.data.tags.map(({ name, description }) => {
|
||||
description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ").trim();
|
||||
return (typeof name === "string" ? [name, description] : [name["eng"]!, description]) as [
|
||||
string,
|
||||
string | undefined,
|
||||
];
|
||||
});
|
||||
tagList.forEach(([tag, _], index) => {
|
||||
if (index !== tagList.findLastIndex(([otherTag, _]) => tag == otherTag)) {
|
||||
throw new Error(`Duplicated tag "${tag}" found in multiple tag-categories lists`);
|
||||
}
|
||||
if (tagsSet.has(tag)) {
|
||||
uncategorizedTagsSet.delete(tag);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
];
|
||||
});
|
||||
});
|
||||
return [
|
||||
category.data.name,
|
||||
category.id,
|
||||
tagList.filter(([tag, _]) => {
|
||||
if (draftOnlyTagsSet.has(tag)) {
|
||||
console.log(`Omitting draft-only tag "${tag}"`);
|
||||
return false;
|
||||
}
|
||||
if (tagsSet.has(tag)) {
|
||||
uncategorizedTagsSet.delete(tag);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
if (uncategorizedTagsSet.size > 0) {
|
||||
const tagList = [...uncategorizedTagsSet];
|
||||
console.log("The following tags have no category:", tagList);
|
||||
categorizedTags.push(["Uncategorized tags", "others", tagList]);
|
||||
console.warn("The following tags have no category:", tagList);
|
||||
// categorizedTags.push(["Uncategorized tags", "others", tagList]);
|
||||
}
|
||||
---
|
||||
|
||||
|
|
@ -92,9 +101,9 @@ if (uncategorizedTagsSet.size > 0) {
|
|||
{category}
|
||||
</h2>
|
||||
<ul class="flex max-w-3xl flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
{tagList.map((tag) => (
|
||||
{tagList.map(([tag, description]) => (
|
||||
<li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white">
|
||||
<a class="hover:underline focus:underline" href={`/tags/${slug(tag)}`}>
|
||||
<a class="hover:underline focus:underline" href={`/tags/${slug(tag)}`} title={description}>
|
||||
{tag}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@
|
|||
import type { GetStaticPaths } from "astro";
|
||||
import { Image } from "astro:assets";
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import { Markdown } from "@astropub/md";
|
||||
import { slug } from "github-slugger";
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
import Prose from "../../components/Prose.astro";
|
||||
import { t } from "../../i18n";
|
||||
|
||||
type Props = {
|
||||
tag: string;
|
||||
description?: string;
|
||||
related?: string[];
|
||||
stories: CollectionEntry<"stories">[];
|
||||
games: CollectionEntry<"games">[];
|
||||
};
|
||||
|
|
@ -16,10 +21,11 @@ type Params = {
|
|||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const [stories, games, series] = await Promise.all([
|
||||
const [stories, games, series, tagCategories] = await Promise.all([
|
||||
getCollection("stories"),
|
||||
getCollection("games"),
|
||||
getCollection("series"),
|
||||
getCollection("tag-categories"),
|
||||
]);
|
||||
const seriesTags = new Set(series.map((s) => s.data.name));
|
||||
const tags = new Set<string>();
|
||||
|
|
@ -33,12 +39,40 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
tags.add(tag);
|
||||
});
|
||||
});
|
||||
const tagDescriptions = Object.fromEntries(
|
||||
tagCategories.flatMap((category) =>
|
||||
category.data.tags.reduce(
|
||||
(acc, { name, description, related }) => {
|
||||
if (related) {
|
||||
related = related.filter((relatedTag) => {
|
||||
if (relatedTag == name) {
|
||||
console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
|
||||
return false;
|
||||
}
|
||||
if (!tags.has(relatedTag)) {
|
||||
console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
acc.push(
|
||||
typeof name === "string" ? [name, { description, related }] : [name["eng"]!, { description, related }],
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
[] as [string, { description: string | undefined; related: string[] | undefined }][],
|
||||
),
|
||||
),
|
||||
);
|
||||
return [...tags]
|
||||
.filter((tag) => !seriesTags.has(tag))
|
||||
.map((tag) => ({
|
||||
params: { slug: slug(tag) } satisfies Params,
|
||||
props: {
|
||||
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()),
|
||||
|
|
@ -49,28 +83,43 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
}));
|
||||
};
|
||||
|
||||
const { tag, stories, games } = Astro.props;
|
||||
const { tag, description, stories, games, related } = Astro.props;
|
||||
if (!description) {
|
||||
console.warn(`Tag "${tag}" has no description!`);
|
||||
}
|
||||
const count = stories.length + games.length;
|
||||
let tagDescription: string = "";
|
||||
let totalWorksWithTag: string = "";
|
||||
if (count == 1) {
|
||||
if (stories.length == 1) {
|
||||
tagDescription = `One story tagged with "${tag}".`;
|
||||
totalWorksWithTag = `One story tagged with "${tag}".`;
|
||||
} else if (games.length == 1) {
|
||||
tagDescription = `One game tagged with "${tag}".`;
|
||||
totalWorksWithTag = `One game tagged with "${tag}".`;
|
||||
}
|
||||
} else if (stories.length == 0) {
|
||||
tagDescription = `${games.length} games tagged with "${tag}".`;
|
||||
totalWorksWithTag = `${games.length} games tagged with "${tag}".`;
|
||||
} else if (games.length == 0) {
|
||||
tagDescription = `${stories.length} stories tagged with "${tag}".`;
|
||||
totalWorksWithTag = `${stories.length} stories tagged with "${tag}".`;
|
||||
} else {
|
||||
tagDescription = `${stories.length == 1 ? "One story" : `${stories.length} stories`} and ${games.length == 1 ? "one game" : `${games.length} games`} tagged with "${tag}".`;
|
||||
totalWorksWithTag = `${stories.length == 1 ? "One story" : `${stories.length} stories`} and ${games.length == 1 ? "one game" : `${games.length} games`} tagged with "${tag}".`;
|
||||
}
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle={`Works tagged "${tag}"`}>
|
||||
<meta slot="head-description" content={`Bad Manners || ${tagDescription || tag}`} property="og:description" />
|
||||
<meta slot="head-description" content={`Bad Manners || ${totalWorksWithTag || tag}`} property="og:description" />
|
||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{tag}"</h1>
|
||||
{tagDescription ? <p class="my-4">{tagDescription}</p> : null}
|
||||
<div class="my-4">
|
||||
<Prose>
|
||||
{description ? <Markdown of={description} /> : null}
|
||||
{
|
||||
related?.length ? (
|
||||
<p
|
||||
set:html={`See also: ${related.map((relatedTag) => `<a href="/tags/${slug(relatedTag)}">${relatedTag}</a>`).join(", ")}.`}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
<p>{totalWorksWithTag}</p>
|
||||
</Prose>
|
||||
</div>
|
||||
{
|
||||
stories.length > 0 && (
|
||||
<section class="my-2" aria-labelledby="content-stories">
|
||||
|
|
@ -80,7 +129,11 @@ if (count == 1) {
|
|||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||
{stories.map((story) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
||||
<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())}
|
||||
>
|
||||
{story.data.thumbnail ? (
|
||||
<div class="flex aspect-square max-w-[192px] justify-center">
|
||||
<Image
|
||||
|
|
@ -108,7 +161,11 @@ if (count == 1) {
|
|||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||
{games.map((game) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/games/${game.slug}`}>
|
||||
<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())}
|
||||
>
|
||||
{game.data.thumbnail ? (
|
||||
<div class="flex aspect-[630/500] max-w-[192px] justify-center">
|
||||
<Image
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue