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,
       ),
       {