diff --git a/src/content/config.ts b/src/content/config.ts index 37958d0..5052139 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -145,7 +145,7 @@ const copyrightedCharacters = z `"copyrightedCharacters" cannot mix empty catch-all key with other keys`, ) .default({}); -/** A record of the format `{ en: string, tok?: string, ... }`. */ +/** A record with a mandatory `en` value and optional strings for the remaining languages. */ const langRecord = z.object({ [DEFAULT_LANG]: z.string() }).and(z.record(lang, z.string())); /** Common attributes for published content (stories + games). */ const publishedContent = z @@ -241,7 +241,6 @@ const publishedContent = z ) .refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published content`) .refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published content`) - .refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published content`) .refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published content`); // Types @@ -312,14 +311,16 @@ const blogCollection = defineCollection({ schema: ({ image }) => z .object({ - // Optional parameters + // Required parameters, but optional for drafts (isDraft === true) thumbnail: image().optional(), + // Optional parameters thumbnailWidth: z.number().int().optional(), thumbnailHeight: z.number().int().optional(), prev: reference("blog").nullish(), next: reference("blog").nullish(), }) - .and(publishedContent), + .and(publishedContent) + .refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published blog post`), }); // Data collections @@ -364,7 +365,7 @@ const tagCategoriesCollection = defineCollection({ .array( z.object({ name: langRecord.or(z.string()), - description: z.string().trim().optional(), + description: langRecord.or(z.string().trim()).optional(), related: z.array(z.string()).default([]), }), ) diff --git a/src/content/stories/good-pet.md b/src/content/stories/good-pet.md index c8c8742..c7bbde5 100644 --- a/src/content/stories/good-pet.md +++ b/src/content/stories/good-pet.md @@ -15,7 +15,7 @@ posts: furaffinity: https://www.furaffinity.net/view/58363412/ inkbunny: https://inkbunny.net/s/3442516 sofurry: https://www.sofurry.com/view/2185882 - # sofurrybeta: TODO + sofurrybeta: https://sofurrybeta.com/s/znJELqqm weasyl: https://www.weasyl.com/~badmanners/submissions/2422380/good-pet mastodon: https://meow.social/@BadManners/113260697648221209 tags: @@ -73,7 +73,7 @@ Sitting on a mess of his anal juices over the carpet, the otter hummed as he adm "Hmmmf..." Jake carefully got up to sit back on the couch. "You were a good pet, but you make for an even better toy..." -Brad spoke apprehensively. "M-Master, I–" +Brad spoke in an apprehensive Manner. "M-Master, I–" "Quiet. Keep licking." diff --git a/src/content/tag-categories/1-types-of-vore.yaml b/src/content/tag-categories/1-types-of-vore.yaml index 950cf56..f012fb8 100644 --- a/src/content/tag-categories/1-types-of-vore.yaml +++ b/src/content/tag-categories/1-types-of-vore.yaml @@ -2,7 +2,9 @@ name: Types of vore index: 1 tags: - name: { en: oral vore, tok: moku musi kepeken uta } - description: Scenarios where prey are consumed by the predator's mouth. + description: + en: Scenarios where prey are consumed by the predator's mouth. + tok: jan li moku musi e jan ante kepeken uta. - name: anal vore description: Scenarios where prey are consumed by the predator's butt/anus. - name: cock vore diff --git a/src/content/tag-categories/2-body-types.yaml b/src/content/tag-categories/2-body-types.yaml index ddadbdf..1264646 100644 --- a/src/content/tag-categories/2-body-types.yaml +++ b/src/content/tag-categories/2-body-types.yaml @@ -8,7 +8,9 @@ tags: - name: taur predator description: Scenarios where at least one of the predators is a multi-legged centaur-like creature, with an animal lower body and anthropomorphic upper body. - name: { en: ambiguous predator, tok: sijelo pi jan pi wawa mute li ale } - description: Scenarios where the body type of at least one of the predators is left ambiguous. + description: + en: Scenarios where the body type of at least one of the predators is left ambiguous. + tok: jan pi moku musi pi wawa mute la toki tan sijelo li lon ala. - name: human prey description: Scenarios where at least one of the prey is a human person. - name: anthro prey @@ -16,4 +18,6 @@ tags: - name: feral prey description: Scenarios where at least one of the predators is an animal based on a real or mythological creature. - name: { en: ambiguous prey, tok: sijelo pi jan pi wawa lili li ale } - description: Scenarios where the body type of at least one of the predators is left ambiguous. + description: + en: Scenarios where the body type of at least one of the predators is left ambiguous. + tok: jan pi moku musi pi wawa lili la toki tan sijelo li lon ala. diff --git a/src/content/tag-categories/3-genders.yaml b/src/content/tag-categories/3-genders.yaml index aff336e..aa4a51b 100644 --- a/src/content/tag-categories/3-genders.yaml +++ b/src/content/tag-categories/3-genders.yaml @@ -14,7 +14,9 @@ tags: - name: non-binary predator description: Scenarios where at least one of the predators has a non-binary gender expression, be they genderless/agender, intersex, androgynous, gender-fluid, non-binary and/or non-binary-presenting, et cetera, regardless of undergoing or having undergone gender transition or not. - name: { en: ambiguous gender predator, tok: jan pi wawa mute li meli anu mije } - description: Scenarios where the gender at least one of the predators is left ambiguous. + description: + en: Scenarios where the gender at least one of the predators is left ambiguous. + tok: jan pi moku musi pi wawa mute la toki tan meli anu mije anu tonsi li lon ala. - name: male prey description: Scenarios where at least one of the prey is a man and/or male-presenting. - name: female prey @@ -28,4 +30,6 @@ tags: - name: non-binary prey description: Scenarios where at least one of the predators has a non-binary gender expression, be they genderless/agender, intersex, androgynous, gender-fluid, non-binary and/or non-binary-presenting, et cetera, regardless of undergoing or having undergone gender transition or not. - name: { en: ambiguous gender prey, tok: jan pi wawa lili li meli anu mije } - description: Scenarios where the gender at least one of the predators is left ambiguous. + description: + en: Scenarios where the gender at least one of the predators is left ambiguous. + tok: jan pi moku musi pi wawa lili la toki tan meli anu mije anu tonsi li lon ala. diff --git a/src/content/tag-categories/5-willingness.yaml b/src/content/tag-categories/5-willingness.yaml index 326cd50..f276150 100644 --- a/src/content/tag-categories/5-willingness.yaml +++ b/src/content/tag-categories/5-willingness.yaml @@ -2,7 +2,9 @@ name: Willingness index: 5 tags: - name: { en: willing predator, tok: jan pi wawa mute li wile e moku musi } - description: Scenarios where at least one of the predators participates in vore willingly. + description: + en: Scenarios where at least one of the predators participates in vore willingly. + tok: jan pi moku musi pi wawa mute la jan li wile e moku musi. - name: semi-willing predator description: Scenarios where the willingness of at least one of the predators might be partial, oscillating between willing and unwilling, somewhere in-between, or ambiguous. - name: unwilling predator @@ -14,6 +16,8 @@ tags: - name: semi-willing prey description: Scenarios where the willingness of at least one of the prey might be partial, oscillating between willing and unwilling, somewhere in-between, or ambiguous. - name: { en: unwilling prey, tok: jan pi wawa lili li wile ala e moku musi } - description: Scenarios where at least one of the prey participates in vore unwillingly. + description: + en: Scenarios where at least one of the prey participates in vore unwillingly. + tok: jan pi moku musi pi wawa lili la jan li wile ala e moku musi. - name: asleep prey description: Scenarios where at least one of the predators participates in vore while asleep. diff --git a/src/content/tag-categories/9-type-of-content.yaml b/src/content/tag-categories/9-type-of-content.yaml index f2990c9..546341c 100644 --- a/src/content/tag-categories/9-type-of-content.yaml +++ b/src/content/tag-categories/9-type-of-content.yaml @@ -6,9 +6,13 @@ tags: - name: commission description: Stories made as part of a commission to someone else. - name: { en: flash fiction, tok: lipu lili } - description: Short-format stories with no more than 2,500 words. + description: + en: Short-format stories with no more than 2,500 words. + tok: lipu li jo e nanpa nimi lili. - name: toki pona - description: Stories written in toki pona, the language of good. + description: + en: Stories written in toki pona, the language of good. + tok: lipu li kepeken toki pona. - name: behind the scenes description: Content where I go over the process of making other content. - name: retrospective diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 77761f5..af585f2 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -126,7 +126,7 @@ const UI_STRINGS = { }, "published_content/syndication_furaffinity": { en: "Fur Affinity", - tok: "lipu Panapinisi", + tok: "lipu Panwapinisi", }, "published_content/syndication_inkbunny": { en: "Inkbunny", @@ -134,7 +134,7 @@ const UI_STRINGS = { }, "published_content/syndication_sofurry": { en: "SoFurry", - tok: "lipu Sopanli", + tok: "lipu Sopanwi", }, "published_content/syndication_weasyl": { en: "Weasyl", diff --git a/src/layouts/PublishedContentLayout.astro b/src/layouts/PublishedContentLayout.astro index d517b1b..55d0281 100644 --- a/src/layouts/PublishedContentLayout.astro +++ b/src/layouts/PublishedContentLayout.astro @@ -66,8 +66,22 @@ const categorizedTags: Record<string, { name: string | null; description?: strin (await getCollection("tag-categories")).flatMap((category) => category.data.tags.map<[string, { name: string | null; description?: string }]>(({ name, description }) => typeof name === "string" - ? [name, { name, description }] - : [name[DEFAULT_LANG], { name: name[props.lang] ?? null, description }], + ? [ + name, + { + name, + description: + typeof description === "object" ? (description[props.lang] ?? description[DEFAULT_LANG]) : description, + }, + ] + : [ + name[DEFAULT_LANG], + { + name: name[props.lang] ?? null, + description: + typeof description === "object" ? (description[props.lang] ?? description[DEFAULT_LANG]) : description, + }, + ], ), ), ); diff --git a/src/pages/tags.astro b/src/pages/tags.astro index 04cbffb..57896f2 100644 --- a/src/pages/tags.astro +++ b/src/pages/tags.astro @@ -3,6 +3,7 @@ import { getCollection } from "astro:content"; import { slug } from "github-slugger"; import GalleryLayout from "@layouts/GalleryLayout.astro"; import { markdownToPlaintext } from "@utils/markdown_to_plaintext"; +import { DEFAULT_LANG } from "@i18n"; interface Tag { id: string; @@ -38,7 +39,12 @@ const categorizedTags = tagCategories }) .map((category) => { const tagList = category.data.tags.map<Tag>(({ name, description }) => { - description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " "); + description = + description && + markdownToPlaintext(typeof description === "object" ? description[DEFAULT_LANG] : description).replaceAll( + /\n+/g, + " ", + ); const tag = typeof name === "string" ? name : name.en; return { id: slug(tag), name: tag, description }; }); diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro index 26ad01d..2e354e1 100644 --- a/src/pages/tags/[slug].astro +++ b/src/pages/tags/[slug].astro @@ -62,7 +62,10 @@ export const getStaticPaths: GetStaticPaths = async () => { if (key in acc) { throw new Error(`Duplicated tag "${key}" found in multiple tag-categories lists`); } - acc[key] = { description, related: related.length > 0 ? related : undefined }; + acc[key] = { + description: typeof description === "object" ? description[DEFAULT_LANG] : description, + related: related.length > 0 ? related : undefined, + }; }); return acc; }, diff --git a/src/utils/feed.ts b/src/utils/feed.ts index 202a3d8..5e013d9 100644 --- a/src/utils/feed.ts +++ b/src/utils/feed.ts @@ -10,6 +10,7 @@ import { t, type Lang } from "@i18n"; import type { AstroComponentFactory } from "astro/runtime/server/index.js"; import mdxRenderer from "astro/jsx/server.js"; import { htmlToAbsoluteUrls } from "./html_to_absolute_urls"; +import { formatCopyrightedCharacters } from "./format_copyrighted_characters"; export type FeedItem = RSSFeedItem & Required<Pick<RSSFeedItem, "title" | "pubDate" | "link" | "description" | "categories" | "content">>; @@ -39,6 +40,7 @@ export async function storyFeedItem( slug: CollectionEntry<"stories">["slug"], content: AstroComponentFactory, ): Promise<FeedItem> { + const copyrightedCharacters = await formatCopyrightedCharacters(data.copyrightedCharacters); return { title: `New story! "${data.title}"`, pubDate: toNoonUTCDate(data.pubDate), @@ -74,7 +76,10 @@ export async function storyFeedItem( : "") + `<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}</em></p>` + `<hr>${await container.renderToString(content)}` + - `<hr><h2>Description</h2>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`, + `<hr><h2>Description</h2>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}` + + (copyrightedCharacters.length > 0 + ? `<ul>${copyrightedCharacters.map(({ user, characters }) => "<li>" + t(data.lang, "characters/characters_are_copyrighted_by", getLinkForUser(user, data.lang), characters) + "</li>")}</ul>` + : ""), site, ), { @@ -90,6 +95,7 @@ export async function gameFeedItem( slug: CollectionEntry<"games">["slug"], content: AstroComponentFactory, ): Promise<FeedItem> { + const copyrightedCharacters = await formatCopyrightedCharacters(data.copyrightedCharacters); return { title: `New game! "${data.title}"`, pubDate: toNoonUTCDate(data.pubDate), @@ -112,7 +118,10 @@ export async function gameFeedItem( `<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` + `<hr><p><em>${data.contentWarning}</em></p>` + `<hr>${await container.renderToString(content)}` + - `<hr><h2>Description</h2>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`, + `<hr><h2>Description</h2>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}` + + (copyrightedCharacters.length > 0 + ? `<ul>${copyrightedCharacters.map(({ user, characters }) => "<li>" + t(data.lang, "characters/characters_are_copyrighted_by", getLinkForUser(user, data.lang), characters) + "</li>")}</ul>` + : ""), site, ), {