From 808f565e59407b7038c99a7dbe4386a9567577e5 Mon Sep 17 00:00:00 2001
From: Bad Manners <me@badmanners.xyz>
Date: Sun, 24 Mar 2024 22:48:07 -0300
Subject: [PATCH] Add `export-story` script and draft for Tiny Accident

---
 README.md                                     |   9 +-
 package-lock.json                             | 239 +++++++++++++++++-
 package.json                                  |   8 +-
 scripts/export-story.ts                       | 127 ++++++++++
 src/assets/thumbnails/bm_18_tiny_accident.png | Bin 0 -> 21808 bytes
 src/components/Authors.astro                  |   2 +-
 src/components/UserComponent.astro            |   8 +-
 src/content/stories/taken-in.md               |   1 +
 src/content/stories/tiny-accident.md          | 169 +++++++++++++
 src/content/users/anonymous.yaml              |   3 +
 src/layouts/StoryLayout.astro                 |   4 +-
 src/pages/healthcheck.ts                      |  10 +
 .../{ => description}/[website]/[...slug].ts  |   4 +-
 src/pages/stories/export/story/[...slug].ts   |  72 ++++++
 .../stories/export/thumbnail/[...slug].ts     |  27 ++
 src/pages/tags/digestion.astro                |  10 +
 16 files changed, 678 insertions(+), 15 deletions(-)
 create mode 100644 scripts/export-story.ts
 create mode 100644 src/assets/thumbnails/bm_18_tiny_accident.png
 create mode 100644 src/content/stories/tiny-accident.md
 create mode 100644 src/pages/healthcheck.ts
 rename src/pages/stories/export/{ => description}/[website]/[...slug].ts (98%)
 create mode 100644 src/pages/stories/export/story/[...slug].ts
 create mode 100644 src/pages/stories/export/thumbnail/[...slug].ts
 create mode 100644 src/pages/tags/digestion.astro

diff --git a/README.md b/README.md
index 70134df..3ff5923 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,18 @@
 # gallery.badmanners.xyz
 
+## Development
+
+### Export story for upload
+
+```bash
+npm run export-story -- --output-dir ~/Documents/TO_UPLOAD slug-for-selected-story
+```
+
 ## Deployment
 
 ### Remote
 
 ```bash
-npm install
 npm run build
 scp -r ./dist/* my-ssh-server:./gallery.badmanners.xyz/
 ```
diff --git a/package-lock.json b/package-lock.json
index 770a572..2eb806b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,9 +23,12 @@
         "typescript": "^5.4.2"
       },
       "devDependencies": {
+        "commander": "^12.0.0",
+        "fetch-retry": "^6.0.0",
         "prettier": "^3.2.5",
         "prettier-plugin-astro": "^0.13.0",
-        "prettier-plugin-tailwindcss": "^0.5.12"
+        "prettier-plugin-tailwindcss": "^0.5.12",
+        "tsx": "^4.7.1"
       }
     },
     "node_modules/@alloc/quick-lru": {
@@ -549,6 +552,30 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@cspotcode/source-map-support": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+      "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/trace-mapping": "0.3.9"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.9",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+      "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.0.3",
+        "@jridgewell/sourcemap-codec": "^1.4.10"
+      }
+    },
     "node_modules/@emmetio/abbreviation": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz",
@@ -1223,6 +1250,34 @@
         "node": ">=4"
       }
     },
+    "node_modules/@tsconfig/node10": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.10.tgz",
+      "integrity": "sha512-PiaIWIoPvO6qm6t114ropMCagj6YAF24j9OkCA2mJDXFnlionEwhsBCJ8yek4aib575BI3OkART/90WsgHgLWw==",
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/@tsconfig/node12": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+      "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/@tsconfig/node14": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+      "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/@tsconfig/node16": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+      "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+      "optional": true,
+      "peer": true
+    },
     "node_modules/@types/babel__core": {
       "version": "7.20.5",
       "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1312,6 +1367,16 @@
       "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
       "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
     },
+    "node_modules/@types/node": {
+      "version": "20.11.30",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
+      "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
     "node_modules/@types/unist": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
@@ -1433,6 +1498,16 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/acorn-walk": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
+      "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/ansi-align": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@@ -2225,11 +2300,12 @@
       }
     },
     "node_modules/commander": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
-      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz",
+      "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==",
+      "dev": true,
       "engines": {
-        "node": ">= 6"
+        "node": ">=18"
       }
     },
     "node_modules/common-ancestor-path": {
@@ -2250,6 +2326,13 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/create-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+      "optional": true,
+      "peer": true
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -2636,6 +2719,12 @@
         "reusify": "^1.0.4"
       }
     },
+    "node_modules/fetch-retry": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz",
+      "integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==",
+      "dev": true
+    },
     "node_modules/fill-range": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -2771,6 +2860,18 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/get-tsconfig": {
+      "version": "4.7.3",
+      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz",
+      "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==",
+      "dev": true,
+      "dependencies": {
+        "resolve-pkg-maps": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+      }
+    },
     "node_modules/github-from-package": {
       "version": "0.0.0",
       "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -3526,6 +3627,13 @@
         "node": ">=12"
       }
     },
+    "node_modules/make-error": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+      "optional": true,
+      "peer": true
+    },
     "node_modules/markdown-table": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz",
@@ -5504,6 +5612,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/resolve-pkg-maps": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+      "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+      }
+    },
     "node_modules/restore-cursor": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
@@ -6374,6 +6491,14 @@
         "node": ">=16 || 14 >=14.17"
       }
     },
+    "node_modules/sucrase/node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/suf-log": {
       "version": "2.5.3",
       "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz",
@@ -6546,6 +6671,67 @@
       "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
       "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
     },
+    "node_modules/ts-node": {
+      "version": "10.9.2",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+      "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@cspotcode/source-map-support": "^0.8.0",
+        "@tsconfig/node10": "^1.0.7",
+        "@tsconfig/node12": "^1.0.7",
+        "@tsconfig/node14": "^1.0.0",
+        "@tsconfig/node16": "^1.0.2",
+        "acorn": "^8.4.1",
+        "acorn-walk": "^8.1.1",
+        "arg": "^4.1.0",
+        "create-require": "^1.1.0",
+        "diff": "^4.0.1",
+        "make-error": "^1.1.1",
+        "v8-compile-cache-lib": "^3.0.1",
+        "yn": "3.1.1"
+      },
+      "bin": {
+        "ts-node": "dist/bin.js",
+        "ts-node-cwd": "dist/bin-cwd.js",
+        "ts-node-esm": "dist/bin-esm.js",
+        "ts-node-script": "dist/bin-script.js",
+        "ts-node-transpile-only": "dist/bin-transpile.js",
+        "ts-script": "dist/bin-script-deprecated.js"
+      },
+      "peerDependencies": {
+        "@swc/core": ">=1.2.50",
+        "@swc/wasm": ">=1.2.50",
+        "@types/node": "*",
+        "typescript": ">=2.7"
+      },
+      "peerDependenciesMeta": {
+        "@swc/core": {
+          "optional": true
+        },
+        "@swc/wasm": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ts-node/node_modules/arg": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/ts-node/node_modules/diff": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
     "node_modules/tsconfck": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz",
@@ -6565,6 +6751,25 @@
         }
       }
     },
+    "node_modules/tsx": {
+      "version": "4.7.1",
+      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.1.tgz",
+      "integrity": "sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==",
+      "dev": true,
+      "dependencies": {
+        "esbuild": "~0.19.10",
+        "get-tsconfig": "^4.7.2"
+      },
+      "bin": {
+        "tsx": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      }
+    },
     "node_modules/tunnel-agent": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -6613,6 +6818,13 @@
         "semver": "^7.3.8"
       }
     },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "optional": true,
+      "peer": true
+    },
     "node_modules/unherit": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/unherit/-/unherit-3.0.1.tgz",
@@ -6798,6 +7010,13 @@
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
     },
+    "node_modules/v8-compile-cache-lib": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+      "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+      "optional": true,
+      "peer": true
+    },
     "node_modules/vfile": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz",
@@ -7393,6 +7612,16 @@
         "node": ">=8"
       }
     },
+    "node_modules/yn": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/yocto-queue": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
diff --git a/package.json b/package.json
index 08e972b..214e963 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,8 @@
     "build": "astro check && astro build",
     "preview": "astro preview",
     "astro": "astro",
-    "prettier": "prettier . --write"
+    "prettier": "prettier . --write",
+    "export-story": "tsx scripts/export-story.ts"
   },
   "dependencies": {
     "@astrojs/check": "^0.5.9",
@@ -26,8 +27,11 @@
     "typescript": "^5.4.2"
   },
   "devDependencies": {
+    "commander": "^12.0.0",
+    "fetch-retry": "^6.0.0",
     "prettier": "^3.2.5",
     "prettier-plugin-astro": "^0.13.0",
-    "prettier-plugin-tailwindcss": "^0.5.12"
+    "prettier-plugin-tailwindcss": "^0.5.12",
+    "tsx": "^4.7.1"
   }
 }
diff --git a/scripts/export-story.ts b/scripts/export-story.ts
new file mode 100644
index 0000000..9587245
--- /dev/null
+++ b/scripts/export-story.ts
@@ -0,0 +1,127 @@
+import { type ChildProcess, exec, execSync } from "node:child_process";
+import { readdir, mkdir, mkdtemp, writeFile, readFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join as pathJoin, normalize } from "node:path";
+import { setTimeout } from "node:timers/promises";
+import { program } from "commander";
+import fetchRetryWrapper from "fetch-retry";
+
+function getRTFStyles(rtfSource: string) {
+  const matches = rtfSource.matchAll(
+    /\\s(\d+)(?:\\sbasedon\d+)?\\snext\d+((?:\\[a-z0-9]+ ?)+)(?: ([A-Z][a-zA-Z ]*));/g,
+  );
+  let hasMatches = false;
+  const rtfStyles: Record<number | string, string> = {};
+  for (const [_, styleNumber, partialRTFStyle, styleName] of matches) {
+    hasMatches = true;
+    const rtfStyle = `\\s${styleNumber}${partialRTFStyle}`;
+    rtfStyles[Number.parseInt(styleNumber)] = rtfStyle;
+    rtfStyles[styleName] = rtfStyle;
+  }
+  if (!hasMatches) {
+    throw new Error("Couldn't find valid RTF styles!");
+  }
+  return rtfStyles;
+}
+
+const fetchRetry = fetchRetryWrapper(global.fetch);
+
+async function exportStory(slug: string, options: { outputDir: string }) {
+  /* Check that outputDir is valid */
+  const outputDir = normalize(options.outputDir);
+  let files: string[];
+  try {
+    files = await readdir(outputDir);
+  } catch {
+    files = [];
+    console.log(`Created directory at ${await mkdir(outputDir, { recursive: true })}`);
+  }
+  if (files.length > 1) {
+    console.error(`Directory ${outputDir} is not empty!`);
+    process.exit(1);
+  }
+  /* Check if Astro development server needs to be spawned */
+  const healthcheckURL = `http://localhost:4321/healthcheck`;
+  let devServerProcess: ChildProcess | null = null;
+  try {
+    await fetchRetry(healthcheckURL, { retries: 3, retryDelay: 1000 });
+  } catch {
+    /* Spawn Astro dev server */
+    console.log("Starting Astro development server...");
+    devServerProcess = exec("npm run dev");
+    await setTimeout(2000);
+    try {
+      await fetchRetry(healthcheckURL, { retries: 5, retryDelay: 2000 });
+    } catch {
+      console.error("Astro dev server didn't respond in time!");
+      devServerProcess && devServerProcess.kill();
+      devServerProcess = null;
+      process.exit(1);
+    }
+  }
+  /* Get data (story, thumbnail, descriptions) from Astro development server */
+  let storyText = "";
+  try {
+    console.log("Getting data from Astro...");
+
+    const exportStoryURL = `http://localhost:4321/stories/export/story/${slug}`;
+    const exportThumbnailURL = `http://localhost:4321/stories/export/thumbnail/${slug}`;
+    const exportDescriptionURLs = (website: string) =>
+      `http://localhost:4321/stories/export/description/${website}/${slug}`;
+
+    await Promise.all(
+      ["eka", "furaffinity", "inkbunny", "sofurry", "weasyl"].map(async (website) => {
+        const description = await fetch(exportDescriptionURLs(website));
+        if (!description.ok) {
+          throw new Error(`Failed to get description for "${website}"`);
+        }
+        const descriptionExt = description.headers.get("Content-Type")?.startsWith("text/markdown") ? "md" : "txt";
+        return await writeFile(
+          pathJoin(outputDir, `description_${website}.${descriptionExt}`),
+          await description.text(),
+        );
+      }),
+    );
+    const thumbnail = await fetch(exportThumbnailURL);
+    if (!thumbnail.ok) {
+      throw new Error("Failed to get thumbnail");
+    }
+    const thumbnailExt = thumbnail.headers.get("Content-Type")?.startsWith("image/png") ? "png" : "jpg";
+    writeFile(pathJoin(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer()));
+    const story = await fetch(exportStoryURL);
+    if (!story.ok) {
+      throw new Error("Failed to get story");
+    }
+    storyText = await story.text();
+    writeFile(pathJoin(outputDir, `${slug}.txt`), storyText);
+  } finally {
+    if (devServerProcess) {
+      console.log("Shutting down the Astro development server...");
+      devServerProcess.kill();
+      devServerProcess = null;
+    }
+  }
+
+  /* Parse story into output formats */
+  console.log("Parsing story into output formats...");
+  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")}`);
+  const rtfText = await readFile(pathJoin(tempDir, "temp.rtf"), "utf-8");
+  const rtfStyles = getRTFStyles(rtfText);
+  await writeFile(
+    pathJoin(outputDir, `${slug}.rtf`),
+    rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
+  );
+  console.log("Success!");
+  return;
+}
+
+await program
+  .name("export-story")
+  .description("Generate and export formatted upload files for a story")
+  .argument("<story-slug>", `Slug portion of the story's URL (eg. "the-lost-of-the-marshes/chapter-1")`)
+  .option("-o, --output-dir <directory>", `Empty or inexistent directory path to export files to`)
+  .action(exportStory)
+  .parseAsync();
diff --git a/src/assets/thumbnails/bm_18_tiny_accident.png b/src/assets/thumbnails/bm_18_tiny_accident.png
new file mode 100644
index 0000000000000000000000000000000000000000..b23b2c0de67cba556a16ee88fafaf62d5c50518a
GIT binary patch
literal 21808
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

literal 0
HcmV?d00001

diff --git a/src/components/Authors.astro b/src/components/Authors.astro
index f19ebde..779efbe 100644
--- a/src/components/Authors.astro
+++ b/src/components/Authors.astro
@@ -39,7 +39,7 @@ const authorsArray = [authors].flat();
       {lang === "tok" &&
         (authorsArray.length > 1 ? (
           <span>
-            lipu ni li tan ni:{" "}
+            lipu ni li tan jan ni:{" "}
             {authorsArray.slice(0, authorsArray.length - 1).map((author) => (
               <Fragment>
                 <UserComponent lang="tok" user={author} />
diff --git a/src/components/UserComponent.astro b/src/components/UserComponent.astro
index 8df4846..767f46b 100644
--- a/src/components/UserComponent.astro
+++ b/src/components/UserComponent.astro
@@ -1,16 +1,20 @@
 ---
 import { type CollectionEntry } from "astro:content";
 import { type Lang } from "../content/config";
+import { getEntry } from "astro:content";
 
 type Props = {
   lang: Lang;
   user: CollectionEntry<"users">;
 };
 
-const { user, lang } = Astro.props;
+let { user, lang } = Astro.props;
+if (user.data.isAnonymous) {
+  user = await getEntry("users", "anonymous");
+}
 const username = user.data.nameLang[lang] || user.data.name;
 let link: string | null = null;
-if (!user.data.isAnonymous && user.data.preferredLink) {
+if (user.data.preferredLink) {
   if (user.data.preferredLink in user.data.links) {
     const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string];
     if (typeof preferredLink === "string") {
diff --git a/src/content/stories/taken-in.md b/src/content/stories/taken-in.md
index f4d16d0..188d582 100644
--- a/src/content/stories/taken-in.md
+++ b/src/content/stories/taken-in.md
@@ -17,6 +17,7 @@ tags:
   - willing predator
   - unwilling prey
   - oral vore
+  - same size
   - full tour
   - point of view
 copyrightedCharacters:
diff --git a/src/content/stories/tiny-accident.md b/src/content/stories/tiny-accident.md
new file mode 100644
index 0000000..065140e
--- /dev/null
+++ b/src/content/stories/tiny-accident.md
@@ -0,0 +1,169 @@
+---
+title: Tiny Accident
+pubDate: 2024-03-25
+isDraft: true
+authors: bad-manners
+wordCount: 2800
+contentWarning: >
+  Contains: Non-fatal oral vore, with unwilling to willing anthro male rat predator, unwilling micro anthro male wolf prey. Also includes implied regurgitation, masturbation, sizeplay, and unaware micro groping.
+thumbnail: /src/assets/thumbnails/bm_18_tiny_accident.png
+description: |
+  Kolo's day at the airship is nearly over, but a tiny stalker will unwittingly make his evening quite eventful...
+
+  Finally got around to finishing a story ever since I worked on my game! I wanna get back into writing more stuff again, and this short story has finally broken my writer's block. My goal is to go back to working on commissions, but I feel I'm not quite in the headspace to tackle them just yet... Nevertheless, I hope you enjoy this!
+tags:
+  - anthro predator
+  - anthro prey
+  - male predator
+  - male prey
+  - unwilling predator
+  - willing predator
+  - unwilling prey
+  - oral vore
+  - micro prey
+  - regurgitation
+  - masturbation
+  - sizeplay
+---
+
+Kolo sauntered back to his private cabin, wearing only flip-flops and a bath towel around his waist. The rat's pink tail swung low, arching up just inches from the floor grating. Another uneventful day on the airship, he thought gladly to himself – and after hopping by the locker room showers when they were almost empty, he was ready to kick back and relax on his bunk bed until the dinner bell rang.
+
+Traversing the narrow corridors of the living area with ease, he approached a reinforced steel door with '418' painted in white – no different than the many other doors in this corridor and the ones above and below. Kolo punched in the code on the keypad, and pushed his brown shoulder against the cold, heavy metal as it opened inward with his weight.
+
+"Jo, you in here?" The rodent called, noticing that the lights were already on.
+
+He checked behind the partition where the bedroom, since Joaquim wasn't in the study room. The whole place was pretty cramped – not much more than 100 square feet total –, so there was nowhere for his roommate to be hiding. Kolo simply shut the reinforced door behind him, taking off his towel as he made his way to the lower bunk bed. He didn't need to feel embarrassed to be naked on his lonesome, after all.
+
+The brown-furred rat flung himself with his back against the squeaky mattress, before taking his portable game console from its wall-mounted charger. He booted up his game to continue from where he left off – but his round ear swiftly twitched.
+
+"Hm? Jo?" Kolo looked around, thinking that he'd heard something close-by. But he couldn't see any signs of life in his vicinity. "...Must be the heating pipes." And he brought his console close to his face again.
+
+Minutes passed, and Kolo was fully immersed in his game, shooting several monsters on his small screen. After a day of hard work, he didn't want to think about building heavy and complex machinery. He just wanted to cool down and enjoy himself – even forgetting that he was stark naked. His lower bits were exposed and, unbeknownst to him, being ogled.
+
+The rat didn't notice the other man on his bed, due to the diminutive size of his visitor. Tiny paws walked on the mattress, too light to make any noise on the creaky springs underneath. The floor felt hard for the micro, shaking with the smallest movements from the rodent focused on his game. The tiny intruder stealthily moved up the bed, ducking to walk underneath one of those large legs that had its knee raised. And past that furry overpass, there was nothing more in the way of the exposed sheath.
+
+But Kolo wasn't aware in the slightest. Too focused on this particular boss that he'd already lost to a few times, his sensitive ears didn't pick up the slightest hint of something scurrying about on his bed – until he felt a gentle brush to his sensitive testicles.
+
+"Eek–!"
+
+The console on his paws nearly jumped all the way to the bottom of the top bunk. He managed to get a grip on it with his left paw instead of clumsily flinging it away – while his right paw darted towards the strange feeling on his furry groins. His first instinct was to crush whatever cockroach had climbed onto his bed, but once reason kicked in, he managed to spare himself from a painful slap to his own balls.
+
+He carefully cupped his testicles, trapping the micro groper between them and his fingers. It definitely wasn't a bug – unless it was a very hairy and warm spider. Kolo could hear a distant voice coming from it, struggling against the paw prying it from Kolo's sac. Its shape was definitely that of an anthro, with arms and legs, head and tail – all of which he was careful to handle. It only made him more confused, and he sat up while putting away his console and bringing his fist closer to his face.
+
+With all of his attention on the micro shifting against his palm, the rat slowly opened his paw. The halogen light of the cabin peeked through his fingers, revealing a familiar rust-red and white lupine figure no taller than his index finger.
+
+"J-JO?!" He shouted, loud enough to make the micro wolf wince in surprise too.
+
+"K-Ko, hey..." Joaquim stammered, bashfully enough that his face was even redder than usual. "I can explain–"
+
+"What are you–! Wh-Why are you–! What the _fuck_ are you doing, being so small?!"
+
+"K-Keep quiet, Ko...! I don't want Jason next door to hear us." The wolf pleaded.
+
+"What? Why would you–" Kolo stopped himself, his brows arching even higher than they already were. "Wait... That fucking shrink ray you had us make for you at Engineering. You told us it was for storage purposes...!"
+
+"Y-Yeah, I, um, made some adjustments..." Joaquim raised his arm towards the pile of clothes at the corner of the room, where the barrel-less gun was sitting on. "I didn't mean to–"
+
+The rodent was quickly losing his patience. "You didn't mean to...what? Be a creepy pervert and sneak your way to my fucking dick?!"
+
+Too nervous to lie his way out of this, the wolf simply looked away from the giant face and closed his lies. "I-I didn't think you'd feel or notice me..."
+
+Kolo's face turned red with anger. "Ohhh you little shit...! Time to give you a fucking lesson you won't forget."
+
+"W-Wait, Ko. I'm sorry–"
+
+Joaquim couldn't apologize in time for the furry floor to shift away from his butt. He thought that he'd fall, but the rat's skillful fingers grabbed him by his scuff. With his heart thumping loudly in his chest, the wolf was left dangling in front of the brown rat – making Kolo realize that not only was he naked, but also erect.
+
+"Tch. You fucking freak. This punishment oughta do it for ya."
+
+Not realizing what he'd meant, Joaquim was surprised when Kolo opened his mouth wide enough –even pushing his tongue out. It was a thrilling sight... One that he wouldn't gaze in horror for long, as he noticed the paw around him approaching that fleshy cave.
+
+"Ko, s-stop it! Shit...!"
+
+Thrash as he might, Joaquim was unable to break free as his digitigrade paws brushed against the wet tongue. He could smell the rat's breath in the warm air surrounding him – not too unpleasant, but definitely unavoidable. To his horror, Kolo's fingers released him, and he began falling towards the back of his roommate's maw. He thought that this was it, and he'd end up being devoured by the rat... Only the jaws clamped shut before he slid too far, squishing and pinning him against the palate in complete darkness.
+
+The rodent tilted his head forward, leaving the wolf mostly horizontal in his maw. He had no intention of swallowing Jo, but to simply teach him a lesson. Being so small, Kolo's tongue easily overwhelmed him, and the rat easily shoved the micro against the inside of his cheek. If Joaquim had any flavor, it was disguised by all that fur giving an odd mouthfeel. And when he noticed a smoother and saltier texture between his legs, Kolo grumbled – even now, the tiny pervert was erect...!
+
+"L-Lemme out! Ack!" The wolf begged, but the tongue dragged him against the back of Kolo's gums towards the other cheek.
+
+Little did he know that his tiny shouts wouldn't even reach the rat's ears. Joaquim's plan had clearly backfired, and he could only ruminate on the choices that led him here. If only he hadn't come up with this idea after accidentally stumbling upon the naked rat the other day, or if only he hadn't pushed his luck by getting so close to that enticing, furry sheath... Now, he was surrounded by flesh and drool, smooshed around by the powerful yet careful muscle, listening to incessant squelches and subtle clicks.
+
+For a couple of minutes, Kolo focused solely on his tiny catch. As long as he didn't think too hard that it was his usually taller roommate being jostled inside of his maw, it was easy to torment Jo without biting him or otherwise hurting him by accident. Actually, Kolo thought to himself, he might even be able to keep him there while playing his game, and let him go for dinnertime. Having to go to the dining hall while smelling like rat breath would teach the wolf never to try this again.
+
+Joaquim, trying his best to grip onto the slippery tastebuds, found a small reprieve from the onslaught once the muscle below him settled down. Maybe Ko was ready to release him. It was about time...! As carefully as he could in the chaotic and soggy darkness, he planted his digitigrade feet on the tonsils for balance, bringing his paws up to try and climb his way out.
+
+However, when the brown rat jerked his head forward – to try and grab his game console again –, Kolo realized too late that his tongue didn't have a good enough grip on the micro. He felt something push uncomfortably against his larynx, and his body reflexively swallowed, sending the obstruction down into his food pipe instead.
+
+"O-Oh, no..." The rodent whispered to himself, desperation slowly settling in. He opened his maw wide, bringing his fingers inside to try and feel for any signs of the wolf. He ended up pushing them too far back, and coughed as he gagged on them. Once he realized that there was nothing out of place, he looked down at his belly. "Ohh, fuck...!"
+
+Once he'd slipped into Kolo's throat, Joaquim started screaming his name – but his roommate also wasn't able to hear as much. Like any piece of food, he was mercilessly squeezed down the esophagus. His slick fur slid easily through the pulsing tunnel, with no way to fight against gravity. It quickly got uncomfortably warm and damp, before he even ended up at his inevitable destination.
+
+Mercifully, the rat's stomach was empty before the rust-red lupine had arrived. Jo was far from filling, and his body had to conform within the rugged walls that greeted him with an acrid smell. The organ was definitely less agreeable than Kolo's mouth, greeting him with a low-pitched whine that made his heart sink and his own stomach twist.
+
+"K-Kolo!" The micro shouted out, fighting for space in the fleshy chamber. "You fucking _swallowed_ me!!"
+
+"Oh shit... I-I accidentally swallowed you..." The rat's voice sounded much deeper as it arrived from every direction.
+
+Joaquim tried his best to punch and kick at the walls restricting his movements. "Y-You motherfucker... Lemme out now!"
+
+Kolo couldn't see anything different under his brown fur – but he definitely felt something tiny wriggle in his insides, unlike anything he'd ever felt before.
+
+"Sh-Shit... You're _really_ inside me..." He told himself, as the situation dawned on him. "I oughta let you out, b-but... Why is this making me _horny_...?"
+
+"It's...wh-what?"
+
+The wolf was suddenly tossed away, being smooshed snout-fist against a soft but slimy wall. Aside from the stomach's groans and a fast heartbeat, Jo could feel another thumping. It was strong enough to continually shake his surroundings, and as its pace increased, it became louder. The stifling air stewing in the organ alone made him nauseated, and the earthquakes only made his head spin harder. As he tried his best to find any balance, Joaquim was glad that neither of them had dinner yet – or his would definitely have joined Kolo's.
+
+Meanwhile, the rat was preoccupied with his own erection. He knew that it was wrong to take pleasure in his roommate's bleak situation – but with no one else to judge him _but_ his roommate, Kolo's guilt only made him hornier. He could feel the subtle movements deep in his gut, where there shouldn't be any. Jo was definitely wiggling in there, and Kolo like he'd finally put the wolf in his place.
+
+As his fully exposed pinkish cock dribbled more pre onto his paw, he tried to imagine what it was like inside. Was the wolf fighting for his life, yelling at Ko to stop...? Or was he enjoying this once-in-a-lifetime experience as well...? Maybe even thrusting his tiny cock – or perhaps his entire tiny body – against the hungry, churning walls?
+
+"Mmmm, fuckk..." The rat slurred his words as his climax got closer. "Now you're just a snack, you little pervert..."
+
+He continued to moan. Kolo wouldn't stop himself from fapping until he'd reached his peak – Jo owed him as much, after what he tried to pull. And a couple of minutes in his sultry stomach wouldn't be too bad...probably. He was definitely moving in there, spreading his gastric folds, in Manners that regular food isn't capable of. But the tiny stimuli only made him hungrier, reminding him that he still had supper to look forward to. The rat clenched his teeth, imagining that he would keep him in there when he went to the dining hall – and no one would be the wiser as he buried the helpless wolf between mouthfuls of dinner...
+
+That thought pushed him past the edge, and he threw his head back while letting out a strained moan. Rope after rope of cum spewed out and onto his fingers and brown fur, and Kolo let go of his cock as the last of his seed began to drivel towards his sheath. He reached for his bath towel, using it to clean the white ooze as he recovered his breath.
+
+Once his head had cleared up from the whole situation, he calmly got up from his bed and walked to his bulky suitcase on the floor. He unlocked it with one paw, while the other gingerly held his belly. Joaquim was nudging against a spot in his stomach, every now and then. Kolo was glad – the last thing he needed was an investigation regarding his missing roommate.
+
+He started dressing up, putting on some casual clothes. The micro's squirming meant that his erection wasn't going anywhere, but he managed to tuck it in his tight underwear and looser pants in such a way that his bulge should've been inconspicuous enough. Similarly, he slipped into shoes and a shirt. The movements inside of his gurgling belly seemed to urge him to go faster, but he didn't feel rushed. He just knew that Jo would be fine for a few extra minutes.
+
+Once he was dressed, Kolo grabbed the shrink ray from the wolf's clothes pile – as well as a pair of his pants. He left the private cabin and locked the door, and as he headed back to the locker room, he whistled a cheerful tune.
+
+\*\*\*
+
+"What took you so long?!" Jo shouted at the rat. Now at his normal size, he was standing completely naked in the showers, his rust-red fur completely covered in a greenish slime.
+
+Kolo sputtered against the back of his paw. "Keep quiet, unless you wanna alert everyone else on this airship. I had to make sure there wasn't anyone else around to see me throwing you up."
+
+"But what about me?! What if something bad had happened?"
+
+"But nothing bad did, now did it?" He waved his finger in a circle. "Your fur looks fine to me. Did it burn or anything?"
+
+"The acids?" The wolf moved a paw to his hip. "N-Not really, no..."
+
+"See? I'm sure you woulda been fine even for a couple of hours."
+
+The wolf couldn't believe the gall of his roommate. He simply watched incredulously at the rat – who not only had swallowed him alive, but even masturbated at his expense...!
+
+"H-Hey", Joaquim called out as the rodent began turning away. "Where are you going?"
+
+"What? Do you need me to watch you shower? I'm going to the hall, it's almost dinnertime." He pointed at a bench. "I left you a pair of trousers here, once you're done."
+
+"But aren't you afraid?"
+
+"Afraid...? Of what?"
+
+"That I'll tell someone what you did to me."
+
+Kolo let out a soft chuckle. "You'll do no such thing. We both know you won't be stupid enough to admit that you stole company property for your own gain."
+
+He raised his paw holding the shrink gun, before confidently spinning it around his finger.
+
+Jo sighed. "A-And what are you going to do with it...?"
+
+"Oh, this? I think I'll be confiscating it – and I'll make sure you won't find it so soon, Jo."
+
+The rat waved his arm as he walked away from the locker room for good. Left behind, the wolf took this moment to look down at his arms, and the fresh stomach slime still dripping from them. He'd really just been inside of Kolo's stomach, huh.
+
+As he turned towards the shower head, Joaquim continued to conceal his hard-on with a paw, and he turned the faucet with the other.
diff --git a/src/content/users/anonymous.yaml b/src/content/users/anonymous.yaml
index b274e77..1c00098 100644
--- a/src/content/users/anonymous.yaml
+++ b/src/content/users/anonymous.yaml
@@ -1,3 +1,6 @@
 name: Anonymous
+nameLang:
+  eng: anonymous
+  tok: jan pi nimi ala
 isAnonymous: true
 links: {}
diff --git a/src/layouts/StoryLayout.astro b/src/layouts/StoryLayout.astro
index da85c4c..a4a3b50 100644
--- a/src/layouts/StoryLayout.astro
+++ b/src/layouts/StoryLayout.astro
@@ -84,7 +84,7 @@ const tags = props.tags.map<[string, string]>((tag) => {
         <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={props.lang === "eng" ? "Go to description" : props.lang === "tok" ? "o tawa e lipu lili" : null}
+          aria-label={props.lang === "eng" ? "Go to description" : props.lang === "tok" ? "o tawa e toki lipu" : null}
         >
           <svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
             <path
@@ -227,7 +227,7 @@ const tags = props.tags.map<[string, string]>((tag) => {
       }
       <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">
-          {props.lang === "eng" ? "Description" : props.lang === "tok" ? "lipu lili" : null}
+          {props.lang === "eng" ? "Description" : props.lang === "tok" ? "toki lipu" : null}
         </h2>
         <Prose>
           <Markdown of={props.description} />
diff --git a/src/pages/healthcheck.ts b/src/pages/healthcheck.ts
new file mode 100644
index 0000000..6f2f3c4
--- /dev/null
+++ b/src/pages/healthcheck.ts
@@ -0,0 +1,10 @@
+import type { APIRoute } from "astro";
+
+export const GET: APIRoute = () => {
+  if (import.meta.env.PROD) {
+    return new Response(null, { status: 404 });
+  }
+  return new Response(JSON.stringify({ isAlive: true }), {
+    headers: { "Content-Type": "application/json; charset=utf-8" },
+  });
+};
diff --git a/src/pages/stories/export/[website]/[...slug].ts b/src/pages/stories/export/description/[website]/[...slug].ts
similarity index 98%
rename from src/pages/stories/export/[website]/[...slug].ts
rename to src/pages/stories/export/description/[website]/[...slug].ts
index 75e0517..fd9c52f 100644
--- a/src/pages/stories/export/[website]/[...slug].ts
+++ b/src/pages/stories/export/description/[website]/[...slug].ts
@@ -1,8 +1,8 @@
-import type { APIRoute, GetStaticPaths } from "astro";
+import { type APIRoute, type GetStaticPaths } from "astro";
 import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro:content";
 import { marked, type RendererApi } from "marked";
 import he from "he";
-import { type Website } from "../../../../content/config";
+import { type Website } from "../../../../../content/config";
 
 const WEBSITE_LIST = ["eka", "furaffinity", "inkbunny", "sofurry", "weasyl"] as const satisfies Website[];
 
diff --git a/src/pages/stories/export/story/[...slug].ts b/src/pages/stories/export/story/[...slug].ts
new file mode 100644
index 0000000..e98a993
--- /dev/null
+++ b/src/pages/stories/export/story/[...slug].ts
@@ -0,0 +1,72 @@
+import { type APIRoute, type GetStaticPaths } from "astro";
+import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro:content";
+import { type Lang } from "../../../../content/config";
+
+type Props = {
+  story: CollectionEntry<"stories">;
+};
+
+type Params = {
+  slug: CollectionEntry<"stories">["slug"];
+};
+
+export const getStaticPaths: GetStaticPaths = async () => {
+  if (import.meta.env.PROD) {
+    return [];
+  }
+  return (await getCollection("stories")).map((story) => ({
+    params: { slug: story.slug } satisfies Params,
+    props: { story } satisfies Props,
+  }));
+};
+
+function getNameForUser(user: CollectionEntry<"users">, anonymousUser: CollectionEntry<"users">, lang: Lang): string {
+  if (user.data.isAnonymous) {
+    return anonymousUser.data.nameLang[lang] || anonymousUser.data.name;
+  }
+  return user.data.nameLang[lang] || user.data.name;
+}
+
+export const GET: APIRoute<Props, Params> = async ({ props: { story } }) => {
+  const { lang } = story.data;
+  const anonymousUser = await getEntry("users", "anonymous");
+  const authorsNames = (await getEntries([story.data.authors].flat())).map((author) =>
+    getNameForUser(author, anonymousUser, lang),
+  );
+  const commissioner = story.data.commissioner && (await getEntry(story.data.commissioner));
+  const requester = story.data.requester && (await getEntry(story.data.requester));
+
+  let storyHeader = `${story.data.title}\n`;
+  if (lang === "eng") {
+    let authorsString = `by ${authorsNames[0]}`;
+    if (authorsNames.length > 2) {
+      authorsString += `, ${authorsNames.slice(1, authorsNames.length - 1).join(", ")}, and ${authorsNames[authorsNames.length - 1]}`;
+    } else if (authorsNames.length == 2) {
+      authorsString += ` and ${authorsNames[1]}`;
+    }
+    storyHeader +=
+      `${authorsString}\n` +
+      (commissioner ? `Commissioned by ${getNameForUser(commissioner, anonymousUser, lang)}\n` : "") +
+      (requester ? `Requested by ${getNameForUser(requester, anonymousUser, lang)}\n` : "");
+  } else if (lang === "tok") {
+    let authorsString = "lipu ni li tan ";
+    if (authorsNames.length > 1) {
+      authorsString += `jan ni: ${authorsNames.join(" en ")}`;
+    } else {
+      authorsString += authorsNames[0];
+    }
+    if (commissioner) {
+      throw new Error(`No "commissioner" handler for language "tok"`);
+    }
+    if (requester) {
+      throw new Error(`No "requester" handler for language "tok"`);
+    }
+    storyHeader += `${authorsString}\n`;
+  } else {
+    throw new Error(`Unknown language "${lang}"`);
+  }
+
+  const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll("\\*", "*").replaceAll("\\=", "=")}`;
+  const headers = { "Content-Type": "text/plain; charset=utf-8" };
+  return new Response(`${storyText.replaceAll(/\n\n\n+/g, "\n\n").trim()}\n`, { headers });
+};
diff --git a/src/pages/stories/export/thumbnail/[...slug].ts b/src/pages/stories/export/thumbnail/[...slug].ts
new file mode 100644
index 0000000..0c82b11
--- /dev/null
+++ b/src/pages/stories/export/thumbnail/[...slug].ts
@@ -0,0 +1,27 @@
+import { type APIRoute, type GetStaticPaths } from "astro";
+import { getCollection, type CollectionEntry } from "astro:content";
+
+type Props = {
+  story: CollectionEntry<"stories">;
+};
+
+type Params = {
+  slug: CollectionEntry<"stories">["slug"];
+};
+
+export const getStaticPaths: GetStaticPaths = async () => {
+  if (import.meta.env.PROD) {
+    return [];
+  }
+  return (await getCollection("stories")).map((story) => ({
+    params: { slug: story.slug } satisfies Params,
+    props: { story } satisfies Props,
+  }));
+};
+
+export const GET: APIRoute<Props, Params> = async ({ props: { story }, redirect }) => {
+  if (!story.data.thumbnail) {
+    return new Response(null, { status: 404 });
+  }
+  return redirect(story.data.thumbnail.src);
+};
diff --git a/src/pages/tags/digestion.astro b/src/pages/tags/digestion.astro
new file mode 100644
index 0000000..d3d65c6
--- /dev/null
+++ b/src/pages/tags/digestion.astro
@@ -0,0 +1,10 @@
+---
+import GalleryLayout from "../../layouts/GalleryLayout.astro";
+
+const tag = "Digestion";
+---
+
+<GalleryLayout pageTitle={`Works tagged "${tag}"`}>
+  <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{tag}"</h1>
+  <p class="my-4">No.</p>
+</GalleryLayout>