Several minor improvements to typing and misc.
- Improved schema validation - Move username parsing and other validators to schema types - Fix astro check command - Add JSON/YAML schema validation for data collections - Update licenses - Remove deployment script in favor of rsync - Prevent unsanitized input in export-story script - Change "eng" language to "en", per BCP47 - Clean up i18n keys and add aria attributes - Improve MastodonComments behavior on no-JS browsers
This commit is contained in:
parent
fe908a4989
commit
7bb8a952ef
54 changed files with 1005 additions and 962 deletions
|
|
@ -6,12 +6,13 @@ import AgeRestrictedModal from "../components/AgeRestrictedModal.astro";
|
|||
|
||||
type Props = {
|
||||
pageTitle?: string;
|
||||
lang?: string;
|
||||
};
|
||||
|
||||
const { pageTitle } = Astro.props;
|
||||
const { pageTitle = "Gallery", lang = "en" } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<html lang={lang}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
|
|
@ -23,7 +24,7 @@ const { pageTitle } = Astro.props;
|
|||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{pageTitle || "Gallery"} | Bad Manners</title>
|
||||
<title>{pageTitle} | Bad Manners</title>
|
||||
<link rel="me" href="https://meow.social/@BadManners" />
|
||||
<link
|
||||
rel="alternate"
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ const { props } = Astro;
|
|||
const series = props.series && (await getEntry(props.series));
|
||||
const authorsList = await getEntries(props.authors);
|
||||
const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
|
||||
// const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
||||
// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
||||
const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||
const categorizedTags = Object.fromEntries(
|
||||
(await getCollection("tag-categories")).flatMap((category) =>
|
||||
category.data.tags.map<[string, string | null]>(({ name }) =>
|
||||
|
|
@ -44,10 +44,10 @@ const thumbnail =
|
|||
(await getImage({ src: props.thumbnail, width: props.thumbnailWidth, height: props.thumbnailHeight }));
|
||||
---
|
||||
|
||||
<BaseLayout pageTitle={props.title}>
|
||||
<BaseLayout pageTitle={props.title} lang={props.lang}>
|
||||
<Fragment slot="head">
|
||||
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
|
||||
<meta property="og:description" content={props.contentWarning} />
|
||||
<meta property="og:description" content={t(props.lang, "game/warnings", props.platforms, props.contentWarning)} />
|
||||
<meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" />
|
||||
{
|
||||
thumbnail ? (
|
||||
|
|
@ -55,7 +55,7 @@ const thumbnail =
|
|||
<meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content={`Cover art for ${props.title}`}
|
||||
content={t(props.lang, "published_content/cover_art_alt", props.title)}
|
||||
data-pagefind-meta="image_alt[content]"
|
||||
/>
|
||||
</Fragment>
|
||||
|
|
@ -78,7 +78,9 @@ const thumbnail =
|
|||
<a
|
||||
href={series ? series.data.url : "/games"}
|
||||
class="text-link my-1 h-9 w-9 p-2"
|
||||
aria-label={`Return to ${series ? series.data.name : "games"}`}
|
||||
aria-label={series
|
||||
? t(props.lang, "published_content/return_to_series", series.data.name)
|
||||
: t(props.lang, "game/return_to_games")}
|
||||
>
|
||||
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
||||
<path
|
||||
|
|
@ -89,7 +91,7 @@ const thumbnail =
|
|||
<a
|
||||
href="#description"
|
||||
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-label="Go to description"
|
||||
aria-label={t(props.lang, "published_content/go_to_description")}
|
||||
>
|
||||
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
||||
<path
|
||||
|
|
@ -100,7 +102,7 @@ const thumbnail =
|
|||
<button
|
||||
data-dark-mode
|
||||
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-label="Toggle dark mode"
|
||||
aria-label={t(props.lang, "published_content/toggle_dark_mode")}
|
||||
>
|
||||
<svg viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden="true">
|
||||
<path
|
||||
|
|
@ -120,7 +122,11 @@ const thumbnail =
|
|||
data-pagefind-body={props.isDraft ? undefined : ""}
|
||||
data-pagefind-meta="type:game"
|
||||
>
|
||||
<h1 id="game-title" class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
<h1
|
||||
id="game-title"
|
||||
class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100"
|
||||
aria-label={t(props.lang, "game/title_aria_label")}
|
||||
>
|
||||
{props.title}
|
||||
</h1>
|
||||
<section
|
||||
|
|
@ -136,7 +142,7 @@ const thumbnail =
|
|||
{
|
||||
props.isDraft ? (
|
||||
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
||||
{t(props.lang, "story/draft_warning")}
|
||||
{t(props.lang, "published_content/draft_warning")}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
|
|
@ -155,7 +161,7 @@ const thumbnail =
|
|||
<img
|
||||
loading="eager"
|
||||
src={thumbnail.src}
|
||||
alt={`Cover art for ${props.title}`}
|
||||
alt={t(props.lang, "published_content/cover_art_alt", props.title)}
|
||||
width={props.thumbnailWidth}
|
||||
height={props.thumbnailHeight}
|
||||
class="mx-auto my-5 shadow-lg"
|
||||
|
|
@ -165,7 +171,7 @@ const thumbnail =
|
|||
) : null
|
||||
}
|
||||
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||
<article id="game" class="pr-1 font-serif">
|
||||
<article id="game" class="pr-1 font-serif" aria-label={t(props.lang, "game/article_aria_label")}>
|
||||
<Prose>
|
||||
<slot />
|
||||
</Prose>
|
||||
|
|
@ -177,28 +183,26 @@ const thumbnail =
|
|||
id="draft-warning-bottom"
|
||||
class="py-2 text-center font-serif text-2xl font-semibold not-italic text-red-600"
|
||||
>
|
||||
{t(props.lang, "story/draft_warning")}
|
||||
{t(props.lang, "published_content/draft_warning")}
|
||||
</p>
|
||||
) : props.pubDate ? (
|
||||
<p
|
||||
id="publish-date"
|
||||
class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
|
||||
aria-label="Publish date"
|
||||
aria-description={props.pubDate.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
aria-label={t(props.lang, "published_content/publish_date_aria_label")}
|
||||
aria-description={
|
||||
t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
|
||||
}
|
||||
data-pagefind-index-attrs="aria-description"
|
||||
data-pagefind-meta={`date:${props.pubDate.toISOString().slice(0, 10)}`}
|
||||
>
|
||||
{t(props.lang, "story/publish_date", props.pubDate.toISOString().slice(0, 10))}
|
||||
{t(props.lang, "published_content/publish_date", props.pubDate)}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
||||
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "story/description")}
|
||||
{t(props.lang, "published_content/description")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<Markdown of={props.description} />
|
||||
|
|
@ -211,14 +215,50 @@ const thumbnail =
|
|||
><path
|
||||
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
|
||||
></path></svg
|
||||
><span>{t(props.lang, "story/to_top")}</span></a
|
||||
><span>{t(props.lang, "published_content/to_top")}</span></a
|
||||
>
|
||||
</div>
|
||||
{
|
||||
relatedStories.length > 0 ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/related_stories")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{relatedStories.map((story) => (
|
||||
<li>
|
||||
<a href={`/stories/${story.slug}`}>{story.data.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Prose>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
relatedGames.length > 0 ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/related_games")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{relatedGames.map((game) => (
|
||||
<li>
|
||||
<a href={`/games/${game.slug}`}>{game.data.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Prose>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
tags.length > 0 ? (
|
||||
<section id="tags" aria-describedby="title-tags" class="my-5">
|
||||
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
Tags
|
||||
{t(props.lang, "published_content/tags")}
|
||||
</h2>
|
||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
{tags.map(({ id, name }) => (
|
||||
|
|
@ -232,16 +272,12 @@ const thumbnail =
|
|||
</section>
|
||||
) : null
|
||||
}
|
||||
<MastodonComments
|
||||
instance={props.posts.mastodon?.instance}
|
||||
user={props.posts.mastodon?.user}
|
||||
postId={props.posts.mastodon?.postId}
|
||||
/>
|
||||
{props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
|
||||
</main>
|
||||
<div class="pt-6 text-center text-xs text-black dark:text-white">
|
||||
<span>{t(props.lang, "story/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
|
||||
<span>{t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
|
||||
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
|
||||
>{t(props.lang, "story/licenses")}</a
|
||||
>{t(props.lang, "published_content/licenses")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,21 +17,15 @@ import { formatCopyrightedCharacters } from "../utils/format_copyrighted_charact
|
|||
type Props = CollectionEntry<"stories">["data"];
|
||||
|
||||
const { props } = Astro;
|
||||
let prev = props.prev && (await getEntry(props.prev));
|
||||
if (prev && prev.data.isDraft) {
|
||||
prev = undefined;
|
||||
}
|
||||
let next = props.next && (await getEntry(props.next));
|
||||
if (next && next.data.isDraft) {
|
||||
next = undefined;
|
||||
}
|
||||
const prev = props.prev && (await getEntry(props.prev));
|
||||
const next = props.next && (await getEntry(props.next));
|
||||
const series = props.series && (await getEntry(props.series));
|
||||
const authorsList = await getEntries(props.authors);
|
||||
const commissionersList = props.commissioner && (await getEntries(props.commissioner));
|
||||
const requestersList = props.requester && (await getEntries(props.requester));
|
||||
const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
|
||||
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
||||
// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||
const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||
const categorizedTags = Object.fromEntries(
|
||||
(await getCollection("tag-categories")).flatMap((category) =>
|
||||
category.data.tags.map<[string, string | null]>(({ name }) =>
|
||||
|
|
@ -57,7 +51,7 @@ const thumbnail =
|
|||
const wordCount = props.wordCount?.toString();
|
||||
---
|
||||
|
||||
<BaseLayout pageTitle={props.title}>
|
||||
<BaseLayout pageTitle={props.title} lang={props.lang}>
|
||||
<Fragment slot="head">
|
||||
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
|
||||
<meta property="og:description" content={t(props.lang, "story/warnings", wordCount, props.contentWarning)} />
|
||||
|
|
@ -68,7 +62,7 @@ const wordCount = props.wordCount?.toString();
|
|||
<meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content={`Cover art for ${props.shortTitle || props.title}`}
|
||||
content={t(props.lang, "published_content/cover_art_alt", props.shortTitle || props.title)}
|
||||
data-pagefind-meta="image_alt[content]"
|
||||
/>
|
||||
</Fragment>
|
||||
|
|
@ -92,7 +86,7 @@ const wordCount = props.wordCount?.toString();
|
|||
href={series ? series.data.url : "/stories/1"}
|
||||
class="text-link my-1 h-9 w-9 p-2"
|
||||
aria-label={series
|
||||
? t(props.lang, "story/return_to_series", series.data.name)
|
||||
? t(props.lang, "published_content/return_to_series", series.data.name)
|
||||
: t(props.lang, "story/return_to_stories")}
|
||||
>
|
||||
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
||||
|
|
@ -104,7 +98,7 @@ const wordCount = props.wordCount?.toString();
|
|||
<a
|
||||
href="#description"
|
||||
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-label={t(props.lang, "story/go_to_description")}
|
||||
aria-label={t(props.lang, "published_content/go_to_description")}
|
||||
>
|
||||
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
||||
<path
|
||||
|
|
@ -115,7 +109,7 @@ const wordCount = props.wordCount?.toString();
|
|||
<button
|
||||
data-dark-mode
|
||||
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-label={t(props.lang, "story/toggle_dark_mode")}
|
||||
aria-label={t(props.lang, "published_content/toggle_dark_mode")}
|
||||
>
|
||||
<svg viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden="true">
|
||||
<path
|
||||
|
|
@ -136,7 +130,7 @@ const wordCount = props.wordCount?.toString();
|
|||
data-pagefind-meta="type:story"
|
||||
>
|
||||
{
|
||||
prev || next ? (
|
||||
(prev && !prev.data.isDraft) || (next && !next.data.isDraft) ? (
|
||||
<div class="print:hidden">
|
||||
<div id="story-nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2">
|
||||
{prev ? (
|
||||
|
|
@ -170,7 +164,11 @@ const wordCount = props.wordCount?.toString();
|
|||
</div>
|
||||
) : null
|
||||
}
|
||||
<h1 id="story-title" class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
<h1
|
||||
id="story-title"
|
||||
class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100"
|
||||
aria-label={t(props.lang, "story/title_aria_label")}
|
||||
>
|
||||
{props.title}
|
||||
</h1>
|
||||
<section
|
||||
|
|
@ -180,13 +178,6 @@ const wordCount = props.wordCount?.toString();
|
|||
<Authors lang={props.lang}>
|
||||
{authorsList.map((author) => <UserComponent user={author} lang={props.lang} />)}
|
||||
</Authors>
|
||||
{
|
||||
props.isDraft ? (
|
||||
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
||||
{t(props.lang, "story/draft_warning")}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
requestersList && (
|
||||
<Requesters lang={props.lang}>
|
||||
|
|
@ -205,6 +196,13 @@ const wordCount = props.wordCount?.toString();
|
|||
</Commissioners>
|
||||
)
|
||||
}
|
||||
{
|
||||
props.isDraft ? (
|
||||
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
||||
{t(props.lang, "published_content/draft_warning")}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
<div id="content-warning">
|
||||
<p>
|
||||
{t(props.lang, "story/warnings", wordCount, props.contentWarning)}
|
||||
|
|
@ -218,7 +216,7 @@ const wordCount = props.wordCount?.toString();
|
|||
<img
|
||||
loading="eager"
|
||||
src={thumbnail.src}
|
||||
alt={`Cover art for ${props.shortTitle || props.title}`}
|
||||
alt={t(props.lang, "published_content/cover_art_alt", props.shortTitle || props.title)}
|
||||
width={props.thumbnailWidth}
|
||||
height={props.thumbnailHeight}
|
||||
class="mx-auto my-5 shadow-lg"
|
||||
|
|
@ -227,7 +225,7 @@ const wordCount = props.wordCount?.toString();
|
|||
) : null
|
||||
}
|
||||
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||
<article id="story" class="pr-1 font-serif">
|
||||
<article id="story" class="pr-1 font-serif" aria-label={t(props.lang, "story/article_aria_label")}>
|
||||
<Prose>
|
||||
<slot />
|
||||
</Prose>
|
||||
|
|
@ -239,28 +237,26 @@ const wordCount = props.wordCount?.toString();
|
|||
id="draft-warning-bottom"
|
||||
class="py-2 text-center font-serif text-2xl font-semibold not-italic text-red-600"
|
||||
>
|
||||
{t(props.lang, "story/draft_warning")}
|
||||
{t(props.lang, "published_content/draft_warning")}
|
||||
</p>
|
||||
) : props.pubDate ? (
|
||||
<p
|
||||
id="publish-date"
|
||||
class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
|
||||
aria-label="Publish date"
|
||||
aria-description={props.pubDate.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
aria-label={t(props.lang, "published_content/publish_date_aria_label")}
|
||||
aria-description={
|
||||
t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
|
||||
}
|
||||
data-pagefind-index-attrs="aria-description"
|
||||
data-pagefind-meta={`date:${props.pubDate.toISOString().slice(0, 10)}`}
|
||||
>
|
||||
{t(props.lang, "story/publish_date", props.pubDate.toISOString().slice(0, 10))}
|
||||
{t(props.lang, "published_content/publish_date", props.pubDate)}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
||||
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "story/description")}
|
||||
{t(props.lang, "published_content/description")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<Markdown of={props.description} />
|
||||
|
|
@ -292,7 +288,7 @@ const wordCount = props.wordCount?.toString();
|
|||
><path
|
||||
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
|
||||
></path></svg
|
||||
><span>{t(props.lang, "story/to_top")}</span></a
|
||||
><span>{t(props.lang, "published_content/to_top")}</span></a
|
||||
>
|
||||
</div>
|
||||
{
|
||||
|
|
@ -335,13 +331,31 @@ const wordCount = props.wordCount?.toString();
|
|||
relatedStories.length > 0 ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
Related stories
|
||||
{t(props.lang, "published_content/related_stories")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{relatedStories.map((stories) => (
|
||||
{relatedStories.map((story) => (
|
||||
<li>
|
||||
<a href={`/stories/${stories.slug}`}>{stories.data.title}</a>
|
||||
<a href={`/stories/${story.slug}`}>{story.data.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Prose>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
relatedGames.length > 0 ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/related_games")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{relatedGames.map((game) => (
|
||||
<li>
|
||||
<a href={`/games/${game.slug}`}>{game.data.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
@ -353,7 +367,7 @@ const wordCount = props.wordCount?.toString();
|
|||
tags.length > 0 ? (
|
||||
<section id="tags" aria-describedby="title-tags" class="my-5">
|
||||
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "story/tags")}
|
||||
{t(props.lang, "published_content/tags")}
|
||||
</h2>
|
||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
{tags.map(({ id, name }) => (
|
||||
|
|
@ -367,16 +381,12 @@ const wordCount = props.wordCount?.toString();
|
|||
</section>
|
||||
) : null
|
||||
}
|
||||
<MastodonComments
|
||||
instance={props.posts.mastodon?.instance}
|
||||
user={props.posts.mastodon?.user}
|
||||
postId={props.posts.mastodon?.postId}
|
||||
/>
|
||||
{props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
|
||||
</main>
|
||||
<div class="pt-6 text-center text-xs text-black dark:text-white">
|
||||
<span>{t(props.lang, "story/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
|
||||
<span>{t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
|
||||
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
|
||||
>{t(props.lang, "story/licenses")}</a
|
||||
>{t(props.lang, "published_content/licenses")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue