diff --git a/package-lock.json b/package-lock.json
index ae4a1db..eb0546d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
         "@tailwindcss/typography": "^0.5.14",
         "astro": "^4.13.3",
         "astro-pagefind": "^1.6.0",
+        "clsx": "^2.1.1",
         "github-slugger": "^2.0.0",
         "marked": "^14.0.0",
         "pagefind": "^1.1.0",
@@ -5046,11 +5047,12 @@
       ]
     },
     "node_modules/micromatch": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
-      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "license": "MIT",
       "dependencies": {
-        "braces": "^3.0.2",
+        "braces": "^3.0.3",
         "picomatch": "^2.3.1"
       },
       "engines": {
diff --git a/package.json b/package.json
index 6d5ad19..c187973 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
     "@tailwindcss/typography": "^0.5.14",
     "astro": "^4.13.3",
     "astro-pagefind": "^1.6.0",
+    "clsx": "^2.1.1",
     "github-slugger": "^2.0.0",
     "marked": "^14.0.0",
     "pagefind": "^1.1.0",
diff --git a/scripts/deploy-lftp.ts b/scripts/deploy-lftp.ts
index 7ce972a..bfafbf0 100644
--- a/scripts/deploy-lftp.ts
+++ b/scripts/deploy-lftp.ts
@@ -17,7 +17,7 @@ async function deployLftp({ host, user, password, targetFolder, sourceFolder, as
     [
       "-c",
       [
-        `open -u ${user},${password} ${host}`,
+        `open -u ${user},${password.replaceAll(/([ \t.,:;?!`'"^|*+#&$\(\)\[\]{}<>\\/-])/, "\\$1")} ${host}`,
         `mirror --reverse --include-glob ${join(assetsFolder, "*")} --delete --only-missing --no-perms --verbose ${n(sourceFolder)} ${n(targetFolder)}`,
         `mirror --reverse --exclude-glob ${join(assetsFolder, "*")} --delete                --no-perms --verbose ${n(sourceFolder)} ${n(targetFolder)}`,
         `bye`,
@@ -27,9 +27,9 @@ async function deployLftp({ host, user, password, targetFolder, sourceFolder, as
       stdio: "inherit",
     },
   );
-  await new Promise<void>((resolve, reject) => {
+  await new Promise((resolve, reject) => {
     process.on("close", (code) =>
-      (code === 0) ? resolve() : reject(`lftp failed with code ${code}`),
+      (code === 0) ? resolve(0) : reject(`lftp failed with code ${code}`),
     );
   });
 }
diff --git a/scripts/export-story.ts b/scripts/export-story.ts
index 6c72423..f594444 100644
--- a/scripts/export-story.ts
+++ b/scripts/export-story.ts
@@ -66,20 +66,20 @@ async function exportStory(slug: string, options: { outputDir: string }) {
   /* Spawn Astro dev server */
   console.log("Starting Astro development server...");
   const devServerProcess = spawn("./node_modules/.bin/astro", ["dev"], { stdio: 'pipe' });
-  const promise = new Promise<string>((resolve, reject) => {
-    const localServerRegex = /Local\s+(http\S+)/
-    const lines = createInterface({ input: devServerProcess.stdout });
-    lines.on("line", (line) => {
-      const match = localServerRegex.exec(line);
-      if (match && match[1]) {
-        resolve(match[1]);
-      }
-    });
-    lines.on("close", reject);
-  });
-  const astroURL = await promise;
-  console.log(`Astro listening on ${astroURL}`);
+  let astroURL: string | null = null;
   try {
+    astroURL = await new Promise<string>((resolve, reject) => {
+      const localServerRegex = /Local\s+(http:\/\/\S+)/
+      const lines = createInterface({ input: devServerProcess.stdout });
+      lines.on("line", (line) => {
+        const match = localServerRegex.exec(line);
+        if (match && match[1]) {
+          resolve(match[1]);
+        }
+      });
+      lines.on("close", reject);
+    });
+    console.log(`Astro listening on ${astroURL}`);
     const response = await fetchRetry(new URL(`/api/healthcheck`, astroURL), { retries: 5, retryDelay: 2000 });
     if (!response.ok) {
       throw new Error(response.statusText);
@@ -106,9 +106,9 @@ async function exportStory(slug: string, options: { outputDir: string }) {
     }
     const data: { story: string, description: Record<string, string>, thumbnail: string | null } = await response.json();
     await Promise.all(
-      Object.entries(data.description).map(async ([website, description]) => {
+      Object.entries(data.description).map(async ([filename, description]) => {
         return await writeFile(
-          join(outputDir, `description_${website}.${website === "weasyl" ? "md" : "txt"}`),
+          join(outputDir, filename),
           description,
         );
       }),
@@ -124,7 +124,7 @@ async function exportStory(slug: string, options: { outputDir: string }) {
         if (!thumbnail.ok) {
           throw new Error("Failed to get thumbnail");
         }
-        const thumbnailExt = thumbnail.headers.get("Content-Type")?.startsWith("image/png") ? "png" : "jpg";
+        const thumbnailExt = thumbnail.headers.get("Content-Type")!.startsWith("image/png") ? "png" : "jpg";
         await writeFile(join(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer()));
       }
     }
diff --git a/src/components/AgeRestrictedModal.astro b/src/components/AgeRestrictedModal.astro
index 439547b..421ce24 100644
--- a/src/components/AgeRestrictedModal.astro
+++ b/src/components/AgeRestrictedModal.astro
@@ -28,6 +28,7 @@ import IconTriangleExclamation from "./icons/IconTriangleExclamation.astro";
     </p>
     <div class="flex w-full max-w-md flex-col-reverse justify-evenly gap-y-5 px-6 pt-5 sm:max-w-2xl sm:flex-row">
       <button
+        style={{display: "none"}}
         data-modal-reject
         id="age-verification-reject"
         class="rounded bg-stone-400 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-stone-300 dark:text-stone-900 dark:hover:bg-stone-600 dark:hover:text-stone-50 dark:focus:bg-stone-600 dark:focus:text-stone-50"
@@ -35,6 +36,7 @@ import IconTriangleExclamation from "./icons/IconTriangleExclamation.astro";
         Cancel
       </button>
       <button
+        style={{display: "none"}}
         data-modal-accept
         id="age-verification-accept"
         class="rounded bg-bm-500 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-bm-400 dark:text-stone-900 dark:hover:bg-stone-600 dark:hover:text-stone-50 dark:focus:bg-stone-600 dark:focus:text-stone-50"
@@ -49,13 +51,18 @@ import IconTriangleExclamation from "./icons/IconTriangleExclamation.astro";
 
 <script>
   const ENABLE_VIEW_TRANSITIONS = false;
+  type AgeVerified = "true" | undefined;
 
   const ageRestrictedModalSetup = () => {
-    const modal = document.querySelector<HTMLElementTagNameMap["div"]>("body > div#modal-age-restricted");
+    const modal = document.querySelector<HTMLElementTagNameMap["div"]>("div#modal-age-restricted");
+    // Not an age-restricted page
     if (!modal) {
-      throw new Error("Missing #modal-age-restricted element! Make sure that it's a direct child of body.");
+      return;
     }
-    let ageVerified: "true" | undefined = localStorage.ageVerified;
+    if (modal !== document.querySelector("body > div#modal-age-restricted")) {
+      throw new Error("#modal-age-restricted must be a direct child of the body element!");
+    }
+    let ageVerified: AgeVerified = localStorage.ageVerified;
     if (ageVerified !== "true") {
       const rejectButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")!;
       const onRejectButtonClick = (e: MouseEvent) => {
@@ -63,7 +70,9 @@ import IconTriangleExclamation from "./icons/IconTriangleExclamation.astro";
         location.href = "about:blank";
       };
       rejectButton.addEventListener("click", onRejectButtonClick);
-      modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-accept]")!.addEventListener(
+      rejectButton.style.removeProperty("display");
+      const acceptButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-accept]")!;
+      acceptButton.addEventListener(
         "click",
         (e: MouseEvent) => {
           e.preventDefault();
@@ -72,10 +81,16 @@ import IconTriangleExclamation from "./icons/IconTriangleExclamation.astro";
           localStorage.ageVerified = ageVerified;
           document.body.style.overflow = "auto";
           document.querySelectorAll("body > :not(#modal-age-restricted)").forEach((el) => el.removeAttribute("inert"));
+          document.body.querySelectorAll<HTMLElementTagNameMap["a"]>("a[href][data-age-restricted]").forEach((el) => {
+            let newHref = new URL(el.href);
+            newHref.searchParams.set("ageVerified", "true");
+            el.href = newHref.href;
+          });
           modal.style.display = "none";
         },
         { once: true },
       );
+      acceptButton.style.removeProperty("display");
       rejectButton.focus();
     }
   };
diff --git a/src/components/AgeRestrictedScriptInline.astro b/src/components/AgeRestrictedScriptInline.astro
index 6bf941b..ac1696a 100644
--- a/src/components/AgeRestrictedScriptInline.astro
+++ b/src/components/AgeRestrictedScriptInline.astro
@@ -1,4 +1,4 @@
 ---
 ---
 
-<script is:inline>function a(){let b=document,c="#modal-age-restricted",d="true";localStorage.ageVerified!==d&&((b.body.style.overflow="hidden"),b.querySelectorAll("body > :not("+c+")").forEach(e=>e.setAttribute("inert",d)),(b.querySelector("body > "+c).style.display="block"));}document.addEventListener("astro:after-swap",a);a()</script>
+<script is:inline>function a(){let b=document,c="#modal-age-restricted",d="true",e=b.querySelector("body > "+c),f="ageVerified",g="searchParams",h=localStorage;new URL(b.location)[g].get(f)===d&&(h[f]=d);e&&(h[f]===d?b.querySelectorAll("a[href][data-age-restricted]").forEach(x=>{let y=new URL(x.href);y[g].set(f,d);x.href=y.href}):((b.body.style.overflow="hidden"),b.querySelectorAll("body > :not("+c+")").forEach(x=>x.setAttribute("inert",d)),(e.style.display="block")))};document.addEventListener("astro:after-swap",a);a()</script>
diff --git a/src/components/CopyrightedCharacters.astro b/src/components/CopyrightedCharacters.astro
index f7d4d72..7d63011 100644
--- a/src/components/CopyrightedCharacters.astro
+++ b/src/components/CopyrightedCharacters.astro
@@ -24,7 +24,7 @@ const charactersPerUser = copyrightedCharacters ? await formatCopyrightedCharact
               t(lang, "characters/characters_are_copyrighted_by", user, characters[0] === "" ? [] : characters)
             }
           >
-            <UserComponent class="p-name" lang={lang} user={user} />
+            <UserComponent lang={lang} user={user} />
           </CopyrightedCharactersItem>
         ))}
       </ul>
diff --git a/src/components/DarkModeScript.astro b/src/components/DarkModeScript.astro
index 5692f07..f1160a6 100644
--- a/src/components/DarkModeScript.astro
+++ b/src/components/DarkModeScript.astro
@@ -6,10 +6,10 @@ import DarkModeScriptInline from "./DarkModeScriptInline.astro";
 
 <script>
   const ENABLE_VIEW_TRANSITIONS = false;
-  type ColorScheme = "auto" | "dark" | "light";
+  type ColorScheme = "auto" | "dark" | "light" | undefined;
 
   const colorSchemeSetup = () => {
-    let colorScheme: ColorScheme | undefined = localStorage.colorScheme;
+    let colorScheme: ColorScheme = localStorage.colorScheme;
     if (!colorScheme || colorScheme === "auto") {
       colorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
     }
@@ -25,10 +25,10 @@ import DarkModeScriptInline from "./DarkModeScriptInline.astro";
       localStorage.colorScheme = colorScheme;
     };
     document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => {
+      button.addEventListener("click", toggleColorScheme);
       button.classList.remove("hidden");
       button.style.removeProperty("display");
       button.setAttribute("aria-hidden", "false");
-      button.addEventListener("click", toggleColorScheme);
     });
   };
   if (ENABLE_VIEW_TRANSITIONS) {
@@ -36,4 +36,4 @@ import DarkModeScriptInline from "./DarkModeScriptInline.astro";
   } else {
     colorSchemeSetup();
   }
-</script>
\ No newline at end of file
+</script>
diff --git a/src/components/MastodonComments.astro b/src/components/MastodonComments.astro
index 8cb9ed0..b9b2c73 100644
--- a/src/components/MastodonComments.astro
+++ b/src/components/MastodonComments.astro
@@ -84,6 +84,7 @@ const { link, instance, user, postId } = Astro.props;
       <a
         data-author
         class="p-author h-card u-url text-link flex items-center text-lg hover:underline focus:underline"
+        rel="nofollow"
         target="_blank"
       >
         <picture>
@@ -95,6 +96,7 @@ const { link, instance, user, postId } = Astro.props;
       <a
         data-post-link
         class="u-url text-link my-1 flex items-center text-sm font-light hover:underline focus:underline"
+        rel="nofollow"
         target="_blank"
       >
         <time class="dt-published mr-1" data-published-date aria-label="Publish date"></time>
diff --git a/src/components/Navigation.astro b/src/components/Navigation.astro
deleted file mode 100644
index 633d13e..0000000
--- a/src/components/Navigation.astro
+++ /dev/null
@@ -1,38 +0,0 @@
----
-
----
-
-<nav>
-  <ul>
-    <li>
-      <a
-        class="hover:text-green-800 hover:underline focus:text-green-800 focus:underline dark:hover:text-bm-300 dark:focus:text-bm-300"
-        href="/">Home</a
-      >
-    </li>
-    <li>
-      <a
-        class="hover:text-green-800 hover:underline focus:text-green-800 focus:underline dark:hover:text-bm-300 dark:focus:text-bm-300"
-        href="/stories/1">Stories</a
-      >
-    </li>
-    <li>
-      <a
-        class="hover:text-green-800 hover:underline focus:text-green-800 focus:underline dark:hover:text-bm-300 dark:focus:text-bm-300"
-        href="/games">Games</a
-      >
-    </li>
-    <li>
-      <a
-        class="hover:text-green-800 hover:underline focus:text-green-800 focus:underline dark:hover:text-bm-300 dark:focus:text-bm-300"
-        href="/tags">Tags</a
-      >
-    </li>
-    <li>
-      <a
-        class="hover:text-green-800 hover:underline focus:text-green-800 focus:underline dark:hover:text-bm-300 dark:focus:text-bm-300"
-        href="/search">Search</a
-      >
-    </li>
-  </ul>
-</nav>
diff --git a/src/components/UserComponent.astro b/src/components/UserComponent.astro
index a78f8a8..aa2a56f 100644
--- a/src/components/UserComponent.astro
+++ b/src/components/UserComponent.astro
@@ -1,5 +1,6 @@
 ---
 import type { CollectionEntry } from "astro:content";
+import clsx from "clsx";
 import type { Lang } from "../i18n";
 import { getUsernameForLang } from "../utils/get_username_for_lang";
 
@@ -17,10 +18,10 @@ const link = user.data.preferredLink ? user.data.links[user.data.preferredLink]!
 
 {
   link ? (
-    <a rel={rel} href={link} class:list={[className, "h-card u-url text-link underline"]} target="_blank">
+    <a rel={clsx(rel, "nofollow")} href={link} class:list={[className, "h-card u-url p-name text-link underline"]} target="_blank">
       {username}
     </a>
   ) : (
-    <span class:list={[className, "h-card"]}>{username}</span>
+    <span class:list={[className, "h-card p-name"]}>{username}</span>
   )
 }
diff --git a/src/components/icons/IconBook.astro b/src/components/icons/IconBook.astro
new file mode 100644
index 0000000..f99898a
--- /dev/null
+++ b/src/components/icons/IconBook.astro
@@ -0,0 +1,13 @@
+---
+import SVGIcon from "./SVGIcon.astro";
+
+type Props = {
+  width: string;
+  height: string;
+  class?: string;
+};
+---
+
+<SVGIcon {...Astro.props} viewBox="0 0 448 512">
+  <path d="M96 0C43 0 0 43 0 96L0 416c0 53 43 96 96 96l288 0 32 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l0-64c17.7 0 32-14.3 32-32l0-320c0-17.7-14.3-32-32-32L384 0 96 0zm0 384l256 0 0 64L96 448c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16zm16 48l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" />
+</SVGIcon>
diff --git a/src/components/icons/IconBriefcase.astro b/src/components/icons/IconBriefcase.astro
new file mode 100644
index 0000000..cc4d403
--- /dev/null
+++ b/src/components/icons/IconBriefcase.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="M184 48l144 0c4.4 0 8 3.6 8 8l0 40L176 96l0-40c0-4.4 3.6-8 8-8zm-56 8l0 40L64 96C28.7 96 0 124.7 0 160l0 96 192 0 128 0 192 0 0-96c0-35.3-28.7-64-64-64l-64 0 0-40c0-30.9-25.1-56-56-56L184 0c-30.9 0-56 25.1-56 56zM512 288l-192 0 0 32c0 17.7-14.3 32-32 32l-64 0c-17.7 0-32-14.3-32-32l0-32L0 288 0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-128z"
+  ></path>
+</SVGIcon>
diff --git a/src/components/icons/IconGamepad.astro b/src/components/icons/IconGamepad.astro
new file mode 100644
index 0000000..c54e8c8
--- /dev/null
+++ b/src/components/icons/IconGamepad.astro
@@ -0,0 +1,13 @@
+---
+import SVGIcon from "./SVGIcon.astro";
+
+type Props = {
+  width: string;
+  height: string;
+  class?: string;
+};
+---
+
+<SVGIcon {...Astro.props} viewBox="0 0 640 512">
+  <path d="M192 64C86 64 0 150 0 256S86 448 192 448l256 0c106 0 192-86 192-192s-86-192-192-192L192 64zM496 168a40 40 0 1 1 0 80 40 40 0 1 1 0-80zM392 304a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zM168 200c0-13.3 10.7-24 24-24s24 10.7 24 24l0 32 32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0 0 32c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-32-32 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l32 0 0-32z" />
+</SVGIcon>
diff --git a/src/components/icons/IconMagnifyingGlass.astro b/src/components/icons/IconMagnifyingGlass.astro
new file mode 100644
index 0000000..f78e299
--- /dev/null
+++ b/src/components/icons/IconMagnifyingGlass.astro
@@ -0,0 +1,13 @@
+---
+import SVGIcon from "./SVGIcon.astro";
+
+type Props = {
+  width: string;
+  height: string;
+  class?: string;
+};
+---
+
+<SVGIcon {...Astro.props} viewBox="0 0 512 512">
+  <path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z" />
+</SVGIcon>
\ No newline at end of file
diff --git a/src/components/icons/IconTags.astro b/src/components/icons/IconTags.astro
new file mode 100644
index 0000000..2bdf27c
--- /dev/null
+++ b/src/components/icons/IconTags.astro
@@ -0,0 +1,14 @@
+---
+import SVGIcon from "./SVGIcon.astro";
+
+type Props = {
+  width: string;
+  height: string;
+  class?: string;
+};
+---
+
+<SVGIcon {...Astro.props} viewBox="0 0 512 512">
+  <path d="M345 39.1L472.8 168.4c52.4 53 52.4 138.2 0 191.2L360.8 472.9c-9.3 9.4-24.5 9.5-33.9 .2s-9.5-24.5-.2-33.9L438.6 325.9c33.9-34.3 33.9-89.4 0-123.7L310.9 72.9c-9.3-9.4-9.2-24.6 .2-33.9s24.6-9.2 33.9 .2zM0 229.5L0 80C0 53.5 21.5 32 48 32l149.5 0c17 0 33.3 6.7 45.3 18.7l168 168c25 25 25 65.5 0 90.5L277.3 442.7c-25 25-65.5 25-90.5 0l-168-168C6.7 262.7 0 246.5 0 229.5zM144 144a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
+  />
+</SVGIcon>
\ No newline at end of file
diff --git a/src/content/stories/birdroom.md b/src/content/stories/birdroom.md
index fb4d338..ce1768a 100644
--- a/src/content/stories/birdroom.md
+++ b/src/content/stories/birdroom.md
@@ -9,7 +9,7 @@ thumbnail: /src/assets/thumbnails/bm_10_birdroom.png
 description: |
   Beetle finds an odd-shaped friend deep in his work, and does what he does best: be a messy distraction.
 
-  This silly short was inspired by [a wonderful commission](https://booru.badmanners.xyz/index.php?q=post/view/4) that I got from [Eli-Eternity](https://www.furaffinity.net/user/eli-eternity) and was supposed to go along with it, but this story doesn't even come close to doing it justice. Seriously, check out his piece if you haven't! It's turned out amazing, and he's done an incredible job, and I love it so much!
+  This silly short was inspired by <a href="https://booru.badmanners.xyz/index.php?q=post/view/4" target="_blank" data-age-restricted>a wonderful commission</a> that I got from [Eli-Eternity](https://www.furaffinity.net/user/eli-eternity) and was supposed to go along with it, but this story doesn't even come close to doing it justice. Seriously, check out his piece if you haven't! It's turned out amazing, and he's done an incredible job, and I love it so much!
 
   Why are you still reading this instead of clicking the link above?!
 posts:
diff --git a/src/content/stories/flavorful-favor.md b/src/content/stories/flavorful-favor.md
index cd5cf4b..3a890cf 100644
--- a/src/content/stories/flavorful-favor.md
+++ b/src/content/stories/flavorful-favor.md
@@ -9,7 +9,7 @@ thumbnail: /src/assets/thumbnails/bm_8_flavorful_favor.png
 description: |
   One, fearful and furry; one, formidable and feathery; and one, fired-up for the follow-up. A threesome fatefully forced into a fantastical face-to-face.
 
-  So many ideas that I want to write...! I just haven't found a lot of motivation to write as much as before. But I'm still working, and haven't gone anywhere. At least I finally managed to churn out this one, featuring Beetle, one of my OCs! You might recognize him from [one of the few things](https://booru.badmanners.xyz/index.php?q=post/view/3) I actually uploaded this last month. Quite a fun personality to play with, along with my sona Sam and another OC, Muno - someone might remember that last name from one of my previous stories. All of my characters featured in stories are fun really, with a few that I wanna revisit or new ones that I make for each new setting. Just wish I had a better writing rhythm to put all those ideas to life like I used to, though! Managing to finish this story is a start, I guess.
+  So many ideas that I want to write...! I just haven't found a lot of motivation to write as much as before. But I'm still working, and haven't gone anywhere. At least I finally managed to churn out this one, featuring Beetle, one of my OCs! You might recognize him from <a href="https://booru.badmanners.xyz/index.php?q=post/view/3" target="_blank" data-age-restricted>one of the few things</a> I actually uploaded this last month. Quite a fun personality to play with, along with my sona Sam and another OC, Muno - someone might remember that last name from one of my previous stories. All of my characters featured in stories are fun really, with a few that I wanna revisit or new ones that I make for each new setting. Just wish I had a better writing rhythm to put all those ideas to life like I used to, though! Managing to finish this story is a start, I guess.
 posts:
   eka: https://aryion.com/g4/view/884215
   furaffinity: https://www.furaffinity.net/view/51778522/
diff --git a/src/content/stories/part-of-the-show.md b/src/content/stories/part-of-the-show.md
index 8fc4a50..557fb47 100644
--- a/src/content/stories/part-of-the-show.md
+++ b/src/content/stories/part-of-the-show.md
@@ -9,7 +9,7 @@ thumbnail: /src/assets/thumbnails/bm_ff_14_part_of_the_show.png
 description: |
   You attend a show, unaware of how personal it can turn out...
 
-  This is a story based off of a YCH that I got from [Helkumurrr](https://www.furaffinity.net/user/helkumurrr) which [you should definitely check out](https://booru.badmanners.xyz/index.php?q=post/view/5). First piece with myself as pred, to complement my previous commission where I was prey – and also with a short (and hopefully fun) story to go along with it!
+  This is a story based off of a YCH that I got from [Helkumurrr](https://www.furaffinity.net/user/helkumurrr) which <a href="https://booru.badmanners.xyz/index.php?q=post/view/5" target="_blank" data-age-restricted>you should definitely check out</a>. First piece with myself as pred, to complement my previous commission where I was prey – and also with a short (and hopefully fun) story to go along with it!
 posts:
   eka: https://aryion.com/g4/view/902197
   furaffinity: https://www.furaffinity.net/view/52509501/
diff --git a/src/content/tag-categories/1-types-of-vore.yaml b/src/content/tag-categories/1-types-of-vore.yaml
index ce24be4..950cf56 100644
--- a/src/content/tag-categories/1-types-of-vore.yaml
+++ b/src/content/tag-categories/1-types-of-vore.yaml
@@ -2,20 +2,20 @@ 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 through their mouth.
+    description: Scenarios where prey are consumed by the predator's mouth.
   - name: anal vore
-    description: Scenarios where prey are consumed by the predator through their butt/anus.
+    description: Scenarios where prey are consumed by the predator's butt/anus.
   - name: cock vore
-    description: Scenarios where prey are consumed by the predator through their penis.
+    description: Scenarios where prey are consumed by the predator's penis.
   - name: unbirth
-    description: Scenarios where prey are consumed by the predator through their vagina/vulva, sometimes also called "vaginal vore".
+    description: Scenarios where prey are consumed by the predator's vagina/vulva, sometimes also called "vaginal vore".
   - name: tail vore
-    description: Scenarios where prey are consumed by the predator through an opening or mouth in their tail.
+    description: Scenarios where prey are consumed by the predator's tail, through an opening or mouth on the appendage.
   - name: slit vore
-    description: Scenarios where prey are consumed by the predator through a genital slit.
+    description: Scenarios where prey are consumed by the predator's genital slit.
   - name: sheath vore
-    description: Scenarios where prey are consumed by the predator through a genital sheath, generally around their penis.
+    description: Scenarios where prey are consumed by the predator's genital sheath around their penis.
   - name: nipple vore
-    description: Scenarios where prey are consumed by the predator through their nipple/breast.
+    description: Scenarios where prey are consumed by the predator's breast, through the nipples or cleavage.
   - name: chest maw vore
-    description: Scenarios where prey are consumed by the predator through a mouth/maw on their chest cavity.
+    description: Scenarios where prey are consumed by the predator's chest, through a maw on their chest cavity.
diff --git a/src/content/tag-categories/10-recurring-characters.yaml b/src/content/tag-categories/10-recurring-characters.yaml
index ad2f9dc..5084fe9 100644
--- a/src/content/tag-categories/10-recurring-characters.yaml
+++ b/src/content/tag-categories/10-recurring-characters.yaml
@@ -2,8 +2,8 @@ name: Recurring characters
 index: 10
 tags:
   - name: Sam Brendan
-    description: Content that includes my character and fursona [Sam, the mimic x maned wolf hybrid](https://badmanners.xyz/sam_brendan).
+    description: Content that includes my character and fursona <a href="https://badmanners.xyz/sam_brendan" target="_blank" data-age-restricted>Sam, the mimic x maned wolf hybrid</a>.
   - name: Beetle
-    description: Content that includes my character [Beetle, the gryphon](https://booru.badmanners.xyz/index.php?q=post/view/3).
+    description: Content that includes my character <a href="https://booru.badmanners.xyz/index.php?q=post/view/3" target="_blank" data-age-restricted>Beetle, the gryphon</a>.
   - name: Muno
     description: Content that includes my character Muno, the snake.
diff --git a/src/layouts/GalleryLayout.astro b/src/layouts/GalleryLayout.astro
index e7e14c9..b665dab 100644
--- a/src/layouts/GalleryLayout.astro
+++ b/src/layouts/GalleryLayout.astro
@@ -1,13 +1,17 @@
 ---
 import { getImage } from "astro:assets";
 import BaseLayout from "./BaseLayout.astro";
-import Navigation from "../components/Navigation.astro";
 import logoBM from "../assets/images/logo_bm.png";
 import { t } from "../i18n";
 import IconHome from "../components/icons/IconHome.astro";
+import IconBriefcase from "../components/icons/IconBriefcase.astro";
 import IconSquareRSS from "../components/icons/IconSquareRSS.astro";
 import IconSun from "../components/icons/IconSun.astro";
 import IconMoon from "../components/icons/IconMoon.astro";
+import IconMagnifyingGlass from "../components/icons/IconMagnifyingGlass.astro";
+import IconTags from "../components/icons/IconTags.astro";
+import IconGamepad from "../components/icons/IconGamepad.astro";
+import IconBook from "../components/icons/IconBook.astro";
 
 type Props = {
   pageTitle: string;
@@ -18,6 +22,8 @@ type Props = {
 const { pageTitle, enablePagefind, class: className } = Astro.props;
 const logo = await getImage({ src: logoBM, width: 192 });
 const currentYear = new Date().getFullYear().toString();
+const isCurrentRoute = (path: string) =>
+  Astro.url.pathname === path || (path !== "/" && Astro.url.pathname === `${path}/`);
 ---
 
 <BaseLayout pageTitle={pageTitle}>
@@ -31,8 +37,8 @@ const currentYear = new Date().getFullYear().toString();
   <div
     class="flex min-h-screen flex-col bg-stone-200 text-stone-800 md:flex-row dark:bg-stone-800 dark:text-stone-200 print:bg-none"
   >
-    <div
-      class="h-card static mb-4 flex flex-col items-center bg-bm-300 pt-10 text-center text-stone-900 shadow-xl md:fixed md:inset-y-0 md:left-0 md:mb-0 md:w-60 md:pt-20 dark:bg-green-900 dark:text-stone-100 print:bg-none print:shadow-none"
+    <nav
+      class="h-card static mb-4 flex flex-col items-center bg-bm-300 pt-10 pb-10 text-center text-stone-900 shadow-xl md:fixed md:inset-y-0 md:left-0 md:mb-0 md:w-60 md:pt-20 dark:bg-green-900 dark:text-stone-100 print:bg-none print:shadow-none"
     >
       <img
         loading="eager"
@@ -42,7 +48,90 @@ const currentYear = new Date().getFullYear().toString();
         width={192}
       />
       <span class="p-name my-2 text-2xl font-semibold">Bad Manners</span>
-      <Navigation />
+      <ul class="flex flex-col gap-y-2">
+        <li>
+          <a
+            class="u-url group text-link"
+            href="https://badmanners.xyz/"
+            data-age-restricted
+            rel="me"
+          >
+            <IconHome width="1.25rem" height="1.25rem" class="inline align-middle" />
+            <span class="group-focus:underline group-hover:underline">Main website</span>
+          </a>
+        </li>
+        <li>
+          <a
+            class="u-url group text-link"
+            href="/"
+            aria-current={isCurrentRoute("/") ? "page" : undefined}
+          >
+            <IconBriefcase width="1.25rem" height="1.25rem" class="inline align-middle" />
+            <span class="group-focus:underline group-hover:underline">Gallery</span>
+          </a>
+        </li>
+        <li>
+          <a
+            class="u-url group text-link"
+            href="/stories/1"
+            aria-current={isCurrentRoute("/stories/1") ? "page" : undefined}
+          >
+            <IconBook width="1.25rem" height="1.25rem" class="inline align-middle" />
+            <span class="group-focus:underline group-hover:underline">Stories</span>
+          </a>
+        </li>
+        <li>
+          <a
+            class="u-url group text-link"
+            href="/games"
+            aria-current={isCurrentRoute("/games") ? "page" : undefined}
+          >
+            <IconGamepad width="1.25rem" height="1.25rem" class="inline align-middle" />
+            <span class="group-focus:underline group-hover:underline">Games</span>
+          </a>
+        </li>
+        <li>
+          <a
+            class="u-url group text-link"
+            href="/tags"
+            aria-current={isCurrentRoute("/tags") ? "page" : undefined}
+          >
+            <IconTags width="1.25rem" height="1.25rem" class="inline align-middle" />
+            <span class="group-focus:underline group-hover:underline">Tags</span>
+          </a>
+        </li>
+        <li>
+          <a
+            class="u-url group text-link"
+            href="/search"
+            rel="search"
+            aria-current={isCurrentRoute("/search") ? "page" : undefined}
+          >
+            <IconMagnifyingGlass width="1.25rem" height="1.25rem" class="inline align-middle" />
+            <span class="group-focus:underline group-hover:underline">Search</span>
+          </a>
+        </li>
+        <li>
+          <a
+            class="u-url group text-link"
+            href="/feed.xml"
+          >
+            <IconSquareRSS width="1.25rem" height="1.25rem" class="inline align-middle" />
+            <span class="group-focus:underline group-hover:underline">RSS feed</span>
+          </a>
+        </li>
+        <li>
+          <button
+            data-dark-mode
+            style={{ display: "none" }}
+            class="group text-link"
+          >
+            <IconSun width="1.25rem" height="1.25rem" class="align-middle hidden dark:inline" />
+            <IconMoon width="1.25rem" height="1.25rem" class="inline align-middle dark:hidden" />
+            <span class="group-focus:underline group-hover:underline">{t("en", "published_content/toggle_dark_mode")}</span>
+          </a>
+        </li>
+      </ul>
       <div class="pt-4 text-center text-xs text-black dark:text-white">
         <span
           >&copy; {
@@ -55,29 +144,9 @@ const currentYear = new Date().getFullYear().toString();
             )
           } |
         </span>
-        <a class="hover:underline focus:underline" href="/licenses.txt" target="_blank">Licenses</a>
+        <a class="text-link hover:underline focus:underline" href="/licenses.txt" rel="license">Licenses</a>
       </div>
-      <div class="mt-2 flex items-center gap-x-1 pb-10">
-        <a class="u-url text-link p-1" href="https://badmanners.xyz/" target="_blank" aria-labelledby="label-main-website">
-          <IconHome width="1.5rem" height="1.5rem" />
-          <span id="label-main-website" class="sr-only">Main website</span>
-        </a>
-        <a class="text-link p-1" href="/feed.xml" target="_blank" aria-labelledby="label-rss-feed">
-          <IconSquareRSS width="1.5rem" height="1.5rem" />
-          <span id="label-rss-feed" class="sr-only">RSS feed</span>
-        </a>
-        <button
-          data-dark-mode
-          style={{ display: "none" }}
-          class="text-link p-1"
-          aria-labelledby="label-toggle-dark-mode"
-        >
-          <IconSun width="1.5rem" height="1.5rem" class="hidden dark:block" />
-          <IconMoon width="1.5rem" height="1.5rem" class="block dark:hidden" />
-          <span id="label-toggle-dark-mode" class="sr-only">{t("en", "published_content/toggle_dark_mode")}</span>
-        </button>
-      </div>
-    </div>
+    </nav>
     <main
       class:list={[className, "ml-0 max-w-6xl px-2 pb-12 pt-4 md:ml-60 md:px-4 print:pb-0"]}
       data-pagefind-body={enablePagefind ? "" : undefined}
diff --git a/src/layouts/GameLayout.astro b/src/layouts/GameLayout.astro
index 9fbc074..1ff4082 100644
--- a/src/layouts/GameLayout.astro
+++ b/src/layouts/GameLayout.astro
@@ -54,7 +54,7 @@ const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !ga
   />
   <Fragment slot="section-information">
     <Authors lang={props.lang}>
-      {authorsList.map((author) => <UserComponent rel="author" class="p-author" user={author} lang={props.lang} />)}
+      {authorsList.map((author) => <UserComponent rel="author nofollow" class="p-author" user={author} lang={props.lang} />)}
     </Authors>
     <div id="platforms">
       <p>{t(props.lang, "game/platforms", props.platforms)}</p>
diff --git a/src/layouts/PublishedContentLayout.astro b/src/layouts/PublishedContentLayout.astro
index 5f3eef0..1a28200 100644
--- a/src/layouts/PublishedContentLayout.astro
+++ b/src/layouts/PublishedContentLayout.astro
@@ -165,6 +165,7 @@ const thumbnail =
               {props.prev ? (
                 <a
                   href={props.prev.link}
+                  rel="prev"
                   class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
                   aria-label={props.labelPreviousContent}
                 >
@@ -177,6 +178,7 @@ const thumbnail =
               {props.next ? (
                 <a
                   href={props.next.link}
+                  rel="next"
                   class="text-link flex items-center justify-center px-1 py-3 font-light underline"
                   aria-label={props.labelNextContent}
                 >
@@ -299,6 +301,7 @@ const thumbnail =
               {props.prev ? (
                 <a
                   href={props.prev.link}
+                  rel="prev"
                   class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
                   aria-label={props.labelPreviousContent}
                 >
@@ -311,6 +314,7 @@ const thumbnail =
               {props.next ? (
                 <a
                   href={props.next.link}
+                  rel="next"
                   class="text-link flex items-center justify-center px-1 py-3 font-light underline"
                   aria-label={props.labelNextContent}
                 >
@@ -391,7 +395,7 @@ const thumbnail =
       <span
         set:html={t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())}
       /><span>&nbsp;|</span>
-      <a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
+      <a class="hover:underline focus:underline" href="/licenses.txt" rel="license"
         >{t(props.lang, "published_content/licenses")}</a
       >
     </div>
diff --git a/src/layouts/StoryLayout.astro b/src/layouts/StoryLayout.astro
index e7875d3..46913cf 100644
--- a/src/layouts/StoryLayout.astro
+++ b/src/layouts/StoryLayout.astro
@@ -71,7 +71,7 @@ const wordCount = props.wordCount?.toString();
       requestersList && (
         <Requesters lang={props.lang}>
           {requestersList.map((requester) => (
-            <UserComponent class="p-name" user={requester} lang={props.lang} />
+            <UserComponent user={requester} lang={props.lang} />
           ))}
         </Requesters>
       )
@@ -80,7 +80,7 @@ const wordCount = props.wordCount?.toString();
       commissionersList && (
         <Commissioners lang={props.lang}>
           {commissionersList.map((commissioner) => (
-            <UserComponent class="p-name" user={commissioner} lang={props.lang} />
+            <UserComponent user={commissioner} lang={props.lang} />
           ))}
         </Commissioners>
       )
diff --git a/src/pages/[...config].ts b/src/pages/[...config].ts
index b470f7c..772ea01 100644
--- a/src/pages/[...config].ts
+++ b/src/pages/[...config].ts
@@ -1,9 +1,9 @@
 import type { APIRoute, GetStaticPaths } from "astro";
 import { APACHE_CONFIG } from "astro:env/server";
 
-const htaccess = `
+const htaccess = String.raw`
 ErrorDocument 404 /404.html
-RedirectMatch 301 ^/stories(\/(index.html)?)?$ /stories/1/
+RedirectMatch 301 ^/stories(/(index.html)?)?$ /stories/1/
 Redirect 301 /story/ /stories/
 Redirect 301 /game/ /games/
 `.trim();
diff --git a/src/pages/api/export-story/[...slug].ts b/src/pages/api/export-story/[...slug].ts
index b2982d2..53883dd 100644
--- a/src/pages/api/export-story/[...slug].ts
+++ b/src/pages/api/export-story/[...slug].ts
@@ -90,9 +90,9 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
         }, Promise.resolve(""));
         switch (exportFormat) {
           case "bbcode":
-            return { exportWebsite, description: markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n") };
+            return { descriptionFilename: `description_${exportWebsite}.txt`, descriptionText: markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n") };
           case "markdown":
-            return { exportWebsite, description: storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim() };
+            return { descriptionFilename: `description_${exportWebsite}.md`, descriptionText: storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim() };
           default:
             const unknown: never = exportFormat;
             throw new Error(`Unknown export format "${unknown}"`);
@@ -131,10 +131,10 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
         story: storyText,
         description: description.reduce(
           (acc, item) => {
-            acc[item.exportWebsite] = item.description;
+            acc[item.descriptionFilename] = item.descriptionText;
             return acc;
           },
-          {} as Record<PostWebsite, string>,
+          {} as Record<string, string>,
         ),
         thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null,
       }),
diff --git a/src/pages/index.astro b/src/pages/index.astro
index a95ccde..09fbaff 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -90,7 +90,9 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
       For more information about me, please check out <a
         class="text-link underline"
         href="https://badmanners.xyz/"
-        target="_blank">my main website</a
+        data-age-restricted
+        rel="me"
+        >my main website</a
       >.
     </p>
   </div>
diff --git a/src/pages/stories/[page].astro b/src/pages/stories/[page].astro
index 5038cea..d30463a 100644
--- a/src/pages/stories/[page].astro
+++ b/src/pages/stories/[page].astro
@@ -44,7 +44,7 @@ const totalPages = Math.ceil(page.total / page.size);
   <div class="mx-auto mb-6 mt-2 flex w-fit rounded-lg border border-stone-400 dark:border-stone-500">
     {
       page.url.prev && (
-        <a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" href={page.url.prev}>
+        <a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" rel="prev" href={page.url.prev}>
           Previous page
         </a>
       )
@@ -69,7 +69,7 @@ const totalPages = Math.ceil(page.total / page.size);
     }
     {
       page.url.next && (
-        <a class="text-link px-2 py-1 underline" href={page.url.next}>
+        <a class="text-link px-2 py-1 underline" rel="next" href={page.url.next}>
           Next page
         </a>
       )
@@ -129,7 +129,7 @@ const totalPages = Math.ceil(page.total / page.size);
   <div class="mx-auto my-6 flex w-fit rounded-lg border border-stone-400 dark:border-stone-500">
     {
       page.url.prev && (
-        <a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" href={page.url.prev}>
+        <a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" rel="prev" href={page.url.prev}>
           Previous page
         </a>
       )
@@ -154,7 +154,7 @@ const totalPages = Math.ceil(page.total / page.size);
     }
     {
       page.url.next && (
-        <a class="text-link px-2 py-1 underline" href={page.url.next}>
+        <a class="text-link px-2 py-1 underline" rel="next" href={page.url.next}>
           Next page
         </a>
       )
diff --git a/src/utils/markdown_to_bbcode.ts b/src/utils/markdown_to_bbcode.ts
index 588ac37..2988b05 100644
--- a/src/utils/markdown_to_bbcode.ts
+++ b/src/utils/markdown_to_bbcode.ts
@@ -8,8 +8,19 @@ const renderer: RendererObject = {
   blockquote({ tokens }) {
     return `\n[quote]${this.parser.parseInline(tokens)}[/quote]\n`;
   },
-  html() {
-    throw new Error("Not supported by BBCode: html");
+  html(token) {
+    const { type, raw } = token;
+    if (type === "html") {
+      if (raw === "</a>") {
+        return "[/url]";
+      }
+      const match = raw.match(/^<a(?:\w+[a-z-]+(?:="[^"]*"))*\w+href="([^"]+)"(?:\w+[a-z-]+(?:="[^"]*"))*\w*>$/);
+      if (match&&match[1]) {
+        return `[url=${match[1]}]`;
+      }
+    }
+    console.error("ERROR: unhandled token markdownToBbcode.html", token);
+    throw new Error("Unknown HTML token not supported by BBCode renderer");
   },
   heading({ tokens }) {
     return `\n${this.parser.parseInline(tokens)}\n`;
@@ -24,7 +35,8 @@ const renderer: RendererObject = {
   listitem({ tokens }) {
     return `[li]${this.parser.parseInline(tokens)}[/li]\n`;
   },
-  checkbox() {
+  checkbox(token) {
+    console.error("ERROR: unhandled token markdownToBbcode.checkbox", token);
     throw new Error("Not supported by BBCode: checkbox");
   },
   paragraph({ tokens }) {
diff --git a/src/utils/markdown_to_plaintext.ts b/src/utils/markdown_to_plaintext.ts
index 76cabae..a0e77ed 100644
--- a/src/utils/markdown_to_plaintext.ts
+++ b/src/utils/markdown_to_plaintext.ts
@@ -9,7 +9,7 @@ const renderer: RendererObject = {
     return `\n> ${this.parser.parseInline(tokens)}\n`;
   },
   html() {
-    throw new Error("Not supported by plaintext: html");
+    return "";
   },
   heading({ tokens }) {
     return `\n== ${this.parser.parseInline(tokens)} ==\n`;
@@ -23,8 +23,9 @@ const renderer: RendererObject = {
   listitem({ tokens }) {
     return `- ${this.parser.parseInline(tokens)}\n`;
   },
-  checkbox() {
-    throw new Error("Not supported by plaintext: checkbox");
+  checkbox(token) {
+    console.error("ERROR: unhandled token markdownToPlaintext.checkbox", token);
+    throw new Error("Not supported by plaintext renderer: checkbox");
   },
   paragraph({ tokens }) {
     return `\n${this.parser.parseInline(tokens)}\n`;
@@ -52,14 +53,16 @@ const renderer: RendererObject = {
   br() {
     return "\n\n";
   },
-  del() {
-    throw new Error("Not supported by plaintext: del");
+  del(token) {
+    console.error("ERROR: unhandled token markdownToPlaintext.del", token);
+    throw new Error("Not supported by plaintext renderer: del");
   },
   link({ tokens }) {
     return this.parser.parseInline(tokens);
   },
-  image() {
-    throw new Error("Not supported by plaintext: img");
+  image(token) {
+    console.error("ERROR: unhandled token markdownToPlaintext.image", token);
+    throw new Error("Not supported by plaintext renderer: img");
   },
 };