Finish adding blog posts

This commit is contained in:
Bad Manners 2024-09-14 09:08:58 -03:00
parent 4a3ee88f77
commit cf180442c3
Signed by: badmanners
GPG key ID: 8C88292CCB075609
21 changed files with 178 additions and 40 deletions

Binary file not shown.

Before

(image error) Size: 9.3 KiB

After

(image error) Size: 25 KiB

Before After
Before After

Binary file not shown.

After

(image error) Size: 124 KiB

Binary file not shown.

After

(image error) Size: 45 KiB

View file

@ -0,0 +1,15 @@
---
import SVGIcon from "./SVGIcon.astro";
type Props = {
width: string;
height: string;
class?: string;
};
---
<SVGIcon {...Astro.props} viewBox="0 0 512 512">
<path
d="M192 32c0 17.7 14.3 32 32 32c123.7 0 224 100.3 224 224c0 17.7 14.3 32 32 32s32-14.3 32-32C512 128.9 383.1 0 224 0c-17.7 0-32 14.3-32 32zm0 96c0 17.7 14.3 32 32 32c70.7 0 128 57.3 128 128c0 17.7 14.3 32 32 32s32-14.3 32-32c0-106-86-192-192-192c-17.7 0-32 14.3-32 32zM96 144c0-26.5-21.5-48-48-48S0 117.5 0 144L0 368c0 79.5 64.5 144 144 144s144-64.5 144-144s-64.5-144-144-144l-16 0 0 96 16 0c26.5 0 48 21.5 48 48s-21.5 48-48 48s-48-21.5-48-48l0-224z"
></path>
</SVGIcon>

View file

@ -1,5 +1,6 @@
export { default as IconArrowBack } from "./IconArrowBack.astro"; export { default as IconArrowBack } from "./IconArrowBack.astro";
export { default as IconArrowUp } from "./IconArrowUp.astro"; export { default as IconArrowUp } from "./IconArrowUp.astro";
export { default as IconBlog } from "./IconBlog.astro";
export { default as IconBook } from "./IconBook.astro"; export { default as IconBook } from "./IconBook.astro";
export { default as IconBriefcase } from "./IconBriefcase.astro"; export { default as IconBriefcase } from "./IconBriefcase.astro";
export { default as IconChevronLeft } from "./IconChevronLeft.astro"; export { default as IconChevronLeft } from "./IconChevronLeft.astro";

View file

@ -1,13 +1,23 @@
--- ---
title: Jamming Over title: "Jamming Over: A Postmortem"
pubDate: 2024-03-26 pubDate: 2024-03-26
isDraft: true
isAgeRestricted: true isAgeRestricted: true
authors: bad-manners authors: bad-manners
# thumbnail: /src/assets/thumbnails/story_thumbnail.png thumbnail: /src/assets/thumbnails/other/crossing_over_retrospective.png
description: | description: |
Postmortem about my first vore game, [Crossing Over](/games/crossing-over) albeit more of an assortment of random thoughts. **Spoilers for Crossing Over ahead!** A retrospective about my first vore game, [Crossing Over](/games/crossing-over) albeit more of an assortment of random thoughts than an actual postmortem. **Spoilers for Crossing Over ahead!**
tags: [] tags:
- behind the scenes
- retrospective
- oral vore
- anthro predator
- willing predator
- willing prey
- male predator
- non-binary prey
- micro prey
- soul vore
- long-term endo
relatedGames: relatedGames:
- crossing-over - crossing-over
--- ---
@ -280,3 +290,7 @@ Nonetheless, I can't deny that I feel this way. I've been trying to write more s
Well, I managed to finish one other thing, at least: this postmortem! And despite this current slump, I still want to make stuff more stories and, maybe, even more games. I know that these negative feelings will fade from memory, and that I'll remember this project fondly for months and years to come. I want to make more art not just for myself, but for others. I want to improve my skills, and I want to bring people joy. Well, I managed to finish one other thing, at least: this postmortem! And despite this current slump, I still want to make stuff more stories and, maybe, even more games. I know that these negative feelings will fade from memory, and that I'll remember this project fondly for months and years to come. I want to make more art not just for myself, but for others. I want to improve my skills, and I want to bring people joy.
At the end of the day, I would be happy to become even a fraction of who Marco was for Bard in their darkest hour. At the end of the day, I would be happy to become even a fraction of who Marco was for Bard in their darkest hour.
---
Oh, hey, I didn't expect you to read all of this! I hope it was enjoyable. By the way, since I first wrote this retrospective-slash-postmortem, I've [opened the source code for this game](https://gitgud.io/BadMannersXYZ/CrossingOver)! If you are interested in an even more hands-on look at how it was made, or want to make your own visual novel in Godot, feel free to peruse it to your heart's content.

View file

@ -1,15 +1,26 @@
--- ---
title: "Story Breakdown: Taken In" title: "Taken In: Story Breakdown!"
pubDate: 2024-01-23 pubDate: 2024-01-23
isDraft: true
isAgeRestricted: true isAgeRestricted: true
authors: bad-manners authors: bad-manners
# thumbnail: /src/assets/thumbnails/story_thumbnail.png thumbnail: /src/assets/thumbnails/other/taken_in_breakdown.png
description: | description: |
First time annotating a vore story; in this case, [Taken In](/stories/taken-in). Here, I go over my writing process while offering additional tidbits of information. First time annotating a vore story; in this case, [Taken In](/stories/taken-in). Here, I go over my writing process while offering additional tidbits of information.
tags: [] tags:
relatedGames: - behind the scenes
- crossing-over - Sam Brendan
- feral predator
- anthro prey
- male predator
- ambiguous gender prey
- willing predator
- unwilling prey
- oral vore
- same size
- full tour
- point of view
relatedStories:
- taken-in
--- ---
All in all, going over the story and breaking it down was a fun process. This was originally a text document, which I had to completely refit into this blog post that you're reading (oof...!). But for the sake of posteriority, I think it was worth the effort. All in all, going over the story and breaking it down was a fun process. This was originally a text document, which I had to completely refit into this blog post that you're reading (oof...!). But for the sake of posteriority, I think it was worth the effort.

View file

@ -41,7 +41,7 @@ tags:
- soul vore - soul vore
- long-term endo - long-term endo
relatedBlogPosts: relatedBlogPosts:
- crossing-over-postmortem - crossing-over-retrospective
--- ---
<iframe <iframe

View file

@ -29,6 +29,8 @@ tags:
- point of view - point of view
copyrightedCharacters: copyrightedCharacters:
"Sam Brendan": bad-manners "Sam Brendan": bad-manners
relatedBlogPosts:
- taken-in-breakdown
--- ---
Clank! Shuffle! Crunch! The sounds outside are too loud to be stopped by the walls in your room, and you jolt awake. Clank! Shuffle! Crunch! The sounds outside are too loud to be stopped by the walls in your room, and you jolt awake.

View file

@ -9,3 +9,7 @@ tags:
description: Short-format stories with no more than 2,500 words. description: Short-format stories with no more than 2,500 words.
- name: toki pona - name: toki pona
description: Stories written in toki pona, the language of good. description: Stories written in toki pona, the language of good.
- name: behind the scenes
description: Content where I go over the process of making other content.
- name: retrospective
description: Documents detailing the good and bad parts of the process of creation of a certain project.

View file

@ -298,7 +298,7 @@ const UI_STRINGS = {
}, },
// Tag-related strings // Tag-related strings
"tag/total_works_with_tag": { "tag/total_works_with_tag": {
en: (tag: string, storiesCount: number, gamesCount: number) => { en: (tag: string, storiesCount: number, gamesCount: number, blogPostsCount: number) => {
const content = []; const content = [];
if (storiesCount > 0) { if (storiesCount > 0) {
content.push(UI_STRINGS["util/enumerate"].en(storiesCount, "story", "stories")); content.push(UI_STRINGS["util/enumerate"].en(storiesCount, "story", "stories"));
@ -306,6 +306,9 @@ const UI_STRINGS = {
if (gamesCount > 0) { if (gamesCount > 0) {
content.push(UI_STRINGS["util/enumerate"].en(gamesCount, "game", "games")); content.push(UI_STRINGS["util/enumerate"].en(gamesCount, "game", "games"));
} }
if (blogPostsCount > 0) {
content.push(UI_STRINGS["util/enumerate"].en(gamesCount, "blog post", "blog posts"));
}
if (content.length === 0) { if (content.length === 0) {
return `No works tagged with "${tag}".`; return `No works tagged with "${tag}".`;
} }

View file

@ -16,6 +16,7 @@ const series = props.series && (await getEntry(props.series));
const authorsList = await getEntries(props.authors); const authorsList = await getEntries(props.authors);
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.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 relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
const relatedBlogPosts = (await getEntries(props.relatedBlogPosts)).filter((post) => !post.data.isDraft);
--- ---
<PublishedContentLayout <PublishedContentLayout
@ -44,6 +45,7 @@ const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !ga
: undefined} : undefined}
relatedStories={relatedStories} relatedStories={relatedStories}
relatedGames={relatedGames} relatedGames={relatedGames}
relatedBlogPosts={relatedBlogPosts}
posts={props.posts} posts={props.posts}
labelReturnTo={{ title: t(props.lang, "blog/return_to_blog_posts"), link: "/blog" }} labelReturnTo={{ title: t(props.lang, "blog/return_to_blog_posts"), link: "/blog" }}
labelPreviousContent={t(props.lang, "blog/previous_post_aria_label")} labelPreviousContent={t(props.lang, "blog/previous_post_aria_label")}

View file

@ -4,15 +4,16 @@ import BaseLayout from "./BaseLayout.astro";
import logoBM from "../assets/images/logo_bm.png"; import logoBM from "../assets/images/logo_bm.png";
import { t } from "../i18n"; import { t } from "../i18n";
import { import {
IconHome, IconBlog,
IconBook,
IconBriefcase, IconBriefcase,
IconGamepad,
IconHome,
IconMagnifyingGlass,
IconMoon,
IconSquareRSS, IconSquareRSS,
IconSun, IconSun,
IconMoon,
IconMagnifyingGlass,
IconTags, IconTags,
IconGamepad,
IconBook,
} from "../components/icons"; } from "../components/icons";
type Props = { type Props = {
@ -80,6 +81,12 @@ const isCurrentRoute = (path: string) =>
<span class="order-3 group-hover:underline group-focus:underline">Games</span> <span class="order-3 group-hover:underline group-focus:underline">Games</span>
</a> </a>
</li> </li>
<li>
<a class="u-url text-link group" href="/blog" aria-current={isCurrentRoute("/blog") ? "page" : undefined}>
<IconBlog width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" />
<span class="order-3 group-hover:underline group-focus:underline">Blog posts</span>
</a>
</li>
<li> <li>
<a class="u-url text-link group" href="/tags" aria-current={isCurrentRoute("/tags") ? "page" : undefined}> <a class="u-url text-link group" href="/tags" aria-current={isCurrentRoute("/tags") ? "page" : undefined}>
<IconTags width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" /> <IconTags width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" />

View file

@ -15,6 +15,7 @@ const series = props.series && (await getEntry(props.series));
const authorsList = await getEntries(props.authors); const authorsList = await getEntries(props.authors);
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.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 relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
const relatedBlogPosts = (await getEntries(props.relatedBlogPosts)).filter((post) => !post.data.isDraft);
--- ---
<PublishedContentLayout <PublishedContentLayout
@ -40,6 +41,7 @@ const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !ga
: undefined} : undefined}
relatedStories={relatedStories} relatedStories={relatedStories}
relatedGames={relatedGames} relatedGames={relatedGames}
relatedBlogPosts={relatedBlogPosts}
posts={props.posts} posts={props.posts}
labelReturnTo={{ title: t(props.lang, "game/return_to_games"), link: "/games" }} labelReturnTo={{ title: t(props.lang, "game/return_to_games"), link: "/games" }}
labelPreviousContent={t(props.lang, "game/previous_game_aria_label")} labelPreviousContent={t(props.lang, "game/previous_game_aria_label")}

View file

@ -19,6 +19,7 @@ const commissionersList = props.commissioners && (await getEntries(props.commiss
const requestersList = props.requesters && (await getEntries(props.requesters)); const requestersList = props.requesters && (await getEntries(props.requesters));
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.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 relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
const relatedBlogPosts = (await getEntries(props.relatedBlogPosts)).filter((post) => !post.data.isDraft);
const wordCount = props.wordCount?.toString(); const wordCount = props.wordCount?.toString();
--- ---
@ -51,6 +52,7 @@ const wordCount = props.wordCount?.toString();
: undefined} : undefined}
relatedStories={relatedStories} relatedStories={relatedStories}
relatedGames={relatedGames} relatedGames={relatedGames}
relatedBlogPosts={relatedBlogPosts}
posts={props.posts} posts={props.posts}
labelReturnTo={{ title: t(props.lang, "story/return_to_stories"), link: "/stories/1" }} labelReturnTo={{ title: t(props.lang, "story/return_to_stories"), link: "/stories/1" }}
labelPreviousContent={t(props.lang, "story/previous_story_aria_label")} labelPreviousContent={t(props.lang, "story/previous_story_aria_label")}

View file

@ -2,7 +2,6 @@
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import { getCollection, getEntries, type CollectionEntry } from "astro:content"; import { getCollection, getEntries, type CollectionEntry } from "astro:content";
import GalleryLayout from "../layouts/GalleryLayout.astro"; import GalleryLayout from "../layouts/GalleryLayout.astro";
import { t } from "../i18n";
import UserComponent from "../components/UserComponent.astro"; import UserComponent from "../components/UserComponent.astro";
import { markdownToPlaintext } from "../utils/markdown_to_plaintext"; import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
@ -34,13 +33,13 @@ const posts = await Promise.all(
data-tooltip data-tooltip
> >
{post.data.thumbnail ? ( {post.data.thumbnail ? (
<div class="flex aspect-[630/500] max-w-[288px] justify-center"> <div class="flex aspect-square max-w-[288px] justify-center">
<Image <Image
loading={i < 10 ? "eager" : "lazy"} loading={i < 10 ? "eager" : "lazy"}
class="u-photo m-auto" class="u-photo m-auto"
src={post.data.thumbnail} src={post.data.thumbnail}
alt={`Thumbnail for ${post.data.title}`} alt={`Thumbnail for ${post.data.title}`}
width={288} width={192}
/> />
</div> </div>
) : null} ) : null}

View file

@ -2,7 +2,7 @@
import type { GetStaticPaths } from "astro"; import type { GetStaticPaths } from "astro";
import { type CollectionEntry, getCollection } from "astro:content"; import { type CollectionEntry, getCollection } from "astro:content";
import { PUBLISH_DRAFTS } from "astro:env/server"; import { PUBLISH_DRAFTS } from "astro:env/server";
import BlogLayout from "../../layouts/BlogLayout.astro"; import BlogPostLayout from "../../layouts/BlogPostLayout.astro";
type Props = CollectionEntry<"blog">; type Props = CollectionEntry<"blog">;
@ -24,6 +24,6 @@ const post = Astro.props;
const { Content } = await post.render(); const { Content } = await post.render();
--- ---
<BlogLayout {...post.data}> <BlogPostLayout {...post.data}>
<Content /> <Content />
</BlogLayout> </BlogPostLayout>

View file

@ -33,7 +33,7 @@ const games = await Promise.all(
data-tooltip data-tooltip
> >
{game.data.thumbnail ? ( {game.data.thumbnail ? (
<div class="flex aspect-[630/500] max-w-[288px] justify-center"> <div class="flex aspect-square max-w-[288px] justify-center">
<Image <Image
loading={i < 10 ? "eager" : "lazy"} loading={i < 10 ? "eager" : "lazy"}
class="u-photo m-auto" class="u-photo m-auto"

View file

@ -75,7 +75,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
date: post.data.pubDate, date: post.data.pubDate,
fn: async () => fn: async () =>
({ ({
type: "Game", type: "Blog post",
thumbnail: post.data.thumbnail, thumbnail: post.data.thumbnail,
href: `/blog/${post.slug}`, href: `/blog/${post.slug}`,
title: post.data.title, title: post.data.title,

View file

@ -26,6 +26,7 @@ const { badTag } = Astro.props;
<meta content="No." property="og:description" /> <meta content="No." property="og:description" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
</Fragment> </Fragment>
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{badTag}"</h1> <h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{badTag}"</h1>
<hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
<p class="my-4">No.</p> <p class="my-4">No.</p>
</GalleryLayout> </GalleryLayout>

View file

@ -9,6 +9,7 @@ import Prose from "../../components/Prose.astro";
import { t, DEFAULT_LANG } from "../../i18n"; import { t, DEFAULT_LANG } from "../../i18n";
import { qualifyLocalURLsInMarkdown } from "../../utils/qualify_local_urls_in_markdown"; import { qualifyLocalURLsInMarkdown } from "../../utils/qualify_local_urls_in_markdown";
import UserComponent from "../../components/UserComponent.astro"; import UserComponent from "../../components/UserComponent.astro";
import { markdownToPlaintext } from "../../utils/markdown_to_plaintext";
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } }; type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
@ -18,6 +19,7 @@ type Props = {
related?: string[]; related?: string[];
stories: (EntryWithPubDate<"stories"> & { authors: CollectionEntry<"users">[] })[]; stories: (EntryWithPubDate<"stories"> & { authors: CollectionEntry<"users">[] })[];
games: (EntryWithPubDate<"games"> & { authors: CollectionEntry<"users">[] })[]; games: (EntryWithPubDate<"games"> & { authors: CollectionEntry<"users">[] })[];
blogPosts: (EntryWithPubDate<"blog"> & { authors: CollectionEntry<"users">[] })[];
}; };
type Params = { type Params = {
@ -25,24 +27,22 @@ type Params = {
}; };
export const getStaticPaths: GetStaticPaths = async () => { export const getStaticPaths: GetStaticPaths = async () => {
const [stories, games, series, tagCategories] = await Promise.all([ const [stories, games, blogPosts, series, tagCategories] = await Promise.all([
getCollection("stories"), getCollection("stories"),
getCollection("games"), getCollection("games"),
getCollection("blog"),
getCollection("series"), getCollection("series"),
getCollection("tag-categories"), getCollection("tag-categories"),
]); ]);
const seriesTags = new Set(series.map((s) => s.data.name)); const seriesTags = new Set(series.map((s) => s.data.name));
const tags = new Set<string>(); const tags = new Set<string>();
stories.forEach((story) => { [stories, games, blogPosts].forEach((collection) =>
story.data.tags.forEach((tag) => { collection.forEach((content) => {
tags.add(tag); content.data.tags.forEach((tag) => {
}); tags.add(tag);
}); });
games.forEach((game) => { }),
game.data.tags.forEach((tag) => { );
tags.add(tag);
});
});
const tagDescriptions = tagCategories.reduce( const tagDescriptions = tagCategories.reduce(
(acc, category) => { (acc, category) => {
category.data.tags.forEach(({ name, description, related }) => { category.data.tags.forEach(({ name, description, related }) => {
@ -100,6 +100,18 @@ export const getStaticPaths: GetStaticPaths = async () => {
authors: await getEntries(game.data.authors), authors: await getEntries(game.data.authors),
})), })),
), ),
blogPosts: await Promise.all(
(
blogPosts.filter(
(post) => !post.data.isDraft && post.data.pubDate && post.data.tags.includes(tag),
) as EntryWithPubDate<"blog">[]
)
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
.map(async (post) => ({
...post,
authors: await getEntries(post.data.authors),
})),
),
} satisfies Props, } satisfies Props,
})), })),
); );
@ -116,12 +128,14 @@ const totalWorksWithTag = t(
props.tag, props.tag,
props.stories.length, props.stories.length,
props.games.length, props.games.length,
props.blogPosts.length,
); );
--- ---
<GalleryLayout pageTitle={`Works tagged "${props.tag}"`}> <GalleryLayout pageTitle={`Works tagged "${props.tag}"`}>
<meta slot="head" content={`Bad Manners || ${totalWorksWithTag || props.tag}`} property="og:description" /> <meta slot="head" content={`Bad Manners || ${totalWorksWithTag || props.tag}`} property="og:description" />
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{props.tag}"</h1> <h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{props.tag}"</h1>
<hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
<div class="my-4"> <div class="my-4">
<Prose> <Prose>
{description ? <Markdown of={description} /> : null} {description ? <Markdown of={description} /> : null}
@ -148,6 +162,7 @@ const totalWorksWithTag = t(
class="u-url text-link hover:underline focus:underline" class="u-url text-link hover:underline focus:underline"
href={`/stories/${story.slug}`} href={`/stories/${story.slug}`}
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning)} title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}
data-tooltip
> >
{story.data.thumbnail ? ( {story.data.thumbnail ? (
<div class="flex aspect-square max-w-[192px] justify-center"> <div class="flex aspect-square max-w-[192px] justify-center">
@ -210,9 +225,10 @@ const totalWorksWithTag = t(
class="u-url text-link hover:underline focus:underline" class="u-url text-link hover:underline focus:underline"
href={`/games/${game.slug}`} href={`/games/${game.slug}`}
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning)} title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning)}
data-tooltip
> >
{game.data.thumbnail ? ( {game.data.thumbnail ? (
<div class="flex aspect-[630/500] max-w-[192px] justify-center"> <div class="flex aspect-square max-w-[192px] justify-center">
<Image <Image
class="u-photo m-auto" class="u-photo m-auto"
src={game.data.thumbnail} src={game.data.thumbnail}
@ -255,4 +271,63 @@ const totalWorksWithTag = t(
</section> </section>
) )
} }
{
props.blogPosts.length > 0 && (
<section class="my-2" aria-labelledby="content-blogPosts">
<h2 id="content-blogPosts" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
Blog posts
</h2>
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
{props.blogPosts.map((post) => (
<li class="h-entry break-inside-avoid" lang={post.data.lang}>
<a
class="u-url text-link hover:underline focus:underline"
href={`/blog/${post.slug}`}
title={markdownToPlaintext(post.data.description)}
data-tooltip
>
{post.data.thumbnail ? (
<div class="flex aspect-square max-w-[192px] justify-center">
<Image
class="u-photo m-auto"
src={post.data.thumbnail}
alt={`Thumbnail for ${post.data.title}`}
width={192}
/>
</div>
) : null}
<div class="max-w-[192px] text-sm">
<span class="p-name" aria-label="Title">
{post.data.title}
</span>
<br />
<time
class="dt-published italic"
datetime={post.data.pubDate.toISOString().slice(0, 10)}
aria-label="Publish date"
>
{post.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
</time>
</div>
</a>
<div class="sr-only select-none">
<p class="p-category" aria-label="Category">
Blog post
</p>
<p class="p-summary" aria-label="Summary">
{post.data.description}
</p>
<div aria-label="Authors">
<span>{post.authors.length == 1 ? "Author:" : "Authors:"}</span>
{post.authors.map((author) => (
<UserComponent rel="author" class="p-author" user={author} lang={post.data.lang} />
))}
</div>
</div>
</li>
))}
</ul>
</section>
)
}
</GalleryLayout> </GalleryLayout>