diff --git a/src/assets/images/crossing_over/bard.png b/src/assets/images/crossing_over/bard.png index 6177c25..5febb36 100644 Binary files a/src/assets/images/crossing_over/bard.png and b/src/assets/images/crossing_over/bard.png differ diff --git a/src/assets/thumbnails/other/crossing_over_retrospective.png b/src/assets/thumbnails/other/crossing_over_retrospective.png new file mode 100644 index 0000000..94140fb Binary files /dev/null and b/src/assets/thumbnails/other/crossing_over_retrospective.png differ diff --git a/src/assets/thumbnails/other/taken_in_breakdown.png b/src/assets/thumbnails/other/taken_in_breakdown.png new file mode 100644 index 0000000..19734c4 Binary files /dev/null and b/src/assets/thumbnails/other/taken_in_breakdown.png differ diff --git a/src/components/icons/IconBlog.astro b/src/components/icons/IconBlog.astro new file mode 100644 index 0000000..2a12e98 --- /dev/null +++ b/src/components/icons/IconBlog.astro @@ -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> diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts index f973712..3bb5d5c 100644 --- a/src/components/icons/index.ts +++ b/src/components/icons/index.ts @@ -1,5 +1,6 @@ export { default as IconArrowBack } from "./IconArrowBack.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 IconBriefcase } from "./IconBriefcase.astro"; export { default as IconChevronLeft } from "./IconChevronLeft.astro"; diff --git a/src/content/blog/crossing-over-postmortem.md b/src/content/blog/crossing-over-retrospective.md similarity index 98% rename from src/content/blog/crossing-over-postmortem.md rename to src/content/blog/crossing-over-retrospective.md index a33a780..0c78676 100644 --- a/src/content/blog/crossing-over-postmortem.md +++ b/src/content/blog/crossing-over-retrospective.md @@ -1,13 +1,23 @@ --- -title: Jamming Over +title: "Jamming Over: A Postmortem" pubDate: 2024-03-26 -isDraft: true isAgeRestricted: true authors: bad-manners -# thumbnail: /src/assets/thumbnails/story_thumbnail.png +thumbnail: /src/assets/thumbnails/other/crossing_over_retrospective.png 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!** -tags: [] + 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: + - 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: - 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. 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. diff --git a/src/content/blog/breakdown-taken-in.md b/src/content/blog/taken-in-breakdown.md similarity index 99% rename from src/content/blog/breakdown-taken-in.md rename to src/content/blog/taken-in-breakdown.md index be70e3b..5a36024 100644 --- a/src/content/blog/breakdown-taken-in.md +++ b/src/content/blog/taken-in-breakdown.md @@ -1,15 +1,26 @@ --- -title: "Story Breakdown: Taken In" +title: "Taken In: Story Breakdown!" pubDate: 2024-01-23 -isDraft: true isAgeRestricted: true authors: bad-manners -# thumbnail: /src/assets/thumbnails/story_thumbnail.png +thumbnail: /src/assets/thumbnails/other/taken_in_breakdown.png 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. -tags: [] -relatedGames: - - crossing-over +tags: + - behind the scenes + - 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. diff --git a/src/content/games/crossing-over.md b/src/content/games/crossing-over.md index 36ae153..bfd1a4f 100644 --- a/src/content/games/crossing-over.md +++ b/src/content/games/crossing-over.md @@ -41,7 +41,7 @@ tags: - soul vore - long-term endo relatedBlogPosts: - - crossing-over-postmortem + - crossing-over-retrospective --- <iframe diff --git a/src/content/stories/taken-in.md b/src/content/stories/taken-in.md index befdf9e..800a457 100644 --- a/src/content/stories/taken-in.md +++ b/src/content/stories/taken-in.md @@ -29,6 +29,8 @@ tags: - point of view copyrightedCharacters: "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. diff --git a/src/content/tag-categories/9-type-of-content.yaml b/src/content/tag-categories/9-type-of-content.yaml index 83677fe..92739fb 100644 --- a/src/content/tag-categories/9-type-of-content.yaml +++ b/src/content/tag-categories/9-type-of-content.yaml @@ -9,3 +9,7 @@ tags: description: Short-format stories with no more than 2,500 words. - name: toki pona 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. diff --git a/src/i18n/index.ts b/src/i18n/index.ts index cf5dc82..2bbc23b 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -298,7 +298,7 @@ const UI_STRINGS = { }, // Tag-related strings "tag/total_works_with_tag": { - en: (tag: string, storiesCount: number, gamesCount: number) => { + en: (tag: string, storiesCount: number, gamesCount: number, blogPostsCount: number) => { const content = []; if (storiesCount > 0) { content.push(UI_STRINGS["util/enumerate"].en(storiesCount, "story", "stories")); @@ -306,6 +306,9 @@ const UI_STRINGS = { if (gamesCount > 0) { 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) { return `No works tagged with "${tag}".`; } diff --git a/src/layouts/BlogLayout.astro b/src/layouts/BlogPostLayout.astro similarity index 94% rename from src/layouts/BlogLayout.astro rename to src/layouts/BlogPostLayout.astro index 2ea3141..451ab74 100644 --- a/src/layouts/BlogLayout.astro +++ b/src/layouts/BlogPostLayout.astro @@ -16,6 +16,7 @@ const series = props.series && (await getEntry(props.series)); const authorsList = await getEntries(props.authors); const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.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 @@ -44,6 +45,7 @@ const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !ga : undefined} relatedStories={relatedStories} relatedGames={relatedGames} + relatedBlogPosts={relatedBlogPosts} posts={props.posts} labelReturnTo={{ title: t(props.lang, "blog/return_to_blog_posts"), link: "/blog" }} labelPreviousContent={t(props.lang, "blog/previous_post_aria_label")} diff --git a/src/layouts/GalleryLayout.astro b/src/layouts/GalleryLayout.astro index ce96f5e..6739466 100644 --- a/src/layouts/GalleryLayout.astro +++ b/src/layouts/GalleryLayout.astro @@ -4,15 +4,16 @@ import BaseLayout from "./BaseLayout.astro"; import logoBM from "../assets/images/logo_bm.png"; import { t } from "../i18n"; import { - IconHome, + IconBlog, + IconBook, IconBriefcase, + IconGamepad, + IconHome, + IconMagnifyingGlass, + IconMoon, IconSquareRSS, IconSun, - IconMoon, - IconMagnifyingGlass, IconTags, - IconGamepad, - IconBook, } from "../components/icons"; type Props = { @@ -80,6 +81,12 @@ const isCurrentRoute = (path: string) => <span class="order-3 group-hover:underline group-focus:underline">Games</span> </a> </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> <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" /> diff --git a/src/layouts/GameLayout.astro b/src/layouts/GameLayout.astro index 75a76af..f0bdb25 100644 --- a/src/layouts/GameLayout.astro +++ b/src/layouts/GameLayout.astro @@ -15,6 +15,7 @@ const series = props.series && (await getEntry(props.series)); const authorsList = await getEntries(props.authors); const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.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 @@ -40,6 +41,7 @@ const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !ga : undefined} relatedStories={relatedStories} relatedGames={relatedGames} + relatedBlogPosts={relatedBlogPosts} posts={props.posts} labelReturnTo={{ title: t(props.lang, "game/return_to_games"), link: "/games" }} labelPreviousContent={t(props.lang, "game/previous_game_aria_label")} diff --git a/src/layouts/StoryLayout.astro b/src/layouts/StoryLayout.astro index 33e199d..84483d8 100644 --- a/src/layouts/StoryLayout.astro +++ b/src/layouts/StoryLayout.astro @@ -19,6 +19,7 @@ const commissionersList = props.commissioners && (await getEntries(props.commiss const requestersList = props.requesters && (await getEntries(props.requesters)); const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.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(); --- @@ -51,6 +52,7 @@ const wordCount = props.wordCount?.toString(); : undefined} relatedStories={relatedStories} relatedGames={relatedGames} + relatedBlogPosts={relatedBlogPosts} posts={props.posts} labelReturnTo={{ title: t(props.lang, "story/return_to_stories"), link: "/stories/1" }} labelPreviousContent={t(props.lang, "story/previous_story_aria_label")} diff --git a/src/pages/blog.astro b/src/pages/blog.astro index 8a7a961..dcac40b 100644 --- a/src/pages/blog.astro +++ b/src/pages/blog.astro @@ -2,7 +2,6 @@ import { Image } from "astro:assets"; import { getCollection, getEntries, type CollectionEntry } from "astro:content"; import GalleryLayout from "../layouts/GalleryLayout.astro"; -import { t } from "../i18n"; import UserComponent from "../components/UserComponent.astro"; import { markdownToPlaintext } from "../utils/markdown_to_plaintext"; @@ -34,13 +33,13 @@ const posts = await Promise.all( data-tooltip > {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 loading={i < 10 ? "eager" : "lazy"} class="u-photo m-auto" src={post.data.thumbnail} alt={`Thumbnail for ${post.data.title}`} - width={288} + width={192} /> </div> ) : null} diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro index f037d78..adba74c 100644 --- a/src/pages/blog/[...slug].astro +++ b/src/pages/blog/[...slug].astro @@ -2,7 +2,7 @@ import type { GetStaticPaths } from "astro"; import { type CollectionEntry, getCollection } from "astro:content"; import { PUBLISH_DRAFTS } from "astro:env/server"; -import BlogLayout from "../../layouts/BlogLayout.astro"; +import BlogPostLayout from "../../layouts/BlogPostLayout.astro"; type Props = CollectionEntry<"blog">; @@ -24,6 +24,6 @@ const post = Astro.props; const { Content } = await post.render(); --- -<BlogLayout {...post.data}> +<BlogPostLayout {...post.data}> <Content /> -</BlogLayout> +</BlogPostLayout> diff --git a/src/pages/games.astro b/src/pages/games.astro index 9e44b6f..8474c32 100644 --- a/src/pages/games.astro +++ b/src/pages/games.astro @@ -33,7 +33,7 @@ const games = await Promise.all( data-tooltip > {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 loading={i < 10 ? "eager" : "lazy"} class="u-photo m-auto" diff --git a/src/pages/index.astro b/src/pages/index.astro index 4f46779..51db8e7 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -75,7 +75,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all( date: post.data.pubDate, fn: async () => ({ - type: "Game", + type: "Blog post", thumbnail: post.data.thumbnail, href: `/blog/${post.slug}`, title: post.data.title, diff --git a/src/pages/tags/[badSlug].astro b/src/pages/tags/[badSlug].astro index 2bb7fb5..666134c 100644 --- a/src/pages/tags/[badSlug].astro +++ b/src/pages/tags/[badSlug].astro @@ -26,6 +26,7 @@ const { badTag } = Astro.props; <meta content="No." property="og:description" /> <meta name="robots" content="noindex" /> </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> </GalleryLayout> diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro index 0a92916..60d9ca5 100644 --- a/src/pages/tags/[slug].astro +++ b/src/pages/tags/[slug].astro @@ -9,6 +9,7 @@ import Prose from "../../components/Prose.astro"; import { t, DEFAULT_LANG } from "../../i18n"; import { qualifyLocalURLsInMarkdown } from "../../utils/qualify_local_urls_in_markdown"; import UserComponent from "../../components/UserComponent.astro"; +import { markdownToPlaintext } from "../../utils/markdown_to_plaintext"; type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } }; @@ -18,6 +19,7 @@ type Props = { related?: string[]; stories: (EntryWithPubDate<"stories"> & { authors: CollectionEntry<"users">[] })[]; games: (EntryWithPubDate<"games"> & { authors: CollectionEntry<"users">[] })[]; + blogPosts: (EntryWithPubDate<"blog"> & { authors: CollectionEntry<"users">[] })[]; }; type Params = { @@ -25,24 +27,22 @@ type Params = { }; 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("games"), + getCollection("blog"), getCollection("series"), getCollection("tag-categories"), ]); const seriesTags = new Set(series.map((s) => s.data.name)); const tags = new Set<string>(); - stories.forEach((story) => { - story.data.tags.forEach((tag) => { - tags.add(tag); - }); - }); - games.forEach((game) => { - game.data.tags.forEach((tag) => { - tags.add(tag); - }); - }); + [stories, games, blogPosts].forEach((collection) => + collection.forEach((content) => { + content.data.tags.forEach((tag) => { + tags.add(tag); + }); + }), + ); const tagDescriptions = tagCategories.reduce( (acc, category) => { category.data.tags.forEach(({ name, description, related }) => { @@ -100,6 +100,18 @@ export const getStaticPaths: GetStaticPaths = async () => { 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, })), ); @@ -116,12 +128,14 @@ const totalWorksWithTag = t( props.tag, props.stories.length, props.games.length, + props.blogPosts.length, ); --- <GalleryLayout pageTitle={`Works tagged "${props.tag}"`}> <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"> <Prose> {description ? <Markdown of={description} /> : null} @@ -148,6 +162,7 @@ const totalWorksWithTag = t( class="u-url text-link hover:underline focus:underline" href={`/stories/${story.slug}`} title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning)} + data-tooltip > {story.data.thumbnail ? ( <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" href={`/games/${game.slug}`} title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning)} + data-tooltip > {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 class="u-photo m-auto" src={game.data.thumbnail} @@ -255,4 +271,63 @@ const totalWorksWithTag = t( </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>