From 7bb8a952ef1bc2ab64feb5f645010c74c9ad66a6 Mon Sep 17 00:00:00 2001
From: Bad Manners <me@badmanners.xyz>
Date: Wed, 7 Aug 2024 19:25:50 -0300
Subject: [PATCH] Several minor improvements to typing and misc.

- Improved schema validation
- Move username parsing and other validators to schema types
- Fix astro check command
- Add JSON/YAML schema validation for data collections
- Update licenses
- Remove deployment script in favor of rsync
- Prevent unsanitized input in export-story script
- Change "eng" language to "en", per BCP47
- Clean up i18n keys and add aria attributes
- Improve MastodonComments behavior on no-JS browsers
---
 .vscode/extensions.json                       |   7 +-
 .vscode/settings.json                         |  19 +
 LICENSE                                       |  21 -
 LICENSE.md                                    |   1 +
 README.md                                     |  19 +-
 astro.config.mjs                              |   1 +
 examples/game.md                              |   2 +-
 examples/story.md                             |   2 +-
 examples/user.yaml                            |   2 +-
 package-lock.json                             | 307 ++++++-------
 package.json                                  |  17 +-
 .../licenses.txt.ts => public/licenses.txt    |  18 +-
 scripts/deploy-lftp.ts                        |  62 ---
 scripts/export-story.ts                       |  11 +-
 .../thumbnails/bm_20_playing_it_safe.png      | Bin 22894 -> 20821 bytes
 src/components/AgeRestrictedModal.astro       |  10 +-
 src/components/Authors.astro                  |  12 +-
 src/components/Commissioners.astro            |  12 +-
 src/components/CopyrightedCharacters.astro    |   6 +-
 src/components/DarkModeScript.astro           |   6 +-
 src/components/MastodonComments.astro         | 271 +++++++-----
 src/components/Requesters.astro               |  12 +-
 src/components/UserComponent.astro            |  11 +-
 src/content/LICENSE                           |   1 -
 src/content/config.ts                         | 413 +++++++++++-------
 src/content/games/crossing-over.md            |   1 +
 src/content/stories/playing-it-safe.md        |   1 -
 src/content/stories/tiny-accident.md          |   1 +
 .../tag-categories/1-types-of-vore.yaml       |   2 +-
 src/content/tag-categories/2-body-types.yaml  |   4 +-
 src/content/tag-categories/3-genders.yaml     |   4 +-
 src/content/tag-categories/5-willingness.yaml |   4 +-
 .../6-vore-related-scenarios.yaml             |   2 +-
 .../tag-categories/9-type-of-content.yaml     |   2 +-
 src/content/users/anonymous.yaml              |   5 +-
 src/content/users/asof-yeun.yaml              |   2 +-
 src/content/users/bad-manners.yaml            |   9 +-
 src/content/users/hans-woofington.yaml        |   4 +-
 src/content/users/yolkmonkey.yaml             |   4 +-
 src/i18n/index.ts                             | 209 +++++----
 src/layouts/BaseLayout.astro                  |   7 +-
 src/layouts/GameLayout.astro                  |  96 ++--
 src/layouts/StoryLayout.astro                 | 102 +++--
 src/pages/api/export-story/[...slug].ts       | 115 ++---
 src/pages/api/healthcheck.ts                  |   8 +-
 src/pages/feed.xml.ts                         |   3 +-
 src/pages/games/[...slug].astro               |  11 -
 src/pages/index.astro                         |   2 +-
 src/pages/stories/[...slug].astro             |  16 +-
 src/pages/tags.astro                          |  58 +--
 src/pages/tags/[slug].astro                   |  30 +-
 src/utils/get_username_for_lang.ts            |  13 +-
 src/utils/is_anonymous_user.ts                |   6 +-
 tsconfig.json                                 |   3 +-
 54 files changed, 1005 insertions(+), 962 deletions(-)
 delete mode 100644 LICENSE
 create mode 100644 LICENSE.md
 rename src/pages/licenses.txt.ts => public/licenses.txt (53%)
 delete mode 100644 scripts/deploy-lftp.ts
 delete mode 120000 src/content/LICENSE

diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 82e5582..966bf65 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,3 +1,8 @@
 {
-  "recommendations": ["astro-build.astro-vscode", "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"]
+  "recommendations": [
+    "astro-build.astro-vscode",
+    "bradlc.vscode-tailwindcss",
+    "esbenp.prettier-vscode",
+    "redhat.vscode-yaml"
+  ]
 }
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 43f71ef..57dc4ec 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,6 +2,25 @@
   "files.associations": {
     "*.css": "tailwindcss"
   },
+  "json.schemas": [
+    {
+      "fileMatch": ["/src/content/series/**"],
+      "url": "./.astro/collections/series.schema.json"
+    },
+    {
+      "fileMatch": ["/src/content/tag-categories/**"],
+      "url": "./.astro/collections/tag-categories.schema.json"
+    },
+    {
+      "fileMatch": ["/src/content/users/**"],
+      "url": "./.astro/collections/users.schema.json"
+    }
+  ],
+  "yaml.schemas": {
+    "./.astro/collections/series.schema.json": "/src/content/series/**",
+    "./.astro/collections/tag-categories.schema.json": "/src/content/tag-categories/**",
+    "./.astro/collections/users.schema.json": "/src/content/users/**"
+  },
   "[astro]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index ea120b0..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2024 Bad Manners
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..1d4c39d
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1 @@
+See [public/licenses.txt](public/licenses.txt)
diff --git a/README.md b/README.md
index 3fd7a82..eec5f18 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ Static website built in Astro + Typescript + TailwindCSS.
 ## Requirements
 
 - Node.js 20+
-- (optional) LFTP, for the remote deployment script.
+- (optional) rsync, for remote deployment.
 - (optional) LibreOffice, for the story export script.
 
 ## Development
@@ -31,7 +31,7 @@ npm run prettier  # Prettier formatting
 Requires `libreoffice` to be installed and in your path.
 
 ```bash
-npm run export-story -- --output-dir ~/Documents/TO_UPLOAD slug-for-story-to-export
+npm run export-story -- -o ~/Documents/TO_UPLOAD slug-for-story-to-export
 ```
 
 ### Build and deploy to remote
@@ -40,19 +40,8 @@ npm run export-story -- --output-dir ~/Documents/TO_UPLOAD slug-for-story-to-exp
 npm run build
 ```
 
-Then, if you're using LFTP:
-
-1. Create a new `.env` file at the root of the project with your credentials (SSH, SFTP, WebDav, etc.) if you haven't already:
-
-```env
-DEPLOY_LFTP_HOST=https://example-webdav-server.com
-DEPLOY_LFTP_USER=example_user
-DEPLOY_LFTP_PASSWORD=sup3r_s3cr3t_password
-DEPLOY_LFTP_TARGETFOLDER=sites/gallery.badmanners.xyz/
-```
-
-2. Run the deploy command:
+Then, after configuring the `gallerybm` host (or the name of your choosing) in `~/.ssh/config`:
 
 ```bash
-npm run deploy-lftp
+rsync --delete -acP dist/ gallerybm:/home/public
 ```
diff --git a/astro.config.mjs b/astro.config.mjs
index d5c6758..12c378e 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -19,6 +19,7 @@ export default defineConfig({
   build: {
     assets: "assets",
   },
+  outDir: "./dist",
   redirects: {
     "/stories": "/stories/1",
   },
diff --git a/examples/game.md b/examples/game.md
index b45edc0..7d2e131 100644
--- a/examples/game.md
+++ b/examples/game.md
@@ -17,7 +17,7 @@ tags: []
 # series: the-lost-of-the-marshes
 # relatedStories: []
 # relatedGames: []
-# lang: eng
+# lang: en
 ---
 
 The game content (i.e. embed) goes here.
diff --git a/examples/story.md b/examples/story.md
index e7a3bfa..aded0f7 100644
--- a/examples/story.md
+++ b/examples/story.md
@@ -25,7 +25,7 @@ tags: []
 #   Some funny summary
 # relatedStories: []
 # relatedGames: []
-# lang: eng
+# lang: en
 ---
 
 The story goes here.
diff --git a/examples/user.yaml b/examples/user.yaml
index ba7eadf..bae2e71 100644
--- a/examples/user.yaml
+++ b/examples/user.yaml
@@ -1,6 +1,6 @@
 name: Nameless User
 nameLang:
-  eng: Nameless
+  en: Nameless
   tok: jan Nenle pi nimi ala
 # avatar: /src/assets/images/logo_bm.png
 links:
diff --git a/package-lock.json b/package-lock.json
index 52d78bd..96648c9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,26 +1,26 @@
 {
   "name": "gallery-badmanners-xyz",
-  "version": "1.6.0",
+  "version": "1.6.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "gallery-badmanners-xyz",
-      "version": "1.6.0",
+      "version": "1.6.1",
       "dependencies": {
-        "@astrojs/check": "^0.8.2",
+        "@astrojs/check": "^0.9.2",
         "@astrojs/rss": "^4.0.7",
         "@astrojs/tailwind": "^5.1.0",
         "@astropub/md": "^1.0.0",
         "@tailwindcss/typography": "^0.5.13",
-        "astro": "^4.12.2",
+        "astro": "^4.13.1",
         "astro-pagefind": "^1.6.0",
         "github-slugger": "^2.0.0",
-        "marked": "^12.0.1",
+        "marked": "^12.0.2",
         "pagefind": "^1.1.0",
         "reading-time": "^1.5.0",
         "sanitize-html": "^2.13.0",
-        "tailwindcss": "^3.4.6",
+        "tailwindcss": "^3.4.7",
         "tiny-decode": "^0.1.3",
         "typescript": "^5.5.4"
       },
@@ -32,7 +32,7 @@
         "prettier": "^3.3.3",
         "prettier-plugin-astro": "^0.14.1",
         "prettier-plugin-tailwindcss": "^0.6.5",
-        "tsx": "^4.16.2"
+        "tsx": "^4.16.5"
       }
     },
     "../astro-pagefind/packages/astro-pagefind": {
@@ -83,12 +83,12 @@
       }
     },
     "node_modules/@astrojs/check": {
-      "version": "0.8.2",
-      "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.8.2.tgz",
-      "integrity": "sha512-L0V9dGb2PGvK9Mf3kby99Y+qm7EqxaC9tN1MVCvaqp/3pPPZBadR4XAySHipxXqQsxwJS25WQow8/1kMl1e25g==",
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.2.tgz",
+      "integrity": "sha512-6rWxtJTbd/ctdAlmla0CAvloGaai5IUTG0K21kctJHHGKJKnGH6Xana7m0zNOtHpVPEJi1SgC/TcsN+ltYt0Cg==",
       "license": "MIT",
       "dependencies": {
-        "@astrojs/language-server": "^2.12.1",
+        "@astrojs/language-server": "^2.13.2",
         "chokidar": "^3.5.3",
         "fast-glob": "^3.3.1",
         "kleur": "^4.1.5",
@@ -102,9 +102,9 @@
       }
     },
     "node_modules/@astrojs/compiler": {
-      "version": "2.9.2",
-      "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.9.2.tgz",
-      "integrity": "sha512-Vpu0Ffsj8SoV+N0DFHlxxOMKHwSC9059Xy/OlG1t6uFYSoJXxkBC2WyF6igO7x10V+8uJrhOxaXr3nA90kJXow==",
+      "version": "2.10.2",
+      "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.10.2.tgz",
+      "integrity": "sha512-bvH+v8AirwpRWCkYJEyWYdc5Cs/BjG2ZTxIJzttHilXgfKJAdW2496KsUQKzf5j2tOHtaHXKKn9hb9WZiBGpEg==",
       "license": "MIT"
     },
     "node_modules/@astrojs/internal-helpers": {
@@ -114,12 +114,12 @@
       "license": "MIT"
     },
     "node_modules/@astrojs/language-server": {
-      "version": "2.12.1",
-      "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.12.1.tgz",
-      "integrity": "sha512-CCibE6XwSmrZEKlPDr48LZJN7NWxOurOJK1yOzqZFMNV8Y6DIqF6s1e60gbNNHMZkthWYBNTPno4Ni/XyviinQ==",
+      "version": "2.13.2",
+      "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.13.2.tgz",
+      "integrity": "sha512-l435EZLKjaUO/6iewJ7xqd3eHf3zAosVWG4woILbxluQcianBoNPepnnqAg7uUriZUaC44ae5v0Q+AfB8UI64g==",
       "license": "MIT",
       "dependencies": {
-        "@astrojs/compiler": "^2.9.1",
+        "@astrojs/compiler": "^2.10.2",
         "@jridgewell/sourcemap-codec": "^1.4.15",
         "@volar/kit": "~2.4.0-alpha.15",
         "@volar/language-core": "~2.4.0-alpha.15",
@@ -256,30 +256,30 @@
       }
     },
     "node_modules/@babel/compat-data": {
-      "version": "7.24.9",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz",
-      "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==",
+      "version": "7.25.2",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz",
+      "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==",
       "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/core": {
-      "version": "7.24.9",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
-      "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
+      "version": "7.25.2",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
+      "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
       "license": "MIT",
       "dependencies": {
         "@ampproject/remapping": "^2.2.0",
         "@babel/code-frame": "^7.24.7",
-        "@babel/generator": "^7.24.9",
-        "@babel/helper-compilation-targets": "^7.24.8",
-        "@babel/helper-module-transforms": "^7.24.9",
-        "@babel/helpers": "^7.24.8",
-        "@babel/parser": "^7.24.8",
-        "@babel/template": "^7.24.7",
-        "@babel/traverse": "^7.24.8",
-        "@babel/types": "^7.24.9",
+        "@babel/generator": "^7.25.0",
+        "@babel/helper-compilation-targets": "^7.25.2",
+        "@babel/helper-module-transforms": "^7.25.2",
+        "@babel/helpers": "^7.25.0",
+        "@babel/parser": "^7.25.0",
+        "@babel/template": "^7.25.0",
+        "@babel/traverse": "^7.25.2",
+        "@babel/types": "^7.25.2",
         "convert-source-map": "^2.0.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.2",
@@ -304,12 +304,12 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.24.10",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz",
-      "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==",
+      "version": "7.25.0",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
+      "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
       "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.24.9",
+        "@babel/types": "^7.25.0",
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.25",
         "jsesc": "^2.5.1"
@@ -331,12 +331,12 @@
       }
     },
     "node_modules/@babel/helper-compilation-targets": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz",
-      "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==",
+      "version": "7.25.2",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
+      "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
       "license": "MIT",
       "dependencies": {
-        "@babel/compat-data": "^7.24.8",
+        "@babel/compat-data": "^7.25.2",
         "@babel/helper-validator-option": "^7.24.8",
         "browserslist": "^4.23.1",
         "lru-cache": "^5.1.1",
@@ -355,43 +355,6 @@
         "semver": "bin/semver.js"
       }
     },
-    "node_modules/@babel/helper-environment-visitor": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz",
-      "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/types": "^7.24.7"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helper-function-name": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz",
-      "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/template": "^7.24.7",
-        "@babel/types": "^7.24.7"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helper-hoist-variables": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz",
-      "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/types": "^7.24.7"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
     "node_modules/@babel/helper-module-imports": {
       "version": "7.24.7",
       "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz",
@@ -406,16 +369,15 @@
       }
     },
     "node_modules/@babel/helper-module-transforms": {
-      "version": "7.24.9",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz",
-      "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==",
+      "version": "7.25.2",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz",
+      "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==",
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-environment-visitor": "^7.24.7",
         "@babel/helper-module-imports": "^7.24.7",
         "@babel/helper-simple-access": "^7.24.7",
-        "@babel/helper-split-export-declaration": "^7.24.7",
-        "@babel/helper-validator-identifier": "^7.24.7"
+        "@babel/helper-validator-identifier": "^7.24.7",
+        "@babel/traverse": "^7.25.2"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -446,18 +408,6 @@
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/helper-split-export-declaration": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz",
-      "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/types": "^7.24.7"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
     "node_modules/@babel/helper-string-parser": {
       "version": "7.24.8",
       "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
@@ -486,13 +436,13 @@
       }
     },
     "node_modules/@babel/helpers": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz",
-      "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==",
+      "version": "7.25.0",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz",
+      "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==",
       "license": "MIT",
       "dependencies": {
-        "@babel/template": "^7.24.7",
-        "@babel/types": "^7.24.8"
+        "@babel/template": "^7.25.0",
+        "@babel/types": "^7.25.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -514,10 +464,13 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz",
-      "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==",
+      "version": "7.25.3",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
+      "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
       "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.25.2"
+      },
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -541,16 +494,16 @@
       }
     },
     "node_modules/@babel/plugin-transform-react-jsx": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz",
-      "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==",
+      "version": "7.25.2",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz",
+      "integrity": "sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA==",
       "license": "MIT",
       "dependencies": {
         "@babel/helper-annotate-as-pure": "^7.24.7",
         "@babel/helper-module-imports": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7",
+        "@babel/helper-plugin-utils": "^7.24.8",
         "@babel/plugin-syntax-jsx": "^7.24.7",
-        "@babel/types": "^7.24.7"
+        "@babel/types": "^7.25.2"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -560,33 +513,30 @@
       }
     },
     "node_modules/@babel/template": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
-      "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
+      "version": "7.25.0",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz",
+      "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==",
       "license": "MIT",
       "dependencies": {
         "@babel/code-frame": "^7.24.7",
-        "@babel/parser": "^7.24.7",
-        "@babel/types": "^7.24.7"
+        "@babel/parser": "^7.25.0",
+        "@babel/types": "^7.25.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz",
-      "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==",
+      "version": "7.25.3",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
+      "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
       "license": "MIT",
       "dependencies": {
         "@babel/code-frame": "^7.24.7",
-        "@babel/generator": "^7.24.8",
-        "@babel/helper-environment-visitor": "^7.24.7",
-        "@babel/helper-function-name": "^7.24.7",
-        "@babel/helper-hoist-variables": "^7.24.7",
-        "@babel/helper-split-export-declaration": "^7.24.7",
-        "@babel/parser": "^7.24.8",
-        "@babel/types": "^7.24.8",
+        "@babel/generator": "^7.25.0",
+        "@babel/parser": "^7.25.3",
+        "@babel/template": "^7.25.0",
+        "@babel/types": "^7.25.2",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
@@ -595,9 +545,9 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.24.9",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz",
-      "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==",
+      "version": "7.25.2",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
+      "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
       "license": "MIT",
       "dependencies": {
         "@babel/helper-string-parser": "^7.24.8",
@@ -1582,9 +1532,10 @@
       }
     },
     "node_modules/@jridgewell/sourcemap-codec": {
-      "version": "1.4.15",
-      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
-      "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+      "license": "MIT"
     },
     "node_modules/@jridgewell/trace-mapping": {
       "version": "0.3.25",
@@ -1863,9 +1814,9 @@
       ]
     },
     "node_modules/@shikijs/core": {
-      "version": "1.11.1",
-      "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.11.1.tgz",
-      "integrity": "sha512-Qsn8h15SWgv5TDRoDmiHNzdQO2BxDe86Yq6vIHf5T0cCvmfmccJKIzHtep8bQO9HMBZYCtCBzaXdd1MnxZBPSg==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.12.1.tgz",
+      "integrity": "sha512-biCz/mnkMktImI6hMfMX3H9kOeqsInxWEyCHbSlL8C/2TR1FqfmGxTLRNwYCKsyCyxWLbB8rEqXRVZuyxuLFmA==",
       "license": "MIT",
       "dependencies": {
         "@types/hast": "^3.0.4"
@@ -2285,34 +2236,33 @@
       }
     },
     "node_modules/astro": {
-      "version": "4.12.2",
-      "resolved": "https://registry.npmjs.org/astro/-/astro-4.12.2.tgz",
-      "integrity": "sha512-l6OmqlL+FiuSi9x6F+EGZitteOznq1JffOil7st7cdqeMCTEIym4oagI1a6zp6QekliKWEEZWdplGhgh1k1f7Q==",
+      "version": "4.13.1",
+      "resolved": "https://registry.npmjs.org/astro/-/astro-4.13.1.tgz",
+      "integrity": "sha512-VnMjAc+ykFsIVjgbu9Mt/EA1fMIcPMXbU89h3ATwGOzSIKDsQH72bDgfJkWiwk6u0OE90GeP5EPhAT28Twf9oA==",
       "license": "MIT",
       "dependencies": {
-        "@astrojs/compiler": "^2.9.0",
+        "@astrojs/compiler": "^2.10.0",
         "@astrojs/internal-helpers": "0.4.1",
         "@astrojs/markdown-remark": "5.2.0",
         "@astrojs/telemetry": "3.1.0",
-        "@babel/core": "^7.24.9",
-        "@babel/generator": "^7.24.10",
-        "@babel/parser": "^7.24.8",
-        "@babel/plugin-transform-react-jsx": "^7.24.7",
-        "@babel/traverse": "^7.24.8",
-        "@babel/types": "^7.24.9",
+        "@babel/core": "^7.25.2",
+        "@babel/generator": "^7.25.0",
+        "@babel/parser": "^7.25.3",
+        "@babel/plugin-transform-react-jsx": "^7.25.2",
+        "@babel/traverse": "^7.25.3",
+        "@babel/types": "^7.25.2",
         "@types/babel__core": "^7.20.5",
         "@types/cookie": "^0.6.0",
         "acorn": "^8.12.1",
         "aria-query": "^5.3.0",
         "axobject-query": "^4.1.0",
         "boxen": "7.1.1",
-        "chokidar": "^3.6.0",
         "ci-info": "^4.0.0",
         "clsx": "^2.1.1",
         "common-ancestor-path": "^1.0.1",
         "cookie": "^0.6.0",
         "cssesc": "^3.0.0",
-        "debug": "^4.3.5",
+        "debug": "^4.3.6",
         "deterministic-object-hash": "^2.0.2",
         "devalue": "^5.0.0",
         "diff": "^5.2.0",
@@ -2330,7 +2280,7 @@
         "http-cache-semantics": "^4.1.1",
         "js-yaml": "^4.1.0",
         "kleur": "^4.1.5",
-        "magic-string": "^0.30.10",
+        "magic-string": "^0.30.11",
         "mrmime": "^2.0.0",
         "ora": "^8.0.1",
         "p-limit": "^6.1.0",
@@ -2339,19 +2289,19 @@
         "preferred-pm": "^4.0.0",
         "prompts": "^2.4.2",
         "rehype": "^13.0.1",
-        "semver": "^7.6.2",
-        "shiki": "^1.10.3",
+        "semver": "^7.6.3",
+        "shiki": "^1.12.0",
         "string-width": "^7.2.0",
         "strip-ansi": "^7.1.0",
         "tsconfck": "^3.1.1",
         "unist-util-visit": "^5.0.0",
         "vfile": "^6.0.2",
-        "vite": "^5.3.4",
+        "vite": "^5.3.5",
         "vitefu": "^0.2.5",
         "which-pm": "^3.0.0",
         "yargs-parser": "^21.1.1",
         "zod": "^3.23.8",
-        "zod-to-json-schema": "^3.23.1"
+        "zod-to-json-schema": "^3.23.2"
       },
       "bin": {
         "astro": "astro.js"
@@ -2958,9 +2908,9 @@
       }
     },
     "node_modules/debug": {
-      "version": "4.3.5",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
-      "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+      "version": "4.3.6",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
+      "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
       "license": "MIT",
       "dependencies": {
         "ms": "2.1.2"
@@ -4270,12 +4220,12 @@
       }
     },
     "node_modules/magic-string": {
-      "version": "0.30.10",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
-      "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
+      "version": "0.30.11",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
+      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
       "license": "MIT",
       "dependencies": {
-        "@jridgewell/sourcemap-codec": "^1.4.15"
+        "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
     "node_modules/make-error": {
@@ -4295,9 +4245,10 @@
       }
     },
     "node_modules/marked": {
-      "version": "12.0.1",
-      "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.1.tgz",
-      "integrity": "sha512-Y1/V2yafOcOdWQCX0XpAKXzDakPOpn6U0YLxTJs3cww6VxOzZV1BTOOYWLvH3gX38cq+iLwljHHTnMtlDfg01Q==",
+      "version": "12.0.2",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
+      "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
+      "license": "MIT",
       "bin": {
         "marked": "bin/marked.js"
       },
@@ -6336,12 +6287,12 @@
       }
     },
     "node_modules/shiki": {
-      "version": "1.11.1",
-      "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.11.1.tgz",
-      "integrity": "sha512-VHD3Q0EBXaaa245jqayBe5zQyMQUdXBFjmGr9MpDaDpAKRMYn7Ff00DM5MLk26UyKjnml3yQ0O2HNX7PtYVNFQ==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.12.1.tgz",
+      "integrity": "sha512-nwmjbHKnOYYAe1aaQyEBHvQymJgfm86ZSS7fT8OaPRr4sbAcBNz7PbfAikMEFSDQ6se2j2zobkXvVKcBOm0ysg==",
       "license": "MIT",
       "dependencies": {
-        "@shikijs/core": "1.11.1",
+        "@shikijs/core": "1.12.1",
         "@types/hast": "^3.0.4"
       }
     },
@@ -6616,9 +6567,9 @@
       }
     },
     "node_modules/tailwindcss": {
-      "version": "3.4.6",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz",
-      "integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==",
+      "version": "3.4.7",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
+      "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
       "license": "MIT",
       "dependencies": {
         "@alloc/quick-lru": "^5.2.0",
@@ -6838,9 +6789,9 @@
       "optional": true
     },
     "node_modules/tsx": {
-      "version": "4.16.2",
-      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.2.tgz",
-      "integrity": "sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==",
+      "version": "4.16.5",
+      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.5.tgz",
+      "integrity": "sha512-ArsiAQHEW2iGaqZ8fTA1nX0a+lN5mNTyuGRRO6OW3H/Yno1y9/t1f9YOI1Cfoqz63VAthn++ZYcbDP7jPflc+A==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -7122,9 +7073,9 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.3.4",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz",
-      "integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==",
+      "version": "5.3.5",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
+      "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
       "license": "MIT",
       "dependencies": {
         "esbuild": "^0.21.3",
@@ -7363,9 +7314,9 @@
       }
     },
     "node_modules/vscode-languageserver-textdocument": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz",
-      "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==",
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
+      "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
       "license": "MIT"
     },
     "node_modules/vscode-languageserver-types": {
@@ -7716,9 +7667,9 @@
       }
     },
     "node_modules/zod-to-json-schema": {
-      "version": "3.23.1",
-      "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.1.tgz",
-      "integrity": "sha512-oT9INvydob1XV0v1d2IadrR74rLtDInLvDFfAa1CG0Pmg/vxATk7I2gSelfj271mbzeM4Da0uuDQE/Nkj3DWNw==",
+      "version": "3.23.2",
+      "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.2.tgz",
+      "integrity": "sha512-uSt90Gzc/tUfyNqxnjlfBs8W6WSGpNBv0rVsNxP/BVSMHMKGdthPYff4xtCHYloJGM0CFxFsb3NbC0eqPhfImw==",
       "license": "ISC",
       "peerDependencies": {
         "zod": "^3.23.3"
diff --git a/package.json b/package.json
index d3dbd83..58ef62a 100644
--- a/package.json
+++ b/package.json
@@ -1,33 +1,32 @@
 {
   "name": "gallery-badmanners-xyz",
   "type": "module",
-  "version": "1.6.0",
+  "version": "1.6.1",
   "scripts": {
     "dev": "astro dev",
     "start": "astro dev",
-    "build": "npm run check && astro build",
+    "build": "astro check && astro build",
     "preview": "astro preview",
     "sync": "astro sync",
-    "check": "astro check --minimumSeverity warning",
+    "check": "astro check",
     "astro": "astro",
     "prettier": "prettier --write .",
-    "deploy-lftp": "dotenv tsx scripts/deploy-lftp.ts --",
     "export-story": "tsx scripts/export-story.ts"
   },
   "dependencies": {
-    "@astrojs/check": "^0.8.2",
+    "@astrojs/check": "^0.9.2",
     "@astrojs/rss": "^4.0.7",
     "@astrojs/tailwind": "^5.1.0",
     "@astropub/md": "^1.0.0",
     "@tailwindcss/typography": "^0.5.13",
-    "astro": "^4.12.2",
+    "astro": "^4.13.1",
     "astro-pagefind": "^1.6.0",
     "github-slugger": "^2.0.0",
-    "marked": "^12.0.1",
+    "marked": "^12.0.2",
     "pagefind": "^1.1.0",
     "reading-time": "^1.5.0",
     "sanitize-html": "^2.13.0",
-    "tailwindcss": "^3.4.6",
+    "tailwindcss": "^3.4.7",
     "tiny-decode": "^0.1.3",
     "typescript": "^5.5.4"
   },
@@ -39,6 +38,6 @@
     "prettier": "^3.3.3",
     "prettier-plugin-astro": "^0.14.1",
     "prettier-plugin-tailwindcss": "^0.6.5",
-    "tsx": "^4.16.2"
+    "tsx": "^4.16.5"
   }
 }
diff --git a/src/pages/licenses.txt.ts b/public/licenses.txt
similarity index 53%
rename from src/pages/licenses.txt.ts
rename to public/licenses.txt
index 10010f1..3bf892e 100644
--- a/src/pages/licenses.txt.ts
+++ b/public/licenses.txt
@@ -1,19 +1,11 @@
-import type { APIRoute } from "astro";
+The source code of this website is licensed under the MIT License: https://opensource.org/license/mit
+
+The stories and games hosted on the website are copyrighted by me and licensed under CC-BY-NC-ND-4.0: https://creativecommons.org/licenses/by-nc-nd/4.0/
 
-const licenses = `
 The briefcase logo and any unattributed characters are copyrighted and trademarked by me.
 
-The source code of this website is licensed under the MIT License. The content hosted on it (i.e. stories, games, and respective thumbnails/images) is copyrighted by me and distributed under the CC BY-NC-ND 4.0 license.
+The Noto Sans and Noto Serif typefaces are copyrighted to the Noto Project Authors and distributed under the SIL Open Font License v1.1: https://opensource.org/license/ofl-1-1
 
-The Noto Sans and Noto Serif typefaces are copyrighted to the Noto Project Authors and distributed under the SIL Open Font License v1.1.
-
-The generic SVG icons were created by Font Awesome and are distributed under the CC-BY-4.0 license.
+The generic SVG icons were created by Font Awesome and are distributed under CC-BY-4.0: https://creativecommons.org/licenses/by/4.0/
 
 All third-party trademarks and attributed characters belong to their respective owners, and I'm not affiliated with any of them.
-`.trim();
-
-const headers = { "Content-Type": "text/plain; charset=utf-8" };
-
-export const GET: APIRoute = () => {
-  return new Response(licenses, { headers });
-};
diff --git a/scripts/deploy-lftp.ts b/scripts/deploy-lftp.ts
deleted file mode 100644
index d418de3..0000000
--- a/scripts/deploy-lftp.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { spawn } from "node:child_process";
-import { program, Option } from "commander";
-
-interface DeployLftpOptions {
-  host: string;
-  user: string;
-  password: string;
-  targetFolder: string;
-  sourceFolder: string;
-}
-
-async function deployLftp({ host, user, password, targetFolder, sourceFolder }: DeployLftpOptions) {
-  const process = spawn(
-    "lftp",
-    [
-      "-c",
-      [
-        `open -u ${user},${password} ${host}`,
-        `mirror --reverse --include-glob assets/* --delete --only-missing --no-perms --verbose ${sourceFolder} ${targetFolder}`,
-        `mirror --reverse --exclude-glob assets/* --delete                --no-perms --verbose ${sourceFolder} ${targetFolder}`,
-        `bye`,
-      ].join("\n"),
-    ],
-    {
-      stdio: "inherit",
-    },
-  );
-  await new Promise<void>((resolve, reject) => {
-    process.on("close", (code) => {
-      if (code === 0) {
-        resolve();
-      } else {
-        reject(`deploy-lftp failed with code ${code}`);
-      }
-    });
-  });
-}
-
-await program
-  .name("deploy-lftp")
-  .description("Deploy files to remote server with LFTP")
-  .addOption(
-    new Option("-h, --host <hostname>", "Hostname of the LFTP (i.e. WebDav, SCP, SFTP...) remote.").env(
-      "DEPLOY_LFTP_HOST",
-    ),
-  )
-  .addOption(new Option("-u, --user <username>", "Username portion of the LFTP credentials").env("DEPLOY_LFTP_USER"))
-  .addOption(
-    new Option("-p, --password <pass>", "Password portion of the LFTP credentials").env("DEPLOY_LFTP_PASSWORD"),
-  )
-  .addOption(
-    new Option("-t, --target-folder <remoteDir>", "Folder to mirror files to in the LFTP remote").env(
-      "DEPLOY_LFTP_TARGETFOLDER",
-    ),
-  )
-  .addOption(
-    new Option("-s, --source-folder <localDir>", "Folder to read files from in the local machine")
-      .env("DEPLOY_LFTP_SOURCEFOLDER")
-      .default("dist/"),
-  )
-  .action(deployLftp)
-  .parseAsync();
diff --git a/scripts/export-story.ts b/scripts/export-story.ts
index 8108a0a..178ce38 100644
--- a/scripts/export-story.ts
+++ b/scripts/export-story.ts
@@ -1,4 +1,4 @@
-import { type ChildProcess, exec, execSync } from "node:child_process";
+import { type ChildProcess, exec, spawnSync } from "node:child_process";
 import { readdir, mkdir, mkdtemp, writeFile, readFile, copyFile } from "node:fs/promises";
 import { tmpdir } from "node:os";
 import { join as pathJoin, normalize } from "node:path";
@@ -150,7 +150,13 @@ async function exportStory(slug: string, options: { outputDir: string }) {
   await writeFile(pathJoin(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*"));
   const tempDir = await mkdtemp(pathJoin(tmpdir(), "export-story-"));
   await writeFile(pathJoin(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
-  execSync(`libreoffice --convert-to "rtf:Rich Text Format" --outdir ${tempDir} ${pathJoin(tempDir, "temp.txt")}`);
+  spawnSync("libreoffice", [
+    "--convert-to",
+    "rtf:Rich Text Format",
+    "--outdir",
+    tempDir,
+    pathJoin(tempDir, "temp.txt"),
+  ]);
   const rtfText = await readFile(pathJoin(tempDir, "temp.rtf"), "utf-8");
   const rtfStyles = getRTFStyles(rtfText);
   await writeFile(
@@ -158,7 +164,6 @@ async function exportStory(slug: string, options: { outputDir: string }) {
     rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
   );
   console.log("Success!");
-  process.exit(0);
 }
 
 await program
diff --git a/src/assets/thumbnails/bm_20_playing_it_safe.png b/src/assets/thumbnails/bm_20_playing_it_safe.png
index 1dc1384e8477b07043ab641354b846fc0e41b71b..3f075b127fcb09af7f386c90916323594c5e6882 100755
GIT binary patch
literal 20821
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

literal 22894
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

diff --git a/src/components/AgeRestrictedModal.astro b/src/components/AgeRestrictedModal.astro
index 764f2c1..7edb7af 100644
--- a/src/components/AgeRestrictedModal.astro
+++ b/src/components/AgeRestrictedModal.astro
@@ -54,11 +54,13 @@
   (function () {
     if (localStorage.getItem("ageVerified") !== "true") {
       document.body.appendChild(
-        (document.getElementById("template-modal-age-restricted") as HTMLTemplateElement).content.cloneNode(true),
+        document
+          .querySelector<HTMLElementTagNameMap["template"]>("template#template-modal-age-restricted")!
+          .content.cloneNode(true),
       );
-      const modal = document.querySelector<HTMLDivElement>("body > #modal-age-restricted")!;
-      const rejectButton = modal.querySelector<HTMLButtonElement>("button[data-modal-reject]")!;
-      const acceptButton = modal.querySelector<HTMLButtonElement>("button[data-modal-accept]")!;
+      const modal = document.querySelector<HTMLElementTagNameMap["div"]>("body > div#modal-age-restricted")!;
+      const rejectButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")!;
+      const acceptButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-accept]")!;
       function onRejectButtonClick(e: MouseEvent) {
         e.preventDefault();
         location.href = "about:blank";
diff --git a/src/components/Authors.astro b/src/components/Authors.astro
index 7e74761..89a156c 100644
--- a/src/components/Authors.astro
+++ b/src/components/Authors.astro
@@ -1,5 +1,5 @@
 ---
-import { type Lang } from "../content/config";
+import type { Lang } from "../content/config";
 import { t } from "../i18n";
 
 type Props = {
@@ -12,4 +12,12 @@ const authors = Astro.slots.has("default")
   : [];
 ---
 
-{authors.length ? <p id="authors" set:html={t(lang, "story/authors", authors)} /> : null}
+{
+  authors.length ? (
+    <p
+      id="authors"
+      aria-label={t(lang, "story/authors_aria_label", authors)}
+      set:html={t(lang, "story/authors", authors)}
+    />
+  ) : null
+}
diff --git a/src/components/Commissioners.astro b/src/components/Commissioners.astro
index bd4400e..cb3f5f7 100644
--- a/src/components/Commissioners.astro
+++ b/src/components/Commissioners.astro
@@ -1,5 +1,5 @@
 ---
-import { type Lang } from "../content/config";
+import type { Lang } from "../content/config";
 import { t } from "../i18n";
 
 type Props = {
@@ -12,4 +12,12 @@ const commissioners = Astro.slots.has("default")
   : [];
 ---
 
-{commissioners.length ? <p id="commissioners" set:html={t(lang, "story/commissioned_by", commissioners)} /> : null}
+{
+  commissioners.length ? (
+    <p
+      id="commissioners"
+      aria-label={t(lang, "story/commissioners_aria_label", commissioners)}
+      set:html={t(lang, "story/commissioned_by", commissioners)}
+    />
+  ) : null
+}
diff --git a/src/components/CopyrightedCharacters.astro b/src/components/CopyrightedCharacters.astro
index e4f2936..94a5ff5 100644
--- a/src/components/CopyrightedCharacters.astro
+++ b/src/components/CopyrightedCharacters.astro
@@ -1,6 +1,6 @@
 ---
-import { type CollectionEntry } from "astro:content";
-import { type Lang } from "../content/config";
+import type { CollectionEntry } from "astro:content";
+import type { Lang } from "../content/config";
 import { t } from "../i18n";
 import UserComponent from "./UserComponent.astro";
 import CopyrightedCharactersItem from "./CopyrightedCharactersItem.astro";
@@ -15,7 +15,7 @@ const { copyrightedCharacters, lang } = Astro.props;
 
 {
   copyrightedCharacters ? (
-    <section id="copyrighted-characters">
+    <section id="copyrighted-characters" aria-label={t(lang, "characters/copyrighted_characters_aria_label")}>
       <ul>
         {copyrightedCharacters.map(([owner, characterList]) => (
           <CopyrightedCharactersItem
diff --git a/src/components/DarkModeScript.astro b/src/components/DarkModeScript.astro
index 3abd902..1bf3f22 100644
--- a/src/components/DarkModeScript.astro
+++ b/src/components/DarkModeScript.astro
@@ -6,12 +6,12 @@
 
 <script>
   (function () {
-    var colorScheme = localStorage.getItem("colorScheme");
+    let colorScheme = localStorage.getItem("colorScheme");
     if (colorScheme == null || colorScheme === "auto") {
       colorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
     }
-    document.querySelectorAll("button[data-dark-mode]").forEach(function (button) {
-      button.addEventListener("click", function (e) {
+    document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => {
+      button.addEventListener("click", (e) => {
         e.preventDefault();
         if (colorScheme === "dark") {
           colorScheme = "light";
diff --git a/src/components/MastodonComments.astro b/src/components/MastodonComments.astro
index a9eff55..19fb3c2 100644
--- a/src/components/MastodonComments.astro
+++ b/src/components/MastodonComments.astro
@@ -1,37 +1,47 @@
 ---
+import type { Lang } from "../content/config";
+
 type Props = {
-  instance?: string;
-  user?: string;
-  postId?: string;
+  lang: Lang;
+  link: string;
+  instance: string;
+  user: string;
+  postId: string;
 };
 
-const { instance, user, postId } = Astro.props;
+const { link, instance, user, postId } = Astro.props;
 ---
 
 <section
   id="comment-section"
-  class="hidden px-2 font-serif"
+  class="px-2 font-serif"
   aria-describedby="title-comment-section"
-  data-instance={instance || ""}
-  data-user={user || ""}
-  data-post-id={postId || ""}
+  data-link={link}
+  data-instance={instance}
+  data-user={user}
+  data-post-id={postId}
 >
   <h2 id="title-comment-section" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
     Comments
   </h2>
   <div class="text-stone-800 dark:text-stone-100" id="comments">
-    <button
-      class="mx-auto w-64 rounded-lg bg-bm-300 px-4 py-1 underline disabled:bg-bm-400 disabled:no-underline dark:bg-green-800 dark:disabled:bg-green-900"
-      id="load-comments-button"
-      data-load-comments
-    >
-      <span>Click to load comments</span>
-    </button>
+    <p class="my-1">
+      <a class="text-link underline" href={link} target="_blank">View comments on Mastodon</a>
+    </p>
   </div>
 </section>
 
-<template id="template-comments-loading">
-  <svg class="-mt-1 mr-1 inline h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24">
+<template id="template-button">
+  <button
+    class="mx-auto w-64 rounded-lg bg-bm-300 px-4 py-1 underline disabled:bg-bm-400 disabled:no-underline dark:bg-green-800 dark:disabled:bg-green-900"
+    id="load-comments-button"
+  >
+    <span>Click to load comments</span>
+  </button>
+</template>
+
+<template id="template-button-loading">
+  <svg class="-mt-1 mr-1 inline h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24" aria-hidden>
     <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
     <path
       class="opacity-100"
@@ -45,12 +55,16 @@ const { instance, user, postId } = Astro.props;
 <template id="template-comment-box">
   <div class="my-2 rounded-md border-2 border-stone-400 bg-stone-200 p-2 dark:border-stone-600 dark:bg-stone-800">
     <div class="ml-1">
-      <a data-author class="text-link flex items-center text-lg hover:underline focus:underline">
+      <a data-author class="text-link flex items-center text-lg hover:underline focus:underline" target="_blank">
         <img data-avatar class="mr-2 w-10 rounded-full border border-stone-400 dark:border-stone-600" />
         <span data-display-name></span>
       </a>
-      <a data-post-link class="text-link my-1 flex items-center text-sm font-light hover:underline focus:underline">
-        <span class="mr-1" data-publish-date></span>
+      <a
+        data-post-link
+        class="text-link my-1 flex items-center text-sm font-light hover:underline focus:underline"
+        target="_blank"
+      >
+        <span class="mr-1" data-publish-date aria-label="Publish date"></span>
       </a>
     </div>
     <div data-content class="prose-a:text-link prose-story prose my-1 dark:prose-invert prose-img:my-0"></div>
@@ -72,11 +86,18 @@ const { instance, user, postId } = Astro.props;
         </svg>
       </div>
     </div>
-    <div data-comment-thread class="-mb-2"></div>
+    <div data-comment-thread class="-mb-2" aria-hidden></div>
   </div>
 </template>
 
 <script>
+  interface Post {
+    link: string;
+    instance: string;
+    user: string;
+    postId: string;
+  }
+
   interface Emoji {
     shortcode: string;
     url: string;
@@ -104,115 +125,131 @@ const { instance, user, postId } = Astro.props;
     emojis: Emoji[];
   }
 
+  interface ApiResponse {
+    ancestors: Comment[];
+    descendants: Comment[];
+  }
+
   (function () {
-    const replaceEmojis = (text: string, emojis: Emoji[], imgClass: string) =>
-      emojis.reduce(
+    function replaceEmojis(text: string, emojis: Emoji[]) {
+      return emojis.reduce(
         (acc, emoji) =>
           acc.replaceAll(
             `:${emoji.shortcode}:`,
-            `<img class="${imgClass}" alt=":${emoji.shortcode}: emoji" src="${emoji.url}" />`,
+            `<img class="inline mx-[1px] w-5" alt=":${emoji.shortcode}: emoji" src="${emoji.url}" />`,
           ),
         text,
       );
+    }
 
-    const commentSection = document.querySelector<Element>("#comment-section")!;
-    const instance = commentSection.getAttribute("data-instance");
-    const user = commentSection.getAttribute("data-user");
-    const postId = commentSection.getAttribute("data-post-id");
-    if (instance && user && postId) {
-      commentSection.classList.remove("hidden");
-      commentSection.querySelector<HTMLButtonElement>("button[data-load-comments]")!.addEventListener("click", (e) => {
+    async function renderComments(section: Element, post: Post) {
+      const commentsDiv = section.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!;
+      try {
+        const response = await fetch(`https://${post.instance}/api/v1/statuses/${post.postId}/context`);
+        if (!response.ok) {
+          throw new Error(`Received error status ${response.status} - ${response.statusText}!`);
+        }
+        const data: ApiResponse = await response.json();
+
+        const commentsList: HTMLElement[] = [];
+        const commentMap: Record<string, number> = {};
+        const commentTemplate = document.querySelector<HTMLElementTagNameMap["template"]>(
+          "template#template-comment-box",
+        )!;
+        data.descendants.forEach((comment) => {
+          const commentBox = commentTemplate.content.cloneNode(true) as HTMLDivElement;
+
+          const commentBoxAuthor = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-author]")!;
+          commentBoxAuthor.href = comment.account.url;
+          const avatar = commentBoxAuthor.querySelector<HTMLElementTagNameMap["img"]>("img[data-avatar]")!;
+          avatar.src = comment.account.avatar;
+          avatar.alt = `Profile picture of ${comment.account.username}`;
+          const displayName = commentBoxAuthor.querySelector<HTMLElementTagNameMap["span"]>("span[data-display-name]")!;
+          displayName.innerHTML = replaceEmojis(comment.account.display_name, comment.account.emojis);
+
+          const commentBoxPostLink = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-post-link]")!;
+          commentBoxPostLink.href = comment.url;
+          const publishDate =
+            commentBoxPostLink.querySelector<HTMLElementTagNameMap["span"]>("span[data-publish-date]")!;
+          publishDate.innerText = new Date(Date.parse(comment.created_at)).toLocaleString("en-US", {
+            month: "short",
+            day: "numeric",
+            year: "numeric",
+            hour: "2-digit",
+            minute: "2-digit",
+          });
+
+          if (comment.edited_at) {
+            const edited = document.createElement("span");
+            edited.className = "italic";
+            edited.innerText = "(edited)";
+            commentBoxPostLink.appendChild(edited);
+          }
+
+          const commentBoxContent = commentBox.querySelector<HTMLElementTagNameMap["div"]>("div[data-content]")!;
+          commentBoxContent.innerHTML = replaceEmojis(comment.content, comment.emojis);
+
+          const commentBoxFavorites = commentBox.querySelector<HTMLElementTagNameMap["span"]>("span[data-favorites]")!;
+          commentBoxFavorites.innerText = comment.favourites_count.toString();
+
+          const commentBoxReblogs = commentBox.querySelector<HTMLElementTagNameMap["span"]>("span[data-reblogs]")!;
+          commentBoxReblogs.innerText = comment.reblogs_count.toString();
+
+          if (comment.in_reply_to_id === post.postId || !(comment.in_reply_to_id in commentMap)) {
+            commentMap[comment.id] = commentsList.length;
+            commentsList.push(commentBox);
+          } else {
+            const commentsIndex = commentMap[comment.in_reply_to_id];
+            commentMap[comment.id] = commentsIndex;
+            const parentThreadDiv =
+              commentsList[commentsIndex].querySelector<HTMLElementTagNameMap["div"]>("div[data-comment-thread]")!;
+            parentThreadDiv.removeAttribute("aria-hidden");
+            parentThreadDiv.setAttribute("aria-label", "Replies");
+            parentThreadDiv.appendChild(commentBox);
+          }
+        });
+        if (commentsList.length === 0) {
+          commentsDiv.innerHTML = `<p class="my-1">No comments yet. <a class="text-link underline" href="${post.link}" target="_blank">Be the first to join the conversation on Mastodon</a>.</p>`;
+        } else {
+          commentsDiv.innerHTML = `<p class="my-1">Join the conversation <a class="text-link underline" href="${post.link}" target="_blank">by replying on Mastodon</a>.</p>`;
+          commentsDiv.append(...commentsList);
+        }
+      } catch (e) {
+        commentsDiv.innerHTML = `<p class="my-1">Unable to load comments. Please try again later.</p>`;
+        console.error("Fetch Mastodon comments error", e);
+      }
+    }
+
+    function initCommentSection() {
+      const commentSection = document.querySelector<HTMLElementTagNameMap["section"]>("section#comment-section");
+      if (!commentSection) {
+        return;
+      }
+      const post = {
+        link: commentSection.dataset.link,
+        instance: commentSection.dataset.instance,
+        user: commentSection.dataset.user,
+        postId: commentSection.dataset.postId,
+      };
+      if (!post.link || !post.instance || !post.user || !post.postId) {
+        return;
+      }
+      const loadCommentsButton = document
+        .querySelector<HTMLElementTagNameMap["template"]>("template#template-button")!
+        .content.cloneNode(true) as HTMLButtonElement;
+      commentSection.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!.replaceChildren(loadCommentsButton);
+      loadCommentsButton.addEventListener("click", (e) => {
         e.preventDefault();
-        const loadCommentsButton = e.target as HTMLButtonElement;
         loadCommentsButton.setAttribute("disabled", "true");
         loadCommentsButton.replaceChildren(
-          (document.getElementById("template-comments-loading") as HTMLTemplateElement).content.cloneNode(true),
+          document
+            .querySelector<HTMLElementTagNameMap["template"]>("template#template-button-loading")!
+            .content.cloneNode(true),
         );
-        const renderComments = async () => {
-          try {
-            if (!instance || !user || !postId) {
-              throw new Error(
-                `Cannot fetch comments without all fields (instance=${instance}, user=${user}, post-id=${postId})`,
-              );
-            }
-            const response = await fetch(`https://${instance}/api/v1/statuses/${postId}/context`);
-            if (!response.ok) {
-              throw new Error(`Received error status ${response.status} - ${response.statusText}!`);
-            }
-            const data: { ancestors: Comment[]; descendants: Comment[] } = await response.json();
-
-            const commentsList: HTMLElement[] = [];
-            const commentMap: Record<string, number> = {};
-            const commentTemplate = document.getElementById("template-comment-box") as HTMLTemplateElement;
-            data.descendants.forEach((comment) => {
-              const commentBox = commentTemplate.content.cloneNode(true) as HTMLDivElement;
-
-              const commentBoxAuthor = commentBox.querySelector<HTMLAnchorElement>("[data-author]")!;
-              commentBoxAuthor.href = comment.account.url;
-              commentBoxAuthor.target = "_blank";
-              const avatar = commentBoxAuthor.querySelector<HTMLImageElement>("[data-avatar]")!;
-              avatar.src = comment.account.avatar;
-              avatar.alt = `Profile picture of ${comment.account.username}`;
-              const displayName = commentBoxAuthor.querySelector<HTMLSpanElement>("[data-display-name]")!;
-              displayName.innerHTML = replaceEmojis(
-                comment.account.display_name,
-                comment.account.emojis,
-                "inline mx-[1px] w-5",
-              );
-
-              const commentBoxPostLink = commentBox.querySelector<HTMLAnchorElement>("[data-post-link]")!;
-              commentBoxPostLink.href = comment.url;
-              commentBoxPostLink.target = "_blank";
-              const publishDate = commentBoxPostLink.querySelector<HTMLSpanElement>("[data-publish-date]")!;
-              publishDate.innerText = new Date(Date.parse(comment.created_at)).toLocaleString("en-US", {
-                month: "short",
-                day: "numeric",
-                year: "numeric",
-                hour: "2-digit",
-                minute: "2-digit",
-              });
-
-              if (comment.edited_at) {
-                const edited = document.createElement("span");
-                edited.className = "italic";
-                edited.innerText = "(edited)";
-                commentBoxPostLink.appendChild(edited);
-              }
-
-              const commentBoxContent = commentBox.querySelector<HTMLDivElement>("[data-content]")!;
-              commentBoxContent.innerHTML = replaceEmojis(comment.content, comment.emojis, "inline mx-[1px] w-5");
-
-              const commentBoxFavorites = commentBox.querySelector<HTMLSpanElement>("[data-favorites]")!;
-              commentBoxFavorites.innerText = comment.favourites_count.toString();
-
-              const commentBoxReblogs = commentBox.querySelector<HTMLSpanElement>("[data-reblogs]")!;
-              commentBoxReblogs.innerText = comment.reblogs_count.toString();
-
-              if (comment.in_reply_to_id === postId || !(comment.in_reply_to_id in commentMap)) {
-                commentMap[comment.id] = commentsList.length;
-                commentsList.push(commentBox);
-              } else {
-                const commentsIndex = commentMap[comment.in_reply_to_id];
-                commentMap[comment.id] = commentsIndex;
-                commentsList[commentsIndex]
-                  .querySelector<HTMLDivElement>("[data-comment-thread]")!
-                  .appendChild(commentBox);
-              }
-            });
-            const commentsDiv = commentSection.querySelector<HTMLDivElement>("#comments")!;
-            if (commentsList.length === 0) {
-              commentsDiv.innerHTML = `<p class="my-1">No comments yet. <a class="text-link underline" href="https://${instance}/@${user}/${postId}" target="_noblank">Be the first to join the conversation on Mastodon</a>.</p>`;
-            } else {
-              commentsDiv.innerHTML = `<p class="my-1">Join the conversation <a class="text-link underline" href="https://${instance}/@${user}/${postId}" target="_noblank">by replying on Mastodon</a>.</p>`;
-              commentsDiv.append(...commentsList);
-            }
-          } catch (e) {
-            loadCommentsButton.innerHTML = `<span>Unable to load comments.</span>`;
-            console.error("Fetch Mastodon comments error", e);
-          }
-        };
-        renderComments();
+        renderComments(commentSection, post as Post);
       });
     }
+
+    initCommentSection();
   })();
 </script>
diff --git a/src/components/Requesters.astro b/src/components/Requesters.astro
index 4725719..245af9b 100644
--- a/src/components/Requesters.astro
+++ b/src/components/Requesters.astro
@@ -1,5 +1,5 @@
 ---
-import { type Lang } from "../content/config";
+import type { Lang } from "../content/config";
 import { t } from "../i18n";
 
 type Props = {
@@ -12,4 +12,12 @@ const requesters = Astro.slots.has("default")
   : [];
 ---
 
-{requesters.length ? <p id="requesters" set:html={t(lang, "story/requested_by", requesters)} /> : null}
+{
+  requesters.length ? (
+    <p
+      id="requesters"
+      aria-label={t(lang, "story/requesters_aria_label", requesters)}
+      set:html={t(lang, "story/requested_by", requesters)}
+    />
+  ) : null
+}
diff --git a/src/components/UserComponent.astro b/src/components/UserComponent.astro
index 18ba37e..cacc42a 100644
--- a/src/components/UserComponent.astro
+++ b/src/components/UserComponent.astro
@@ -1,6 +1,6 @@
 ---
-import { type CollectionEntry } from "astro:content";
-import { type Lang } from "../content/config";
+import type { CollectionEntry } from "astro:content";
+import type { Lang } from "../content/config";
 import { getUsernameForLang } from "../utils/get_username_for_lang";
 
 type Props = {
@@ -12,12 +12,7 @@ let { user, lang } = Astro.props;
 const username = getUsernameForLang(user, lang);
 let link: string | null = null;
 if (user.data.preferredLink) {
-  const preferredLink = user.data.links[user.data.preferredLink]!;
-  if (typeof preferredLink === "string") {
-    link = preferredLink;
-  } else {
-    link = preferredLink[0];
-  }
+  link = user.data.links[user.data.preferredLink]!.link;
 }
 ---
 
diff --git a/src/content/LICENSE b/src/content/LICENSE
deleted file mode 120000
index 0b24ab4..0000000
--- a/src/content/LICENSE
+++ /dev/null
@@ -1 +0,0 @@
-../assets/LICENSE
\ No newline at end of file
diff --git a/src/content/config.ts b/src/content/config.ts
index 2ff83af..4a8ba48 100644
--- a/src/content/config.ts
+++ b/src/content/config.ts
@@ -2,89 +2,214 @@ import { defineCollection, reference, z } from "astro:content";
 
 // Constants
 
-export const WEBSITE_LIST = [
-  "website",
-  "eka",
-  "furaffinity",
-  "weasyl",
-  "inkbunny",
-  "sofurry",
-  "mastodon",
-  "twitter",
-  "bluesky",
-  "itaku",
-] as const;
-export const GAME_PLATFORMS = ["web", "windows", "linux", "macos", "android", "ios"] as const;
-export const DEFAULT_LANG = "eng";
-export const DEFAULT_AUTHOR_ID = "bad-manners";
+export const DEFAULT_LANG = "en";
 export const ANONYMOUS_USER_ID = "anonymous";
 
-// Validators
-
-const ekaPostUrlRegex = /^(?:https:\/\/)(?:www\.)?aryion\.com\/g4\/view\/([1-9]\d*)\/?$/;
-const furaffinityPostUrlRegex = /^(?:https:\/\/)(?:www\.)?furaffinity\.net\/view\/([1-9]\d*)\/?$/;
-const weasylPostUrlRegex =
-  /^(?:https:\/\/)(?:www\.)?weasyl\.com\/~([a-zA-Z][a-zA-Z0-9_-]+)\/submissions\/([1-9]\d*(?:\/[a-zA-Z0-9_-]+)?)\/?$/;
-const inkbunnyPostUrlRegex = /^(?:https:\/\/)(?:www\.)?inkbunny\.net\/s\/([1-9]\d*)\/?$/;
-const sofurryPostUrlRegex = /^(?:https:\/\/)www\.sofurry\.com\/view\/([1-9]\d*)\/?$/;
-const mastodonPostUrlRegex = /^(?:https:\/\/)((?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/@([a-zA-Z][a-zA-Z0-9_-]+)\/([1-9]\d*)\/?$/;
-
-const refineAuthors = [
-  (value: { id: any } | any[]) => "id" in value || value.length > 0,
-  `"authors" cannot be empty`,
-] as const;
-const refineCopyrightedCharacters = [
-  (value: Record<string, any>) => !("" in value) || Object.keys(value).length == 1,
-  `"copyrightedCharacters" cannot mix empty catch-all key with other keys`,
-] as const;
-
 // Transformers
 
-const trimText = (text: string) => text.trim();
-const adjustDateForUTCOffset = (date: Date) =>
-  new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
-const parseMastodonPostUrl = (url: string, ctx: z.RefinementCtx) => {
-  const match = mastodonPostUrlRegex.exec(url);
-  if (!match) {
-    ctx.addIssue({
-      code: z.ZodIssueCode.custom,
-      message: `"mastodon" post contains an invalid URL`,
-    });
-    return z.NEVER;
-  }
-  return {
-    instance: match[1]!,
-    user: match[2]!,
-    postId: match[3]!,
+function parseRegex<R extends { [key: string]: string }>(regex: RegExp) {
+  return (url: string, ctx: z.RefinementCtx) => {
+    const match = regex.exec(url);
+    if (!match?.groups) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: `"${ctx.path}" did not match regex`,
+      });
+      return z.NEVER;
+    }
+    return match.groups as R;
   };
-};
+}
+
+// Schema definitions
+
+/** Record of website links for a user.
+ *
+ * For each entry, you can enter a URL for the value or - for any key apart
+ * from `website` - a pre-parsed object containing the link and username.
+ */
+const websiteLinks = z.object({
+  website: z
+    .string()
+    .url()
+    .transform((link) => {
+      link;
+    }),
+  eka: z.object({ link: z.string().url(), username: z.string() }).or(
+    z.string().transform((link, ctx) => {
+      const { username } = parseRegex<{ username: string }>(
+        /^(?:https?:\/\/)?(?:www\.)?aryion\.com\/g4\/user\/(?<username>[^\/]+)\/?$/,
+      )(link, ctx);
+      return { link, username };
+    }),
+  ),
+  furaffinity: z.object({ link: z.string().url(), username: z.string() }).or(
+    z.string().transform((link, ctx) => {
+      const { username } = parseRegex<{ username: string }>(
+        /^(?:https?:\/\/)?(?:www\.)?furaffinity\.net\/user\/(?<username>[^\/]+)\/?$/,
+      )(link, ctx);
+      return { link, username };
+    }),
+  ),
+  weasyl: z.object({ link: z.string().url(), username: z.string() }).or(
+    z.string().transform((link, ctx) => {
+      const { username } = parseRegex<{ username: string }>(
+        /^(?:https?:\/\/)?(?:www\.)?weasyl\.com\/\~(?<username>[^\/]+)\/?$/,
+      )(link, ctx);
+      return { link, username };
+    }),
+  ),
+  inkbunny: z.object({ link: z.string().url(), username: z.string() }).or(
+    z.string().transform((link, ctx) => {
+      const { username } = parseRegex<{ username: string }>(
+        /^(?:https?:\/\/)?(?:www\.)?inkbunny\.net\/(?<username>[^\/]+)\/?$/,
+      )(link, ctx);
+      return { link, username };
+    }),
+  ),
+  sofurry: z.object({ link: z.string().url(), username: z.string() }).or(
+    z.string().transform((link, ctx) => {
+      const { username } = parseRegex<{ username: string }>(/^(?:https?:\/\/)?(?<username>[^\.]+).sofurry.com\/?$/)(
+        link,
+        ctx,
+      );
+      return { link, username };
+    }),
+  ),
+  mastodon: z
+    .object({ link: z.string().url(), username: z.string().regex(/^[^@]+@[^@]+$/) })
+    .or(
+      z.string().transform((link, ctx) => {
+        const { username, instance } = parseRegex<{ username: string; instance: string }>(
+          /^(?:https?:\/\/)(?<instance>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/(?:@|users\/)(?<username>[a-zA-Z][a-zA-Z0-9_-]+)\/?$/,
+        )(link, ctx);
+        return { link, username: `${username}@${instance}` };
+      }),
+    )
+    .transform(({ link, username }) => {
+      const i = username.indexOf("@");
+      return { link, username, handle: username.slice(0, i), instance: username.slice(i + 1) };
+    }),
+  twitter: z.object({ link: z.string().url(), username: z.string() }).or(
+    z.string().transform((link, ctx) => {
+      const { username } = parseRegex<{ username: string }>(
+        /^(?:https?:\/\/)?(?:www\.)?(?:twitter\.com|x\.com)\/@?(?<username>[^\/]+)\/?$/,
+      )(link, ctx);
+      return { link, username };
+    }),
+  ),
+  bluesky: z.object({ link: z.string().url(), username: z.string() }).or(
+    z.string().transform((link, ctx) => {
+      const { username } = parseRegex<{ username: string }>(
+        /^(?:https?:\/\/)?bsky\.app\/profile\/(?<username>[^\/]+)\/?$/,
+      )(link, ctx);
+      return { link, username };
+    }),
+  ),
+  itaku: z.object({ link: z.string().url(), username: z.string() }).or(
+    z.string().transform((link, ctx) => {
+      const { username } = parseRegex<{ username: string }>(
+        /^(?:https?:\/\/)?(?:www\.)?itaku\.ee\/profile\/(?<username>[^\/]+)\/?$/,
+      )(link, ctx);
+      return { link, username };
+    }),
+  ),
+});
+/** Available languages. See https://r12a.github.io/app-subtags/ */
+const lang = z.enum(["en", "tok"]).default(DEFAULT_LANG);
+/** Platforms for a game. */
+const platform = z.enum(["web", "windows", "linux", "macos", "android", "ios"]);
+const userList = z
+  .array(reference("users"))
+  .refine((value) => value.length > 0, `user list cannot be empty`)
+  .or(reference("users").transform((user) => [user]));
+/** A record of the format `{"Character name": "user-id"}`.
+ *
+ * An empty character name `""` indicates that all characters are copyrighted
+ * by a certain user.
+ */
+const copyrightedCharacters = z
+  .record(z.string(), reference("users"))
+  .refine(
+    (value) => !("" in value) || Object.keys(value).length == 1,
+    `"copyrightedCharacters" cannot mix empty catch-all key with other keys`,
+  )
+  .default({});
+/** A record of the format `{ en: string, tok?: string, ... }`. */
+const langRecord = z.object({ [DEFAULT_LANG]: z.string() }).and(z.record(lang, z.string()));
+/** Common attributes for published content (stories + games). */
+const publishedContent = z.object({
+  // Required parameters
+  title: z.string(),
+  authors: userList,
+  contentWarning: z.string().trim(),
+  // Required parameters, but optional for drafts (isDraft == true)
+  pubDate: z
+    .date()
+    .transform((date: Date) => new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0))
+    .optional(),
+  description: z.string().trim(),
+  tags: z.array(z.string()),
+  // Optional parameters
+  isDraft: z.boolean().default(false),
+  relatedStories: z.array(reference("stories")).default([]),
+  relatedGames: z.array(reference("games")).default([]),
+  lang: lang,
+  copyrightedCharacters: copyrightedCharacters,
+  series: reference("series").optional(),
+  posts: z
+    .object({
+      eka: z.string().transform((link, ctx) => {
+        const { postId } = parseRegex<{ postId: string }>(
+          /^(?:https?:\/\/)(?:www\.)?aryion\.com\/g4\/view\/(?<postId>[1-9]\d*)\/?$/,
+        )(link, ctx);
+        return { link, postId };
+      }),
+      furaffinity: z.string().transform((link, ctx) => {
+        const { postId } = parseRegex<{ postId: string }>(
+          /^(?:https?:\/\/)(?:www\.)?furaffinity\.net\/view\/(?<postId>[1-9]\d*)\/?$/,
+        )(link, ctx);
+        return { link, postId };
+      }),
+      weasyl: z.string().transform((link, ctx) => {
+        const { user, postId } = parseRegex<{ user: string; postId: string }>(
+          /^(?:https?:\/\/)(?:www\.)?weasyl\.com\/~(?<user>[a-zA-Z][a-zA-Z0-9_-]+)\/submissions\/(?<postId>[1-9]\d*(?:\/[a-zA-Z0-9_-]+)?)\/?$/,
+        )(link, ctx);
+        return { link, user, postId };
+      }),
+      inkbunny: z.string().transform((link, ctx) => {
+        const { postId } = parseRegex<{ postId: string }>(
+          /^(?:https?:\/\/)(?:www\.)?inkbunny\.net\/s\/(?<postId>[1-9]\d*)\/?$/,
+        )(link, ctx);
+        return { link, postId };
+      }),
+      sofurry: z.string().transform((link, ctx) => {
+        const { postId } = parseRegex<{ postId: string }>(
+          /^(?:https?:\/\/)www\.sofurry\.com\/view\/(?<postId>[1-9]\d*)\/?$/,
+        )(link, ctx);
+        return { link, postId };
+      }),
+      bluesky: z.string().transform((link, ctx) => {
+        const { user, postId } = parseRegex<{ user: string; postId: string }>(
+          /^(?:https?:\/\/)bsky\.app\/profile\/(?<user>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/post\/(?<postId>[a-z0-9]+)\/?$/,
+        )(link, ctx);
+        return { link, user, postId };
+      }),
+      mastodon: z.string().transform((link, ctx) => {
+        const { instance, user, postId } = parseRegex<{ instance: string; user: string; postId: string }>(
+          /^(?:https?:\/\/)(?<instance>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/@(?<user>[a-zA-Z][a-zA-Z0-9_-]+)\/(?<postId>[1-9]\d*)\/?$/,
+        )(link, ctx);
+        return { link, instance, user, postId };
+      }),
+    })
+    .partial()
+    .default({}),
+});
 
 // Types
 
-const lang = z.enum(["eng", "tok"]).default(DEFAULT_LANG);
-const website = z.enum(WEBSITE_LIST);
-const platform = z.enum(GAME_PLATFORMS);
-const mastodonPost = z
-  .object({
-    instance: z.string(),
-    user: z.string(),
-    postId: z.string(),
-  })
-  .or(z.string().transform(parseMastodonPostUrl));
-const userList = z
-  .array(reference("users"))
-  .or(reference("users").transform((user) => [user]))
-  .refine((value) => value.length > 0, `user list cannot be empty`);
-const authors = userList.default([DEFAULT_AUTHOR_ID]);
-const copyrightedCharacters = z
-  .record(z.string(), reference("users"))
-  .default({})
-  .refine(...refineCopyrightedCharacters);
-// { eng: string, tok?: string, ... }
-const langRecord = z.object({ [DEFAULT_LANG]: z.string() }).and(z.record(lang, z.string()));
-
 export type Lang = z.output<typeof lang>;
-export type Website = z.infer<typeof website>;
+export type Website = keyof z.input<typeof websiteLinks>;
 export type GamePlatform = z.infer<typeof platform>;
 export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
 
@@ -93,79 +218,52 @@ export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
 const storiesCollection = defineCollection({
   type: "content",
   schema: ({ image }) =>
-    z.object({
-      // Required
-      title: z.string(),
-      wordCount: z.number().int().optional(),
-      contentWarning: z.string().transform(trimText),
-      description: z.string().transform(trimText),
-      tags: z.array(z.string()),
-      // Optional
-      pubDate: z.date().transform(adjustDateForUTCOffset).optional(),
-      isDraft: z.boolean().default(false),
-      shortTitle: z.string().optional(),
-      authors,
-      summary: z.string().transform(trimText).optional(),
-      thumbnail: image().optional(),
-      thumbnailWidth: z.number().int().optional(),
-      thumbnailHeight: z.number().int().optional(),
-      series: reference("series").optional(),
-      commissioner: userList.optional(),
-      requester: userList.optional(),
-      copyrightedCharacters: copyrightedCharacters,
-      lang,
-      prev: reference("stories").nullish(),
-      next: reference("stories").nullish(),
-      relatedStories: z.array(reference("stories")).default([]),
-      relatedGames: z.array(reference("games")).default([]),
-      posts: z
-        .object({
-          eka: z.string().regex(ekaPostUrlRegex),
-          furaffinity: z.string().regex(furaffinityPostUrlRegex),
-          weasyl: z.string().regex(weasylPostUrlRegex),
-          inkbunny: z.string().regex(inkbunnyPostUrlRegex),
-          sofurry: z.string().regex(sofurryPostUrlRegex),
-          mastodon: mastodonPost,
-        })
-        .partial()
-        .default({}),
-    }),
+    z
+      .object({
+        // Required parameters, but optional for drafts (isDraft == true)
+        wordCount: z.number().int().optional(),
+        thumbnail: image().optional(),
+        // Optional parameters
+        shortTitle: z.string().optional(),
+        commissioner: userList.optional(),
+        requester: userList.optional(),
+        summary: z.string().trim().optional(),
+        thumbnailWidth: z.number().int().optional(),
+        thumbnailHeight: z.number().int().optional(),
+        prev: reference("stories").nullish(),
+        next: reference("stories").nullish(),
+      })
+      .and(publishedContent)
+      .refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published story`)
+      .refine(
+        ({ isDraft, contentWarning }) => isDraft || contentWarning,
+        `Missing "contentWarning" for published story`,
+      )
+      .refine(({ isDraft, wordCount }) => isDraft || wordCount, `Missing "wordCount" for published story`)
+      .refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published story`)
+      .refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published story`)
+      .refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published story`),
 });
 
 const gamesCollection = defineCollection({
   type: "content",
   schema: ({ image }) =>
-    z.object({
-      // Required
-      title: z.string(),
-      contentWarning: z.string().transform(trimText),
-      description: z.string().transform(trimText),
-      platforms: z.array(platform).refine((platforms) => platforms.length > 0, `"platforms" cannot be empty`),
-      tags: z.array(z.string()),
-      // Optional
-      pubDate: z.date().transform(adjustDateForUTCOffset).optional(),
-      isDraft: z.boolean().default(false),
-      authors,
-      thumbnail: image().optional(),
-      thumbnailWidth: z.number().int().optional(),
-      thumbnailHeight: z.number().int().optional(),
-      series: reference("series").optional(),
-      copyrightedCharacters: copyrightedCharacters,
-      lang,
-      relatedStories: z.array(reference("stories")).default([]),
-      relatedGames: z.array(reference("games")).default([]),
-      posts: z
-        .object({
-          eka: z.string().regex(ekaPostUrlRegex),
-          furaffinity: z.string().regex(furaffinityPostUrlRegex),
-          weasyl: z.string().regex(weasylPostUrlRegex),
-          inkbunny: z.string().regex(inkbunnyPostUrlRegex),
-          sofurry: z.string().regex(sofurryPostUrlRegex),
-          mastodon: mastodonPost,
-        })
-        .partial()
-        .default({}),
-    }),
+    z
+      .object({
+        // Required parameters, but optional for drafts (isDraft == true)
+        platforms: z.array(platform).default([]),
+        thumbnail: image().optional(),
+        // Optional parameters
+        thumbnailWidth: z.number().int().optional(),
+        thumbnailHeight: z.number().int().optional(),
+      })
+      .and(publishedContent)
+      .refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published game`)
+      .refine(({ isDraft, contentWarning }) => isDraft || contentWarning, `Missing "contentWarning" for published game`)
+      .refine(({ isDraft, platforms }) => isDraft || platforms.length, `Missing "platforms" for published game`)
+      .refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published game`)
+      .refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published game`)
+      .refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published game`),
 });
 
 // Data collections
@@ -175,12 +273,11 @@ const usersCollection = defineCollection({
   schema: ({ image }) =>
     z
       .object({
-        // Required
-        name: z.string(),
-        links: z.record(website, z.union([z.string().url(), z.tuple([z.string().url(), z.string()])])),
-        // Optional
-        preferredLink: website.nullish(),
-        lang: langRecord.optional(),
+        // Required parameters
+        name: langRecord.or(z.string()),
+        links: websiteLinks.partial(),
+        // Optional parameters
+        preferredLink: websiteLinks.keyof().nullish(),
         avatar: image().optional(),
       })
       .refine(
@@ -195,7 +292,7 @@ const usersCollection = defineCollection({
 const seriesCollection = defineCollection({
   type: "data",
   schema: z.object({
-    // Required
+    // Required parameters
     name: z.string(),
     url: z.string().regex(/^(\/[a-z0-9_-]+)+\/?$/, `"url" must be a local URL`),
   }),
@@ -204,16 +301,18 @@ const seriesCollection = defineCollection({
 const tagCategoriesCollection = defineCollection({
   type: "data",
   schema: z.object({
-    // Required
+    // Required parameters
     name: z.string(),
     index: z.number().int(),
-    tags: z.array(
-      z.object({
-        name: z.union([z.string(), langRecord]),
-        description: z.string().optional(),
-        related: z.array(z.string()).optional(),
-      }),
-    ),
+    tags: z
+      .array(
+        z.object({
+          name: langRecord.or(z.string()),
+          description: z.string().trim().optional(),
+          related: z.array(z.string()).default([]),
+        }),
+      )
+      .refine((tags) => tags.length, `"tags" cannot be empty`),
   }),
 });
 
diff --git a/src/content/games/crossing-over.md b/src/content/games/crossing-over.md
index 8135d42..8c5da85 100644
--- a/src/content/games/crossing-over.md
+++ b/src/content/games/crossing-over.md
@@ -28,6 +28,7 @@ posts:
   inkbunny: https://inkbunny.net/s/3262911
   sofurry: https://www.sofurry.com/view/2109688
   weasyl: https://www.weasyl.com/~badmanners/submissions/2356092/crossing-over-vore-game
+  bluesky: https://bsky.app/profile/badmanners.xyz/post/3kmigrf5q2x24
   mastodon: https://meow.social/@BadManners/112009918919441027
 tags:
   - oral vore
diff --git a/src/content/stories/playing-it-safe.md b/src/content/stories/playing-it-safe.md
index 62005b3..436148f 100644
--- a/src/content/stories/playing-it-safe.md
+++ b/src/content/stories/playing-it-safe.md
@@ -2,7 +2,6 @@
 slug: playing-it-safe
 title: Playing It Safe
 pubDate: 2024-08-08
-isDraft: true
 authors: bad-manners
 wordCount: 9900
 contentWarning: >
diff --git a/src/content/stories/tiny-accident.md b/src/content/stories/tiny-accident.md
index 787da56..cdc5c74 100644
--- a/src/content/stories/tiny-accident.md
+++ b/src/content/stories/tiny-accident.md
@@ -16,6 +16,7 @@ posts:
   inkbunny: https://inkbunny.net/s/3283508
   sofurry: https://www.sofurry.com/view/2118138
   weasyl: https://www.weasyl.com/~badmanners/submissions/2363560/tiny-accident
+  bluesky: https://bsky.app/profile/badmanners.xyz/post/3kok52wijz32c
   mastodon: https://meow.social/@BadManners/112157812554023271
 tags:
   - anthro predator
diff --git a/src/content/tag-categories/1-types-of-vore.yaml b/src/content/tag-categories/1-types-of-vore.yaml
index 165b47d..ce24be4 100644
--- a/src/content/tag-categories/1-types-of-vore.yaml
+++ b/src/content/tag-categories/1-types-of-vore.yaml
@@ -1,7 +1,7 @@
 name: Types of vore
 index: 1
 tags:
-  - name: { eng: oral vore, tok: moku musi kepeken uta }
+  - name: { en: oral vore, tok: moku musi kepeken uta }
     description: Scenarios where prey are consumed by the predator through their mouth.
   - name: anal vore
     description: Scenarios where prey are consumed by the predator through their butt/anus.
diff --git a/src/content/tag-categories/2-body-types.yaml b/src/content/tag-categories/2-body-types.yaml
index 829e7b8..ddadbdf 100644
--- a/src/content/tag-categories/2-body-types.yaml
+++ b/src/content/tag-categories/2-body-types.yaml
@@ -7,7 +7,7 @@ tags:
     description: Scenarios where at least one of the predators is an animal based on a real or mythological creature.
   - 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: { eng: ambiguous predator, tok: sijelo pi jan pi wawa mute li ale }
+  - 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.
   - name: human prey
     description: Scenarios where at least one of the prey is a human person.
@@ -15,5 +15,5 @@ tags:
     description: Scenarios where at least one of the prey is an anthropomorphic animal, i.e. generally regarded as a "furry".
   - name: feral prey
     description: Scenarios where at least one of the predators is an animal based on a real or mythological creature.
-  - name: { eng: ambiguous prey, tok: sijelo pi jan pi wawa lili li ale }
+  - 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.
diff --git a/src/content/tag-categories/3-genders.yaml b/src/content/tag-categories/3-genders.yaml
index ffdaa6a..aff336e 100644
--- a/src/content/tag-categories/3-genders.yaml
+++ b/src/content/tag-categories/3-genders.yaml
@@ -13,7 +13,7 @@ tags:
     description: Scenarios where at least one of the predators is a woman and/or female-presenting.
   - 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: { eng: ambiguous gender predator, tok: jan pi wawa mute li meli anu mije }
+  - 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.
   - name: male prey
     description: Scenarios where at least one of the prey is a man and/or male-presenting.
@@ -27,5 +27,5 @@ tags:
       - female prey
   - 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: { eng: ambiguous gender prey, tok: jan pi wawa lili li meli anu mije }
+  - 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.
diff --git a/src/content/tag-categories/5-willingness.yaml b/src/content/tag-categories/5-willingness.yaml
index c3855cb..326cd50 100644
--- a/src/content/tag-categories/5-willingness.yaml
+++ b/src/content/tag-categories/5-willingness.yaml
@@ -1,7 +1,7 @@
 name: Willingness
 index: 5
 tags:
-  - name: { eng: willing predator, tok: jan pi wawa mute li wile e moku musi }
+  - 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.
   - 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.
@@ -13,7 +13,7 @@ tags:
     description: Scenarios where at least one of the prey participates in vore willingly.
   - 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: { eng: unwilling prey, tok: jan pi wawa lili li wile ala e moku musi }
+  - 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.
   - name: asleep prey
     description: Scenarios where at least one of the predators participates in vore while asleep.
diff --git a/src/content/tag-categories/6-vore-related-scenarios.yaml b/src/content/tag-categories/6-vore-related-scenarios.yaml
index 5af474e..793ce9b 100644
--- a/src/content/tag-categories/6-vore-related-scenarios.yaml
+++ b/src/content/tag-categories/6-vore-related-scenarios.yaml
@@ -66,4 +66,4 @@ tags:
   - name: soul vore
     description: Scenarios where predators consume a soul instead of their prey's body.
   - name: Vore Day
-    description: Stories created in commemoration of Vore Day, which is celebrated on August 8ᵗʰ, and/or are set in said day.
+    description: Stories created in commemoration of Vore Day, which is celebrated on August 8th, and/or are set in said day.
diff --git a/src/content/tag-categories/9-type-of-content.yaml b/src/content/tag-categories/9-type-of-content.yaml
index b1a76f6..1c12c00 100644
--- a/src/content/tag-categories/9-type-of-content.yaml
+++ b/src/content/tag-categories/9-type-of-content.yaml
@@ -5,7 +5,7 @@ tags:
     description: Stories made by someone else's request, as a gift.
   - name: commission
     description: Stories made as part of a commission to someone else.
-  - name: { eng: flash fiction, tok: lipu lili }
+  - name: { en: flash fiction, tok: lipu lili }
     description: Short-format stories of no more than 2,500 words.
   - name: toki pona
     description: Stories written in toki pona, the language of good.
diff --git a/src/content/users/anonymous.yaml b/src/content/users/anonymous.yaml
index d1c1ea4..3d0ff6f 100644
--- a/src/content/users/anonymous.yaml
+++ b/src/content/users/anonymous.yaml
@@ -1,6 +1,5 @@
-name: Anonymous
-lang:
-  eng: anonymous
+name:
+  en: anonymous
   tok: jan pi nimi ala
 links: {}
 preferredLink: ~
diff --git a/src/content/users/asof-yeun.yaml b/src/content/users/asof-yeun.yaml
index 51f891d..36d7e8c 100644
--- a/src/content/users/asof-yeun.yaml
+++ b/src/content/users/asof-yeun.yaml
@@ -1,7 +1,7 @@
 name: Asof Yeun
 links:
   eka: https://aryion.com/g4/user/asofyeun
-  furaffinity: https://www.furaffinity.net/user/asofyeun
+  furaffinity: https://www.furaffinity.net/user/AsofYeun
   inkbunny: https://inkbunny.net/asofyeun
   sofurry: https://asofyeun.sofurry.com/
   weasyl: https://www.weasyl.com/~asofyeun
diff --git a/src/content/users/bad-manners.yaml b/src/content/users/bad-manners.yaml
index 7da80c8..20f6f6d 100644
--- a/src/content/users/bad-manners.yaml
+++ b/src/content/users/bad-manners.yaml
@@ -1,6 +1,5 @@
-name: Bad Manners
-lang:
-  eng: Bad Manners
+name:
+  en: Bad Manners
   tok: nasin ike Pemene
 avatar: /src/assets/images/logo_bm.png
 links:
@@ -9,8 +8,8 @@ links:
   furaffinity: https://www.furaffinity.net/user/BadManners
   inkbunny: https://inkbunny.net/BadManners
   sofurry:
-    - https://bad-manners.sofurry.com/
-    - Bad Manners
+    link: https://bad-manners.sofurry.com/
+    username: Bad Manners
   weasyl: https://www.weasyl.com/~BadManners
   twitter: https://twitter.com/BadManners__
   mastodon: https://meow.social/@BadManners
diff --git a/src/content/users/hans-woofington.yaml b/src/content/users/hans-woofington.yaml
index dd8021d..24749fc 100644
--- a/src/content/users/hans-woofington.yaml
+++ b/src/content/users/hans-woofington.yaml
@@ -1,6 +1,6 @@
 name: Dr. Hans Woofington
 links:
   furaffinity:
-    - https://www.furaffinity.net/user/hanslewdington/
-    - Hans_Lewdington
+    link: https://www.furaffinity.net/user/hanslewdington/
+    username: Hans_Lewdington
 preferredLink: furaffinity
diff --git a/src/content/users/yolkmonkey.yaml b/src/content/users/yolkmonkey.yaml
index fc08adf..54c2764 100644
--- a/src/content/users/yolkmonkey.yaml
+++ b/src/content/users/yolkmonkey.yaml
@@ -2,6 +2,6 @@ name: YolkMonkey
 links:
   furaffinity: https://furaffinity.net/user/Vampire101
   sofurry:
-    - https://vampire101.sofurry.com/
-    - Vampire101
+    link: https://vampire101.sofurry.com/
+    username: Vampire101
 preferredLink: furaffinity
diff --git a/src/i18n/index.ts b/src/i18n/index.ts
index 1958420..b63ef6a 100644
--- a/src/i18n/index.ts
+++ b/src/i18n/index.ts
@@ -1,10 +1,11 @@
-import { type GamePlatform, type Lang } from "../content/config";
+import type { GamePlatform, Lang } from "../content/config";
 import { DEFAULT_LANG } from "../content/config";
 export { DEFAULT_LANG } from "../content/config";
 
 const UI_STRINGS = {
+  // Utility functions
   "util/join_names": {
-    eng: (names: string[]) =>
+    en: (names: string[]) =>
       names.length <= 1
         ? names.join("")
         : names.length == 2
@@ -13,10 +14,10 @@ const UI_STRINGS = {
     tok: (names: string[]) => names.join(" en "),
   },
   "util/capitalize": {
-    eng: (text: string) => (text.length > 0 ? `${text[0].toUpperCase()}${text.slice(1)}` : ""),
+    en: (text: string) => (text.length > 0 ? `${text[0].toUpperCase()}${text.slice(1)}` : ""),
   },
   "util/enumerate": {
-    eng: (count: number, nounSingular: string, nounPlural: string | undefined) => {
+    en: (count: number, nounSingular: string, nounPlural?: string) => {
       if (count == 0) {
         return `no ${nounPlural ?? nounSingular}`;
       }
@@ -25,146 +26,205 @@ const UI_STRINGS = {
       }
       return `${count} ${nounPlural ?? nounSingular}`;
     },
-    tok: (count: number, nounSingular: string, nounPlural: string | undefined) =>
+    tok: (count: number, nounSingular: string, nounPlural?: string) =>
       `${(count > 1 && nounPlural) || nounSingular} ${["ala", "wan", "tu"][count] || "mute"}`,
   },
-  "export_story/writing": {
-    eng: (authorsList: string[]) => `Writing: ${authorsList.join(" ")}`,
+  // export-story API functions
+  "export_story/authors": {
+    en: (authorsList: string[]) => `Writing: ${authorsList.join(" ")}`,
     tok: (authorsList: string[]) => `lipu ni li tan jan ni: ${authorsList.join(" en ")}`,
   },
   "export_story/request_for": {
-    eng: (requesterList: string[]) => `Request for: ${requesterList.join(" ")}`,
+    en: (requesterList: string[]) => `Request for: ${requesterList.join(" ")}`,
   },
   "export_story/commissioned_by": {
-    eng: (commissionerList: string[]) => `Commissioned by: ${commissionerList.join(" ")}`,
+    en: (commissionerList: string[]) => `Commissioned by: ${commissionerList.join(" ")}`,
   },
-  "story/return_to_stories": {
-    eng: "Return to stories",
-    tok: "o tawa e lipu ale",
+  // Shared strings for published content (stories + games)
+  "published_content/return_to_series": {
+    en: (seriesName: string) => `Return to ${seriesName}`,
   },
-  "story/return_to_series": {
-    eng: (seriesName: string) => `Return to ${seriesName}`,
-  },
-  "story/go_to_description": {
-    eng: "Go to description",
+  "published_content/go_to_description": {
+    en: "Go to description",
     tok: "o tawa e toki lipu",
   },
-  "story/toggle_dark_mode": {
-    eng: "Toggle dark mode",
+  "published_content/toggle_dark_mode": {
+    en: "Toggle dark mode",
     tok: "o ante e kule lipu",
   },
+  "published_content/cover_art_alt": {
+    en: (title: string) => `Cover art for ${title}`,
+    tok: (_title: string) => `sitelen tawa lipu ni`,
+  },
+  "published_content/publish_date": {
+    en: (date: Date) => date.toISOString().slice(0, 10),
+    tok: (date: Date) => `tenpo suno ${date.toISOString().slice(0, 10)}`,
+  },
+  "published_content/publish_date_aria_label": {
+    en: "Publish date",
+    tok: "tenpo pi pana lipu",
+  },
+  "published_content/publish_date_aria_description": {
+    en: (date: Date) =>
+      date.toLocaleDateString("en-US", {
+        month: "long",
+        day: "numeric",
+        year: "numeric",
+      }),
+    tok: (_date: Date) => "",
+  },
+  "published_content/description": {
+    en: "Description",
+    tok: "toki lipu",
+  },
+  "published_content/to_top": {
+    en: "To top",
+    tok: "tawa sewi",
+  },
+  "published_content/tags": {
+    en: "Tags",
+    tok: "nimi kulupu",
+  },
+  "published_content/copyright_year": {
+    en: (year: string | number) => `© ${year}`,
+    tok: (year: string | number) => `© tenpo pi sike suno ${year}`,
+  },
+  "published_content/licenses": {
+    en: "Licenses",
+    tok: "lipu lawa",
+  },
+  "published_content/draft_warning": {
+    en: "DRAFT VERSION – DO NOT REDISTRIBUTE",
+  },
+  "published_content/related_stories": {
+    en: "Related stories",
+  },
+  "published_content/related_games": {
+    en: "Related games",
+  },
+  // Story page-specific strings
+  "story/return_to_stories": {
+    en: "Return to stories",
+    tok: "o tawa e lipu ale",
+  },
+  "story/title_aria_label": {
+    en: "Story title",
+    tok: "nimi lipu",
+  },
+  "story/authors_aria_label": {
+    en: (authors: any[]) => (authors.length == 1 ? "Author" : "Authors"),
+    tok: (_authors: any[]) => "jan pi pali lipu",
+  },
+  "story/requesters_aria_label": {
+    en: (requesters: any[]) => (requesters.length == 1 ? "Requester" : "Requesters"),
+  },
+  "story/commissioners_aria_label": {
+    en: (commissioners: any[]) => (commissioners.length == 1 ? "Commissioner" : "Commissioners"),
+  },
   "story/warnings": {
-    eng: (wordCount: number | string | undefined, contentWarning: string) =>
+    en: (wordCount: number | string | undefined, contentWarning: string) =>
       wordCount ? `Word count: ${wordCount}. ${contentWarning}` : contentWarning,
     tok: (_wordCount: number | string | undefined, contentWarning: string) => contentWarning,
   },
-  "story/publish_date": {
-    eng: (date: string) => date,
-    tok: (date: string) => `tenpo suno ${date}`,
-  },
-  "story/description": {
-    eng: "Description",
-    tok: "toki lipu",
+  "story/article_aria_label": {
+    en: "Story",
+    tok: "lipu",
   },
   "story/summary": {
-    eng: "Summary",
+    en: "Summary",
     tok: "lipu tawa tenpo lili",
   },
   "story/reveal_summary": {
-    eng: "Click to reveal",
+    en: "Click to reveal",
     tok: "Click to reveal summary in English",
   },
-  "story/to_top": {
-    eng: "To top",
-    tok: "tawa sewi",
-  },
-  "story/tags": {
-    eng: "Tags",
-    tok: "nimi kulupu",
-  },
-  "story/copyright_year": {
-    eng: (year: string | number) => `© ${year}`,
-    tok: (year: string | number) => `© tenpo pi sike suno ${year}`,
-  },
-  "story/licenses": {
-    eng: "Licenses",
-    tok: "lipu lawa",
-  },
   "story/authors": {
-    eng: (authorsList: string[]) => `by ${UI_STRINGS["util/join_names"].eng(authorsList)}`,
+    en: (authorsList: string[]) => `by ${UI_STRINGS["util/join_names"].en(authorsList)}`,
     tok: (authorsList: string[]) =>
       authorsList.length > 1
         ? `lipu ni li tan jan ni: ${UI_STRINGS["util/join_names"].tok(authorsList)}`
         : `lipu ni li tan ${authorsList[0]}`,
   },
   "story/commissioned_by": {
-    eng: (commissionersList: string[]) => `Commissioned by ${UI_STRINGS["util/join_names"].eng(commissionersList)}`,
+    en: (commissionersList: string[]) => `Commissioned by ${UI_STRINGS["util/join_names"].en(commissionersList)}`,
   },
   "story/requested_by": {
-    eng: (requestersList: string[]) => `Requested by ${UI_STRINGS["util/join_names"].eng(requestersList)}`,
+    en: (requestersList: string[]) => `Requested by ${UI_STRINGS["util/join_names"].en(requestersList)}`,
   },
-  "story/draft_warning": {
-    eng: "DRAFT VERSION – DO NOT REDISTRIBUTE",
+  // Game page-specific strings
+  "game/return_to_games": {
+    en: "Return to games",
   },
-  "characters/characters_are_copyrighted_by": {
-    eng: (owner: string, charactersList: string[]) =>
-      charactersList.length == 1
-        ? `${charactersList[0]} is © ${owner}`
-        : `${UI_STRINGS["util/join_names"].eng(charactersList)} are © ${owner}`,
-  },
-  "characters/all_characters_are_copyrighted_by": {
-    eng: (owner: string) => `All characters are © ${owner}`,
+  "game/title_aria_label": {
+    en: "Game title",
   },
   "game/platforms": {
-    eng: (platforms: GamePlatform[]) => {
+    en: (platforms: GamePlatform[]) => {
       if (platforms.length == 0) {
         return "";
       }
       const translatedPlatforms = platforms.map((platform) => {
-        const platformLang = UI_STRINGS[`game/platform_${platform}`].eng;
+        const platformLang = UI_STRINGS[`game/platform_${platform}`].en;
         if (!platformLang) {
           throw new Error(`Invalid platform "${platform}"`);
         }
         return platformLang;
       });
-      return `A game for ${UI_STRINGS["util/join_names"].eng(translatedPlatforms)}.`;
+      return `A game for ${UI_STRINGS["util/join_names"].en(translatedPlatforms)}.`;
     },
   },
   "game/platform_web": {
-    eng: "web browsers",
+    en: "web browsers",
   },
   "game/platform_windows": {
-    eng: "Windows",
+    en: "Windows",
   },
   "game/platform_linux": {
-    eng: "Linux",
+    en: "Linux",
   },
   "game/platform_macos": {
-    eng: "macOS",
+    en: "macOS",
   },
   "game/platform_android": {
-    eng: "Android",
+    en: "Android",
   },
   "game/platform_ios": {
-    eng: "iOS",
+    en: "iOS",
   },
   "game/warnings": {
-    eng: (platforms: GamePlatform[], contentWarning: string) =>
-      platforms.length > 0 ? `${UI_STRINGS["game/platforms"].eng(platforms)} ${contentWarning}` : contentWarning,
+    en: (platforms: GamePlatform[], contentWarning: string) =>
+      platforms.length > 0 ? `${UI_STRINGS["game/platforms"].en(platforms)} ${contentWarning}` : contentWarning,
   },
+  "game/article_aria_label": {
+    en: "Game",
+  },
+  // Copyrighted character-related strings
+  "characters/copyrighted_characters_aria_label": {
+    en: "Copyrighted characters",
+  },
+  "characters/characters_are_copyrighted_by": {
+    en: (owner: string, charactersList: string[]) =>
+      charactersList.length == 1
+        ? `${charactersList[0]} is © ${owner}`
+        : `${UI_STRINGS["util/join_names"].en(charactersList)} are © ${owner}`,
+  },
+  "characters/all_characters_are_copyrighted_by": {
+    en: (owner: string) => `All characters are © ${owner}`,
+  },
+  // Tag-related strings
   "tag/total_works_with_tag": {
-    eng: (tag: string, storiesCount: number, gamesCount: number) => {
+    en: (tag: string, storiesCount: number, gamesCount: number) => {
       const content = [];
       if (storiesCount > 0) {
-        content.push(UI_STRINGS["util/enumerate"].eng(storiesCount, "story", "stories"));
+        content.push(UI_STRINGS["util/enumerate"].en(storiesCount, "story", "stories"));
       }
       if (gamesCount > 0) {
-        content.push(UI_STRINGS["util/enumerate"].eng(gamesCount, "game", "games"));
+        content.push(UI_STRINGS["util/enumerate"].en(gamesCount, "game", "games"));
       }
       if (content.length == 0) {
         return `No works tagged with "${tag}".`;
       }
-      return UI_STRINGS["util/capitalize"].eng(`${UI_STRINGS["util/join_names"].eng(content)} tagged with "${tag}".`);
+      return UI_STRINGS["util/capitalize"].en(`${UI_STRINGS["util/join_names"].en(content)} tagged with "${tag}".`);
     },
   },
 } as const;
@@ -176,6 +236,9 @@ type TranslationEntry<T> = { [DEFAULT_LANG]: T } & {
 type TranslationArgs<K extends TranslationKey> =
   (typeof UI_STRINGS)[K] extends TranslationEntry<infer T> ? (T extends (...args: infer A) => string ? A : []) : never;
 
+/** Translates some text according to the provided language, a translation key,
+ * and optionally any required arguments.
+ */
 export function t<K extends TranslationKey>(lang: Lang, key: K, ...args: TranslationArgs<K>): string {
   if (key in UI_STRINGS) {
     const translation: string | ((...args: TranslationArgs<K>) => string) =
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
index 6179484..3616a03 100644
--- a/src/layouts/BaseLayout.astro
+++ b/src/layouts/BaseLayout.astro
@@ -6,12 +6,13 @@ import AgeRestrictedModal from "../components/AgeRestrictedModal.astro";
 
 type Props = {
   pageTitle?: string;
+  lang?: string;
 };
 
-const { pageTitle } = Astro.props;
+const { pageTitle = "Gallery", lang = "en" } = Astro.props;
 ---
 
-<html lang="en">
+<html lang={lang}>
   <head>
     <meta charset="UTF-8" />
     <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
@@ -23,7 +24,7 @@ const { pageTitle } = Astro.props;
     <meta name="theme-color" content="#ffffff" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <meta name="generator" content={Astro.generator} />
-    <title>{pageTitle || "Gallery"} | Bad Manners</title>
+    <title>{pageTitle} | Bad Manners</title>
     <link rel="me" href="https://meow.social/@BadManners" />
     <link
       rel="alternate"
diff --git a/src/layouts/GameLayout.astro b/src/layouts/GameLayout.astro
index 4dfc634..933c2bc 100644
--- a/src/layouts/GameLayout.astro
+++ b/src/layouts/GameLayout.astro
@@ -18,8 +18,8 @@ const { props } = Astro;
 const series = props.series && (await getEntry(props.series));
 const authorsList = await getEntries(props.authors);
 const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
-// const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
-// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
+const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
+const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
 const categorizedTags = Object.fromEntries(
   (await getCollection("tag-categories")).flatMap((category) =>
     category.data.tags.map<[string, string | null]>(({ name }) =>
@@ -44,10 +44,10 @@ const thumbnail =
   (await getImage({ src: props.thumbnail, width: props.thumbnailWidth, height: props.thumbnailHeight }));
 ---
 
-<BaseLayout pageTitle={props.title}>
+<BaseLayout pageTitle={props.title} lang={props.lang}>
   <Fragment slot="head">
     <meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
-    <meta property="og:description" content={props.contentWarning} />
+    <meta property="og:description" content={t(props.lang, "game/warnings", props.platforms, props.contentWarning)} />
     <meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" />
     {
       thumbnail ? (
@@ -55,7 +55,7 @@ const thumbnail =
           <meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
           <meta
             property="og:image:alt"
-            content={`Cover art for ${props.title}`}
+            content={t(props.lang, "published_content/cover_art_alt", props.title)}
             data-pagefind-meta="image_alt[content]"
           />
         </Fragment>
@@ -78,7 +78,9 @@ const thumbnail =
         <a
           href={series ? series.data.url : "/games"}
           class="text-link my-1 h-9 w-9 p-2"
-          aria-label={`Return to ${series ? series.data.name : "games"}`}
+          aria-label={series
+            ? t(props.lang, "published_content/return_to_series", series.data.name)
+            : t(props.lang, "game/return_to_games")}
         >
           <svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
             <path
@@ -89,7 +91,7 @@ const thumbnail =
         <a
           href="#description"
           class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
-          aria-label="Go to description"
+          aria-label={t(props.lang, "published_content/go_to_description")}
         >
           <svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
             <path
@@ -100,7 +102,7 @@ const thumbnail =
         <button
           data-dark-mode
           class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
-          aria-label="Toggle dark mode"
+          aria-label={t(props.lang, "published_content/toggle_dark_mode")}
         >
           <svg viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden="true">
             <path
@@ -120,7 +122,11 @@ const thumbnail =
       data-pagefind-body={props.isDraft ? undefined : ""}
       data-pagefind-meta="type:game"
     >
-      <h1 id="game-title" class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100">
+      <h1
+        id="game-title"
+        class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100"
+        aria-label={t(props.lang, "game/title_aria_label")}
+      >
         {props.title}
       </h1>
       <section
@@ -136,7 +142,7 @@ const thumbnail =
         {
           props.isDraft ? (
             <p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
-              {t(props.lang, "story/draft_warning")}
+              {t(props.lang, "published_content/draft_warning")}
             </p>
           ) : null
         }
@@ -155,7 +161,7 @@ const thumbnail =
             <img
               loading="eager"
               src={thumbnail.src}
-              alt={`Cover art for ${props.title}`}
+              alt={t(props.lang, "published_content/cover_art_alt", props.title)}
               width={props.thumbnailWidth}
               height={props.thumbnailHeight}
               class="mx-auto my-5 shadow-lg"
@@ -165,7 +171,7 @@ const thumbnail =
         ) : null
       }
       <hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
-      <article id="game" class="pr-1 font-serif">
+      <article id="game" class="pr-1 font-serif" aria-label={t(props.lang, "game/article_aria_label")}>
         <Prose>
           <slot />
         </Prose>
@@ -177,28 +183,26 @@ const thumbnail =
             id="draft-warning-bottom"
             class="py-2 text-center font-serif text-2xl font-semibold not-italic text-red-600"
           >
-            {t(props.lang, "story/draft_warning")}
+            {t(props.lang, "published_content/draft_warning")}
           </p>
         ) : props.pubDate ? (
           <p
             id="publish-date"
             class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
-            aria-label="Publish date"
-            aria-description={props.pubDate.toLocaleDateString("en-US", {
-              month: "long",
-              day: "numeric",
-              year: "numeric",
-            })}
+            aria-label={t(props.lang, "published_content/publish_date_aria_label")}
+            aria-description={
+              t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
+            }
             data-pagefind-index-attrs="aria-description"
             data-pagefind-meta={`date:${props.pubDate.toISOString().slice(0, 10)}`}
           >
-            {t(props.lang, "story/publish_date", props.pubDate.toISOString().slice(0, 10))}
+            {t(props.lang, "published_content/publish_date", props.pubDate)}
           </p>
         ) : null
       }
       <section id="description" class="px-2 font-serif" aria-describedby="title-description">
         <h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
-          {t(props.lang, "story/description")}
+          {t(props.lang, "published_content/description")}
         </h2>
         <Prose>
           <Markdown of={props.description} />
@@ -211,14 +215,50 @@ const thumbnail =
             ><path
               d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
             ></path></svg
-          ><span>{t(props.lang, "story/to_top")}</span></a
+          ><span>{t(props.lang, "published_content/to_top")}</span></a
         >
       </div>
+      {
+        relatedStories.length > 0 ? (
+          <section id="related" aria-describedby="title-related" class="my-5">
+            <h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
+              {t(props.lang, "published_content/related_stories")}
+            </h2>
+            <Prose>
+              <ul>
+                {relatedStories.map((story) => (
+                  <li>
+                    <a href={`/stories/${story.slug}`}>{story.data.title}</a>
+                  </li>
+                ))}
+              </ul>
+            </Prose>
+          </section>
+        ) : null
+      }
+      {
+        relatedGames.length > 0 ? (
+          <section id="related" aria-describedby="title-related" class="my-5">
+            <h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
+              {t(props.lang, "published_content/related_games")}
+            </h2>
+            <Prose>
+              <ul>
+                {relatedGames.map((game) => (
+                  <li>
+                    <a href={`/games/${game.slug}`}>{game.data.title}</a>
+                  </li>
+                ))}
+              </ul>
+            </Prose>
+          </section>
+        ) : null
+      }
       {
         tags.length > 0 ? (
           <section id="tags" aria-describedby="title-tags" class="my-5">
             <h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
-              Tags
+              {t(props.lang, "published_content/tags")}
             </h2>
             <ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
               {tags.map(({ id, name }) => (
@@ -232,16 +272,12 @@ const thumbnail =
           </section>
         ) : null
       }
-      <MastodonComments
-        instance={props.posts.mastodon?.instance}
-        user={props.posts.mastodon?.user}
-        postId={props.posts.mastodon?.postId}
-      />
+      {props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
     </main>
     <div class="pt-6 text-center text-xs text-black dark:text-white">
-      <span>{t(props.lang, "story/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
+      <span>{t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
       <a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
-        >{t(props.lang, "story/licenses")}</a
+        >{t(props.lang, "published_content/licenses")}</a
       >
     </div>
   </div>
diff --git a/src/layouts/StoryLayout.astro b/src/layouts/StoryLayout.astro
index b3e50e6..b1ea437 100644
--- a/src/layouts/StoryLayout.astro
+++ b/src/layouts/StoryLayout.astro
@@ -17,21 +17,15 @@ import { formatCopyrightedCharacters } from "../utils/format_copyrighted_charact
 type Props = CollectionEntry<"stories">["data"];
 
 const { props } = Astro;
-let prev = props.prev && (await getEntry(props.prev));
-if (prev && prev.data.isDraft) {
-  prev = undefined;
-}
-let next = props.next && (await getEntry(props.next));
-if (next && next.data.isDraft) {
-  next = undefined;
-}
+const prev = props.prev && (await getEntry(props.prev));
+const next = props.next && (await getEntry(props.next));
 const series = props.series && (await getEntry(props.series));
 const authorsList = await getEntries(props.authors);
 const commissionersList = props.commissioner && (await getEntries(props.commissioner));
 const requestersList = props.requester && (await getEntries(props.requester));
 const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
 const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
-// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
+const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
 const categorizedTags = Object.fromEntries(
   (await getCollection("tag-categories")).flatMap((category) =>
     category.data.tags.map<[string, string | null]>(({ name }) =>
@@ -57,7 +51,7 @@ const thumbnail =
 const wordCount = props.wordCount?.toString();
 ---
 
-<BaseLayout pageTitle={props.title}>
+<BaseLayout pageTitle={props.title} lang={props.lang}>
   <Fragment slot="head">
     <meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
     <meta property="og:description" content={t(props.lang, "story/warnings", wordCount, props.contentWarning)} />
@@ -68,7 +62,7 @@ const wordCount = props.wordCount?.toString();
           <meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
           <meta
             property="og:image:alt"
-            content={`Cover art for ${props.shortTitle || props.title}`}
+            content={t(props.lang, "published_content/cover_art_alt", props.shortTitle || props.title)}
             data-pagefind-meta="image_alt[content]"
           />
         </Fragment>
@@ -92,7 +86,7 @@ const wordCount = props.wordCount?.toString();
           href={series ? series.data.url : "/stories/1"}
           class="text-link my-1 h-9 w-9 p-2"
           aria-label={series
-            ? t(props.lang, "story/return_to_series", series.data.name)
+            ? t(props.lang, "published_content/return_to_series", series.data.name)
             : t(props.lang, "story/return_to_stories")}
         >
           <svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
@@ -104,7 +98,7 @@ const wordCount = props.wordCount?.toString();
         <a
           href="#description"
           class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
-          aria-label={t(props.lang, "story/go_to_description")}
+          aria-label={t(props.lang, "published_content/go_to_description")}
         >
           <svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
             <path
@@ -115,7 +109,7 @@ const wordCount = props.wordCount?.toString();
         <button
           data-dark-mode
           class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
-          aria-label={t(props.lang, "story/toggle_dark_mode")}
+          aria-label={t(props.lang, "published_content/toggle_dark_mode")}
         >
           <svg viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden="true">
             <path
@@ -136,7 +130,7 @@ const wordCount = props.wordCount?.toString();
       data-pagefind-meta="type:story"
     >
       {
-        prev || next ? (
+        (prev && !prev.data.isDraft) || (next && !next.data.isDraft) ? (
           <div class="print:hidden">
             <div id="story-nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2">
               {prev ? (
@@ -170,7 +164,11 @@ const wordCount = props.wordCount?.toString();
           </div>
         ) : null
       }
-      <h1 id="story-title" class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100">
+      <h1
+        id="story-title"
+        class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100"
+        aria-label={t(props.lang, "story/title_aria_label")}
+      >
         {props.title}
       </h1>
       <section
@@ -180,13 +178,6 @@ const wordCount = props.wordCount?.toString();
         <Authors lang={props.lang}>
           {authorsList.map((author) => <UserComponent user={author} lang={props.lang} />)}
         </Authors>
-        {
-          props.isDraft ? (
-            <p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
-              {t(props.lang, "story/draft_warning")}
-            </p>
-          ) : null
-        }
         {
           requestersList && (
             <Requesters lang={props.lang}>
@@ -205,6 +196,13 @@ const wordCount = props.wordCount?.toString();
             </Commissioners>
           )
         }
+        {
+          props.isDraft ? (
+            <p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
+              {t(props.lang, "published_content/draft_warning")}
+            </p>
+          ) : null
+        }
         <div id="content-warning">
           <p>
             {t(props.lang, "story/warnings", wordCount, props.contentWarning)}
@@ -218,7 +216,7 @@ const wordCount = props.wordCount?.toString();
             <img
               loading="eager"
               src={thumbnail.src}
-              alt={`Cover art for ${props.shortTitle || props.title}`}
+              alt={t(props.lang, "published_content/cover_art_alt", props.shortTitle || props.title)}
               width={props.thumbnailWidth}
               height={props.thumbnailHeight}
               class="mx-auto my-5 shadow-lg"
@@ -227,7 +225,7 @@ const wordCount = props.wordCount?.toString();
         ) : null
       }
       <hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
-      <article id="story" class="pr-1 font-serif">
+      <article id="story" class="pr-1 font-serif" aria-label={t(props.lang, "story/article_aria_label")}>
         <Prose>
           <slot />
         </Prose>
@@ -239,28 +237,26 @@ const wordCount = props.wordCount?.toString();
             id="draft-warning-bottom"
             class="py-2 text-center font-serif text-2xl font-semibold not-italic text-red-600"
           >
-            {t(props.lang, "story/draft_warning")}
+            {t(props.lang, "published_content/draft_warning")}
           </p>
         ) : props.pubDate ? (
           <p
             id="publish-date"
             class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
-            aria-label="Publish date"
-            aria-description={props.pubDate.toLocaleDateString("en-US", {
-              month: "long",
-              day: "numeric",
-              year: "numeric",
-            })}
+            aria-label={t(props.lang, "published_content/publish_date_aria_label")}
+            aria-description={
+              t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
+            }
             data-pagefind-index-attrs="aria-description"
             data-pagefind-meta={`date:${props.pubDate.toISOString().slice(0, 10)}`}
           >
-            {t(props.lang, "story/publish_date", props.pubDate.toISOString().slice(0, 10))}
+            {t(props.lang, "published_content/publish_date", props.pubDate)}
           </p>
         ) : null
       }
       <section id="description" class="px-2 font-serif" aria-describedby="title-description">
         <h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
-          {t(props.lang, "story/description")}
+          {t(props.lang, "published_content/description")}
         </h2>
         <Prose>
           <Markdown of={props.description} />
@@ -292,7 +288,7 @@ const wordCount = props.wordCount?.toString();
             ><path
               d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
             ></path></svg
-          ><span>{t(props.lang, "story/to_top")}</span></a
+          ><span>{t(props.lang, "published_content/to_top")}</span></a
         >
       </div>
       {
@@ -335,13 +331,31 @@ const wordCount = props.wordCount?.toString();
         relatedStories.length > 0 ? (
           <section id="related" aria-describedby="title-related" class="my-5">
             <h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
-              Related stories
+              {t(props.lang, "published_content/related_stories")}
             </h2>
             <Prose>
               <ul>
-                {relatedStories.map((stories) => (
+                {relatedStories.map((story) => (
                   <li>
-                    <a href={`/stories/${stories.slug}`}>{stories.data.title}</a>
+                    <a href={`/stories/${story.slug}`}>{story.data.title}</a>
+                  </li>
+                ))}
+              </ul>
+            </Prose>
+          </section>
+        ) : null
+      }
+      {
+        relatedGames.length > 0 ? (
+          <section id="related" aria-describedby="title-related" class="my-5">
+            <h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
+              {t(props.lang, "published_content/related_games")}
+            </h2>
+            <Prose>
+              <ul>
+                {relatedGames.map((game) => (
+                  <li>
+                    <a href={`/games/${game.slug}`}>{game.data.title}</a>
                   </li>
                 ))}
               </ul>
@@ -353,7 +367,7 @@ const wordCount = props.wordCount?.toString();
         tags.length > 0 ? (
           <section id="tags" aria-describedby="title-tags" class="my-5">
             <h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
-              {t(props.lang, "story/tags")}
+              {t(props.lang, "published_content/tags")}
             </h2>
             <ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
               {tags.map(({ id, name }) => (
@@ -367,16 +381,12 @@ const wordCount = props.wordCount?.toString();
           </section>
         ) : null
       }
-      <MastodonComments
-        instance={props.posts.mastodon?.instance}
-        user={props.posts.mastodon?.user}
-        postId={props.posts.mastodon?.postId}
-      />
+      {props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
     </main>
     <div class="pt-6 text-center text-xs text-black dark:text-white">
-      <span>{t(props.lang, "story/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
+      <span>{t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
       <a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
-        >{t(props.lang, "story/licenses")}</a
+        >{t(props.lang, "published_content/licenses")}</a
       >
     </div>
   </div>
diff --git a/src/pages/api/export-story/[...slug].ts b/src/pages/api/export-story/[...slug].ts
index f912a15..47eea8e 100644
--- a/src/pages/api/export-story/[...slug].ts
+++ b/src/pages/api/export-story/[...slug].ts
@@ -1,6 +1,6 @@
 import type { APIRoute, GetStaticPaths } from "astro";
-import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro:content";
-import type { Website } from "../../../content/config";
+import { getCollection, type CollectionEntry, getEntries } from "astro:content";
+import type { Lang, Website } from "../../../content/config";
 import { t } from "../../../i18n";
 import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
 import { markdownToBbcode } from "../../../utils/markdown_to_bbcode";
@@ -24,72 +24,8 @@ type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: in
 
 function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website): string {
   const link = user.data.links[website];
-  if (link) {
-    if (typeof link === "string") {
-      switch (website) {
-        case "website":
-          break;
-        case "eka":
-          const ekaMatch = link.match(/^.*\baryion\.com\/g4\/user\/([^\/]+)\/?$/);
-          if (ekaMatch && ekaMatch[1]) {
-            return ekaMatch[1];
-          }
-          break;
-        case "furaffinity":
-          const faMatch = link.match(/^.*\bfuraffinity\.net\/user\/([^\/]+)\/?$/);
-          if (faMatch && faMatch[1]) {
-            return faMatch[1];
-          }
-          break;
-        case "inkbunny":
-          const ibMatch = link.match(/^.*\binkbunny\.net\/([^\/]+)\/?$/);
-          if (ibMatch && ibMatch[1]) {
-            return ibMatch[1];
-          }
-          break;
-        case "sofurry":
-          const sfMatch = link.match(/^(?:https?:\/\/)?([^\.]+).sofurry.com\b.*$/);
-          if (sfMatch && sfMatch[1]) {
-            return sfMatch[1].replaceAll("-", " ");
-          }
-          break;
-        case "weasyl":
-          const weasylMatch = link.match(/^.*\bweasyl\.com\/\~([^\/]+)\/?$/);
-          if (weasylMatch && weasylMatch[1]) {
-            return weasylMatch[1];
-          }
-          break;
-        case "twitter":
-          const twitterMatch = link.match(/^.*(?:\btwitter\.com|\bx\.com)\/@?([^\/]+)\/?$/);
-          if (twitterMatch && twitterMatch[1]) {
-            return twitterMatch[1];
-          }
-          break;
-        case "mastodon":
-          const mastodonMatch = link.match(/^(?:https?\:\/\/)?([^\/]+)\/(?:@|users\/)([^\/]+)\/?$/);
-          if (mastodonMatch && mastodonMatch[1] && mastodonMatch[2]) {
-            return `${mastodonMatch[2]}@${mastodonMatch[1]}`;
-          }
-          break;
-        case "bluesky":
-          const bskyMatch = link.match(/^.*\bbsky\.app\/profile\/([^\/]+)\/?$/);
-          if (bskyMatch && bskyMatch[1]) {
-            return bskyMatch[1];
-          }
-          break;
-        case "itaku":
-          const itakuMatch = link.match(/^.*\bitaku\.ee\/profile\/([^\/]+)\/?$/);
-          if (itakuMatch && itakuMatch[1]) {
-            return itakuMatch[1];
-          }
-          break;
-        default:
-          let _: never = website;
-          throw new Error(`Unhandled website "${website}"`);
-      }
-    } else {
-      return link[1].replace(/^@/, "");
-    }
+  if (link?.username) {
+    return link.username;
   }
   throw new Error(`Cannot get "${website}" username for user "${user.id}"`);
 }
@@ -99,20 +35,21 @@ function isPreferredWebsite(user: CollectionEntry<"users">, website: Website): b
   return !preferredLink || preferredLink == website;
 }
 
-function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteName): string {
+function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteName, lang: Lang): string {
+  const { links, preferredLink } = user.data;
   switch (website) {
     case "eka":
-      if ("eka" in user.data.links) {
+      if ("eka" in links) {
         return `:icon${getUsernameForWebsite(user, "eka")}:`;
       }
       break;
     case "furaffinity":
-      if ("furaffinity" in user.data.links) {
+      if ("furaffinity" in links) {
         return `:icon${getUsernameForWebsite(user, "furaffinity")}:`;
       }
       break;
     case "weasyl":
-      if ("weasyl" in user.data.links) {
+      if ("weasyl" in links) {
         return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
       } else if (isPreferredWebsite(user, "furaffinity")) {
         return `<fa:${getUsernameForWebsite(user, "furaffinity").replaceAll("_", "")}>`;
@@ -123,7 +60,7 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
       }
       break;
     case "inkbunny":
-      if ("inkbunny" in user.data.links) {
+      if ("inkbunny" in links) {
         return `[iconname]${getUsernameForWebsite(user, "inkbunny")}[/iconname]`;
       } else if (isPreferredWebsite(user, "furaffinity")) {
         return `[fa]${getUsernameForWebsite(user, "furaffinity")}[/fa]`;
@@ -134,7 +71,7 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
       }
       break;
     case "sofurry":
-      if ("sofurry" in user.data.links) {
+      if ("sofurry" in links) {
         return `:icon${getUsernameForWebsite(user, "sofurry")}:`;
       } else if (isPreferredWebsite(user, "furaffinity")) {
         return `fa!${getUsernameForWebsite(user, "furaffinity")}`;
@@ -143,11 +80,12 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
       }
       break;
     default:
-      throw new Error(`Unhandled ExportWebsite "${website}"`);
+      const unknown: never = website;
+      throw new Error(`Unhandled export website "${unknown}"`);
   }
-  if (user.data.preferredLink) {
-    const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string];
-    return `[${user.data.name}](${typeof preferredLink === "string" ? preferredLink : preferredLink[0]})`;
+  if (preferredLink) {
+    const preferred = links[preferredLink]!;
+    return `[${getUsernameForLang(user, lang)}](${preferred.link})`;
   }
   throw new Error(
     `No matching "${website}" link for user "${user.id}" (consider setting their "preferredLink" property)`,
@@ -182,14 +120,14 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
   const description = Object.fromEntries(
     WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => {
       const u = (user: CollectionEntry<"users">) =>
-        isAnonymousUser(user) ? getUsernameForLang(user, lang) : getLinkForUser(user, website);
+        isAnonymousUser(user) ? getUsernameForLang(user, lang) : getLinkForUser(user, website, lang);
       const storyDescription = (
         [
           story.data.description,
           `*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}*`,
           t(
             lang,
-            "export_story/writing",
+            "export_story/authors",
             authorsList.map((author) => u(author)),
           ),
           requestersList &&
@@ -216,12 +154,14 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
           /\[([^\]]+)\]\((\/[^\)]+)\)/g,
           (_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
         );
-      if (exportFormat === "bbcode") {
-        return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n")];
-      } else if (exportFormat === "markdown") {
-        return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
-      } else {
-        throw new Error(`Unhandled export format "${exportFormat}"`);
+      switch (exportFormat) {
+        case "bbcode":
+          return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n")];
+        case "markdown":
+          return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
+        default:
+          const unknown: never = exportFormat;
+          throw new Error(`Unknown export format "${unknown}"`);
       }
     }),
   );
@@ -252,13 +192,12 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
     .replaceAll(/\n\n\n+/g, "\n\n")
     .trim();
 
-  const headers = { "Content-Type": "application/json; charset=utf-8" };
   return new Response(
     JSON.stringify({
       story: storyText,
       description,
       thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null,
     }),
-    { headers },
+    { headers: { "Content-Type": "application/json; charset=utf-8" } },
   );
 };
diff --git a/src/pages/api/healthcheck.ts b/src/pages/api/healthcheck.ts
index cf9fedd..6f2f3c4 100644
--- a/src/pages/api/healthcheck.ts
+++ b/src/pages/api/healthcheck.ts
@@ -1,12 +1,10 @@
 import type { APIRoute } from "astro";
 
-const content = { isAlive: true };
-
-const headers = { "Content-Type": "application/json; charset=utf-8" };
-
 export const GET: APIRoute = () => {
   if (import.meta.env.PROD) {
     return new Response(null, { status: 404 });
   }
-  return new Response(JSON.stringify(content), { headers });
+  return new Response(JSON.stringify({ isAlive: true }), {
+    headers: { "Content-Type": "application/json; charset=utf-8" },
+  });
 };
diff --git a/src/pages/feed.xml.ts b/src/pages/feed.xml.ts
index 76dda05..57785f3 100644
--- a/src/pages/feed.xml.ts
+++ b/src/pages/feed.xml.ts
@@ -24,8 +24,7 @@ function toNoonUTCDate(date: Date) {
 const getLinkForUser = (user: CollectionEntry<"users">, lang: Lang) => {
   const userName = getUsernameForLang(user, lang);
   if (user.data.preferredLink) {
-    const link = user.data.links[user.data.preferredLink]!;
-    return `<a href="${typeof link === "string" ? link : link[0]}">${userName}</a>`;
+    return `<a href="${user.data.links[user.data.preferredLink]!.link}">${userName}</a>`;
   }
   return userName;
 };
diff --git a/src/pages/games/[...slug].astro b/src/pages/games/[...slug].astro
index 7c12085..4e3c03b 100644
--- a/src/pages/games/[...slug].astro
+++ b/src/pages/games/[...slug].astro
@@ -18,17 +18,6 @@ export const getStaticPaths: GetStaticPaths = async () => {
 };
 
 const game = Astro.props;
-if (!game.data.isDraft) {
-  if (!game.data.pubDate) {
-    throw new Error(`Missing "pubDate" for published game ${game.data.title} ("${game.slug}")`);
-  }
-  if (!game.data.thumbnail) {
-    throw new Error(`Missing "thumbnail" for published game ${game.data.title} ("${game.slug}")`);
-  }
-  if (game.data.tags.length == 0) {
-    throw new Error(`Missing "tags" for published game ${game.data.title} ("${game.slug}")`);
-  }
-}
 const { Content } = await game.render();
 ---
 
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 2d35136..b70bd29 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,5 +1,5 @@
 ---
-import { type CollectionEntry, getCollection, type CollectionKey } from "astro:content";
+import { type CollectionEntry, type CollectionKey, getCollection } from "astro:content";
 import { Image } from "astro:assets";
 import GalleryLayout from "../layouts/GalleryLayout.astro";
 import { t } from "../i18n";
diff --git a/src/pages/stories/[...slug].astro b/src/pages/stories/[...slug].astro
index 39d5ab0..2f58526 100644
--- a/src/pages/stories/[...slug].astro
+++ b/src/pages/stories/[...slug].astro
@@ -20,21 +20,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
 
 const story = Astro.props;
 const readingTime = getReadingTime(story.body);
-if (!story.data.isDraft) {
-  if (!story.data.wordCount) {
-    throw new Error(`Missing "wordCount" for published story ${story.data.title} ("${story.slug}")`);
-  }
-  if (!story.data.pubDate) {
-    throw new Error(`Missing "pubDate" for published story ${story.data.title} ("${story.slug}")`);
-  }
-  if (!story.data.thumbnail) {
-    throw new Error(`Missing "thumbnail" for published story ${story.data.title} ("${story.slug}")`);
-  }
-  if (story.data.tags.length == 0) {
-    throw new Error(`Missing "tags" for published story ${story.data.title} ("${story.slug}")`);
-  }
-}
-if (story.data.wordCount && Math.abs(story.data.wordCount - readingTime.words) >= 135) {
+if (story.data.wordCount && Math.abs(story.data.wordCount - readingTime.words) >= 150) {
   console.warn(
     `"wordCount" differs greatly from actual word count for published story ${story.data.title} ("${story.slug}") ` +
       `(expected ~${story.data.wordCount}, found ${readingTime.words})`,
diff --git a/src/pages/tags.astro b/src/pages/tags.astro
index 32d1e6e..8c4577e 100644
--- a/src/pages/tags.astro
+++ b/src/pages/tags.astro
@@ -10,65 +10,53 @@ interface Tag {
   description?: string;
 }
 
-const [stories, games, tagCategories] = await Promise.all([
+const [stories, games, tagCategories, seriesCollection] = await Promise.all([
   getCollection("stories"),
   getCollection("games"),
   getCollection("tag-categories"),
+  getCollection("series"),
 ]);
-const tagsSet = new Set<string>();
+const uncategorizedTagsSet = new Set<string>();
 const draftOnlyTagsSet = new Set<string>();
-const seriesCollection = await getCollection("series");
-// Add tags from non-drafts to set; then, add tags only from drafts to separate set
-[stories, games]
-  .flat()
-  .sort((a, b) => (a.data.isDraft ? 1 : b.data.isDraft ? -1 : 0))
-  .forEach((value) => {
-    if (value.data.isDraft) {
-      value.data.tags.forEach((tag) => {
-        if (!tagsSet.has(tag)) {
-          draftOnlyTagsSet.add(tag);
-        }
-      });
-    } else {
-      value.data.tags.forEach((tag) => {
-        tagsSet.add(tag);
-      });
-    }
-  });
+[stories, games].flat().forEach(({ data: { isDraft, tags } }) => {
+  if (isDraft) {
+    tags.forEach((tag) => draftOnlyTagsSet.add(tag));
+  } else {
+    tags.forEach((tag) => uncategorizedTagsSet.add(tag));
+  }
+});
+// Tags from published content shouldn't be included in drafts-only set
+uncategorizedTagsSet.forEach((tag) => draftOnlyTagsSet.delete(tag));
 
-const uncategorizedTagsSet = new Set(tagsSet);
+const uniqueSlugs = new Set<string>();
 const categorizedTags = tagCategories
   .sort((a, b) => {
     if (a.data.index == b.data.index) {
-      throw new Error(
-        `Found tag categories with same index value ${a.data.index} ("${a.data.name}", "${b.data.name}")`,
-      );
+      throw new Error(`Found tag categories with same index value ${a.data.index} ("${a.id}", "${b.id}")`);
     }
     return a.data.index - b.data.index;
   })
   .map((category) => {
     const tagList = category.data.tags.map<Tag>(({ name, description }) => {
       description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ");
-      const tag = typeof name === "string" ? name : name["eng"];
-      const id = slug(tag);
-      return { id, name: tag, description };
+      const tag = typeof name === "string" ? name : name.en;
+      return { id: slug(tag), name: tag, description };
     });
-    tagList.forEach(({ name }, index) => {
-      if (index !== tagList.findLastIndex(({ name: otherTag }) => name == otherTag)) {
-        throw new Error(`Duplicated tag "${name}" found in multiple tag-categories lists`);
+    tagList.forEach(({ id, name }) => {
+      if (uniqueSlugs.has(id)) {
+        throw new Error(`Duplicated tag "${name}" found in multiple tag-categories entries`);
       }
+      uniqueSlugs.add(id);
     });
     return {
       name: category.data.name,
-      id: category.id,
+      id: slug(category.data.name),
       tags: tagList.filter(({ name }) => {
         if (draftOnlyTagsSet.has(name)) {
-          console.log(`Omitting draft-only tag "${name}"`);
+          // console.log(`Omitting draft-only tag "${name}"`);
           return false;
         }
-        if (tagsSet.has(name)) {
-          uncategorizedTagsSet.delete(name);
-        }
+        uncategorizedTagsSet.delete(name);
         return true;
       }),
     };
diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro
index dfee5fb..67278cb 100644
--- a/src/pages/tags/[slug].astro
+++ b/src/pages/tags/[slug].astro
@@ -1,7 +1,7 @@
 ---
 import type { GetStaticPaths } from "astro";
 import { Image } from "astro:assets";
-import { type CollectionEntry, getCollection, type CollectionKey } from "astro:content";
+import { type CollectionEntry, type CollectionKey, getCollection } from "astro:content";
 import { Markdown } from "@astropub/md";
 import { slug } from "github-slugger";
 import GalleryLayout from "../../layouts/GalleryLayout.astro";
@@ -45,24 +45,22 @@ export const getStaticPaths: GetStaticPaths = async () => {
   const tagDescriptions = tagCategories.reduce(
     (acc, category) => {
       category.data.tags.forEach(({ name, description, related }) => {
-        if (related) {
-          related = related.filter((relatedTag) => {
-            if (relatedTag == name) {
-              console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
-              return false;
-            }
-            if (!tags.has(relatedTag)) {
-              console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
-              return false;
-            }
-            return true;
-          });
-        }
-        const key = typeof name === "string" ? name : name["eng"];
+        related = related.filter((relatedTag) => {
+          if (relatedTag == name) {
+            console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
+            return false;
+          }
+          if (!tags.has(relatedTag)) {
+            console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
+            return false;
+          }
+          return true;
+        });
+        const key = typeof name === "string" ? name : name.en;
         if (key in acc) {
           throw new Error(`Duplicated tag "${key}" found in multiple tag-categories lists`);
         }
-        acc[key] = { description, related };
+        acc[key] = { description, related: related.length > 0 ? related : undefined };
       });
       return acc;
     },
diff --git a/src/utils/get_username_for_lang.ts b/src/utils/get_username_for_lang.ts
index c422cc1..f2cace8 100644
--- a/src/utils/get_username_for_lang.ts
+++ b/src/utils/get_username_for_lang.ts
@@ -2,14 +2,15 @@ import type { CollectionEntry } from "astro:content";
 import { DEFAULT_LANG, type Lang } from "../content/config";
 
 export function getUsernameForLang(user: CollectionEntry<"users">, lang: Lang): string {
-  if (user.data.lang) {
-    if (user.data.lang[lang]) {
-      return user.data.lang[lang];
+  const { name } = user.data;
+  if (typeof name === "object") {
+    if (name[lang]) {
+      return name[lang];
     }
-    throw new Error(`No "${lang}" translation for username "${user.data.name}" ("${user.id}")`);
+    throw new Error(`No "${lang}" translation for user "${user.id}"`);
   }
   if (lang !== DEFAULT_LANG) {
-    console.warn(`User "${user.data.name}" ("${user.id}") has no "lang" property for a "${lang}" translation`);
+    console.warn(`Name "${name}" for user "${user.id}" isn't translated to "${lang}"; using default`);
   }
-  return user.data.name;
+  return name;
 }
diff --git a/src/utils/is_anonymous_user.ts b/src/utils/is_anonymous_user.ts
index 070adb0..57ed851 100644
--- a/src/utils/is_anonymous_user.ts
+++ b/src/utils/is_anonymous_user.ts
@@ -1,6 +1,6 @@
 import type { CollectionEntry } from "astro:content";
-import { ANONYMOUS_USER_ID as ID } from "../content/config";
+import { ANONYMOUS_USER_ID } from "../content/config";
 
-const ANONYMOUS_USER_ID: CollectionEntry<"users">["id"] = ID;
+const ID: CollectionEntry<"users">["id"] = ANONYMOUS_USER_ID;
 
-export const isAnonymousUser = (user: CollectionEntry<"users">) => user.id == ANONYMOUS_USER_ID;
+export const isAnonymousUser = (user: CollectionEntry<"users">) => user.id === ID;
diff --git a/tsconfig.json b/tsconfig.json
index bcbf8b5..da42df9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,3 +1,4 @@
 {
-  "extends": "astro/tsconfigs/strict"
+  "extends": "astro/tsconfigs/strict",
+  "exclude": ["dist"]
 }