diff --git a/examples/blog.md b/examples/blog.md
index 5a7c2f9..b1416df 100644
--- a/examples/blog.md
+++ b/examples/blog.md
@@ -5,7 +5,7 @@ title: Example Blog Post
 isDraft: true
 isAgeRestricted: true
 authors: bad-manners
-# thumbnail: /src/assets/thumbnails/post_thumbnail.png
+# thumbnail: /src/assets/thumbnails/blog/post_thumbnail.png
 description: |
   Some funny text.
 # posts:
diff --git a/examples/game.md b/examples/game.md
index 6e7ae04..1ef3655 100644
--- a/examples/game.md
+++ b/examples/game.md
@@ -8,7 +8,7 @@ isAgeRestricted: true
 authors: bad-manners
 contentWarning: >
   This game contains some stuff.
-# thumbnail: /src/assets/thumbnails/game_thumbnail.png
+# thumbnail: /src/assets/thumbnails/games/game_thumbnail.png
 description: |
   Some funny text.
 platforms: [web, windows, linux, macos, android, ios]
diff --git a/examples/story.md b/examples/story.md
index dbfcbdb..884be31 100644
--- a/examples/story.md
+++ b/examples/story.md
@@ -9,7 +9,7 @@ authors: bad-manners
 # wordCount: 1000
 contentWarning: >
   Contains: Same size safe vore/endosoma (oral vore), with willing anthro male fox predator, and unwilling feral female wolf prey. Also includes other stuff.
-# thumbnail: /src/assets/thumbnails/story_thumbnail.png
+# thumbnail: /src/assets/thumbnails/stories/story_thumbnail.png
 description: |
   Some funny text.
 # posts:
diff --git a/package-lock.json b/package-lock.json
index db8c922..1cb64f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "gallery.badmanners.xyz",
-  "version": "1.9.0",
+  "version": "1.10.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "gallery.badmanners.xyz",
-      "version": "1.9.0",
+      "version": "1.10.0",
       "hasInstallScript": true,
       "dependencies": {
         "@astrojs/check": "^0.9.3",
diff --git a/package.json b/package.json
index 2b4c08e..692636b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "gallery.badmanners.xyz",
   "type": "module",
-  "version": "1.9.0",
+  "version": "1.10.0",
   "scripts": {
     "postinstall": "astro sync",
     "dev": "astro dev",
diff --git a/src/assets/images/ssh/multipaint_by_numbers.png b/src/assets/images/ssh/multipaint_by_numbers.png
index 5912da5..223e3b6 100644
Binary files a/src/assets/images/ssh/multipaint_by_numbers.png and b/src/assets/images/ssh/multipaint_by_numbers.png differ
diff --git a/src/assets/thumbnails/other/crossing_over_retrospective.png b/src/assets/thumbnails/blog/crossing_over_retrospective.png
similarity index 100%
rename from src/assets/thumbnails/other/crossing_over_retrospective.png
rename to src/assets/thumbnails/blog/crossing_over_retrospective.png
diff --git a/src/assets/thumbnails/other/ssh_all_the_way_down.png b/src/assets/thumbnails/blog/ssh_all_the_way_down.png
similarity index 100%
rename from src/assets/thumbnails/other/ssh_all_the_way_down.png
rename to src/assets/thumbnails/blog/ssh_all_the_way_down.png
diff --git a/src/assets/thumbnails/blog/supercharged_ssh_apps_on_sish.png b/src/assets/thumbnails/blog/supercharged_ssh_apps_on_sish.png
new file mode 100644
index 0000000..47ff731
Binary files /dev/null and b/src/assets/thumbnails/blog/supercharged_ssh_apps_on_sish.png differ
diff --git a/src/assets/thumbnails/other/taken_in_breakdown.png b/src/assets/thumbnails/blog/taken_in_breakdown.png
similarity index 100%
rename from src/assets/thumbnails/other/taken_in_breakdown.png
rename to src/assets/thumbnails/blog/taken_in_breakdown.png
diff --git a/src/assets/thumbnails/game_crossing_over_cover.png b/src/assets/thumbnails/games/crossing_over_cover.png
similarity index 100%
rename from src/assets/thumbnails/game_crossing_over_cover.png
rename to src/assets/thumbnails/games/crossing_over_cover.png
diff --git a/src/assets/thumbnails/bm_10_birdroom.png b/src/assets/thumbnails/stories/bm_10_birdroom.png
similarity index 100%
rename from src/assets/thumbnails/bm_10_birdroom.png
rename to src/assets/thumbnails/stories/bm_10_birdroom.png
diff --git a/src/assets/thumbnails/bm_11_engaging_contacts.png b/src/assets/thumbnails/stories/bm_11_engaging_contacts.png
similarity index 100%
rename from src/assets/thumbnails/bm_11_engaging_contacts.png
rename to src/assets/thumbnails/stories/bm_11_engaging_contacts.png
diff --git a/src/assets/thumbnails/bm_12_noble_fire.png b/src/assets/thumbnails/stories/bm_12_noble_fire.png
similarity index 100%
rename from src/assets/thumbnails/bm_12_noble_fire.png
rename to src/assets/thumbnails/stories/bm_12_noble_fire.png
diff --git a/src/assets/thumbnails/bm_13_trouble_sleeping.png b/src/assets/thumbnails/stories/bm_13_trouble_sleeping.png
similarity index 100%
rename from src/assets/thumbnails/bm_13_trouble_sleeping.png
rename to src/assets/thumbnails/stories/bm_13_trouble_sleeping.png
diff --git a/src/assets/thumbnails/bm_14_bottom_of_the_food_chain.png b/src/assets/thumbnails/stories/bm_14_bottom_of_the_food_chain.png
similarity index 100%
rename from src/assets/thumbnails/bm_14_bottom_of_the_food_chain.png
rename to src/assets/thumbnails/stories/bm_14_bottom_of_the_food_chain.png
diff --git a/src/assets/thumbnails/bm_15_gentle_and_cruel.png b/src/assets/thumbnails/stories/bm_15_gentle_and_cruel.png
similarity index 100%
rename from src/assets/thumbnails/bm_15_gentle_and_cruel.png
rename to src/assets/thumbnails/stories/bm_15_gentle_and_cruel.png
diff --git a/src/assets/thumbnails/bm_16_big_haul.png b/src/assets/thumbnails/stories/bm_16_big_haul.png
similarity index 100%
rename from src/assets/thumbnails/bm_16_big_haul.png
rename to src/assets/thumbnails/stories/bm_16_big_haul.png
diff --git a/src/assets/thumbnails/bm_17_taken_in.png b/src/assets/thumbnails/stories/bm_17_taken_in.png
similarity index 100%
rename from src/assets/thumbnails/bm_17_taken_in.png
rename to src/assets/thumbnails/stories/bm_17_taken_in.png
diff --git a/src/assets/thumbnails/bm_18_tiny_accident.png b/src/assets/thumbnails/stories/bm_18_tiny_accident.png
similarity index 100%
rename from src/assets/thumbnails/bm_18_tiny_accident.png
rename to src/assets/thumbnails/stories/bm_18_tiny_accident.png
diff --git a/src/assets/thumbnails/bm_19_woofer_exploration.png b/src/assets/thumbnails/stories/bm_19_woofer_exploration.png
similarity index 100%
rename from src/assets/thumbnails/bm_19_woofer_exploration.png
rename to src/assets/thumbnails/stories/bm_19_woofer_exploration.png
diff --git a/src/assets/thumbnails/bm_1_eggs_for_months.png b/src/assets/thumbnails/stories/bm_1_eggs_for_months.png
similarity index 100%
rename from src/assets/thumbnails/bm_1_eggs_for_months.png
rename to src/assets/thumbnails/stories/bm_1_eggs_for_months.png
diff --git a/src/assets/thumbnails/bm_20_playing_it_safe.png b/src/assets/thumbnails/stories/bm_20_playing_it_safe.png
similarity index 100%
rename from src/assets/thumbnails/bm_20_playing_it_safe.png
rename to src/assets/thumbnails/stories/bm_20_playing_it_safe.png
diff --git a/src/assets/thumbnails/bm_2_pet_sit_saturday.png b/src/assets/thumbnails/stories/bm_2_pet_sit_saturday.png
similarity index 100%
rename from src/assets/thumbnails/bm_2_pet_sit_saturday.png
rename to src/assets/thumbnails/stories/bm_2_pet_sit_saturday.png
diff --git a/src/assets/thumbnails/bm_3_annivoresary.png b/src/assets/thumbnails/stories/bm_3_annivoresary.png
similarity index 100%
rename from src/assets/thumbnails/bm_3_annivoresary.png
rename to src/assets/thumbnails/stories/bm_3_annivoresary.png
diff --git a/src/assets/thumbnails/bm_4_you_re_home.png b/src/assets/thumbnails/stories/bm_4_you_re_home.png
similarity index 100%
rename from src/assets/thumbnails/bm_4_you_re_home.png
rename to src/assets/thumbnails/stories/bm_4_you_re_home.png
diff --git a/src/assets/thumbnails/bm_5_accommodation.png b/src/assets/thumbnails/stories/bm_5_accommodation.png
similarity index 100%
rename from src/assets/thumbnails/bm_5_accommodation.png
rename to src/assets/thumbnails/stories/bm_5_accommodation.png
diff --git a/src/assets/thumbnails/bm_6_delicacy_s_dare.png b/src/assets/thumbnails/stories/bm_6_delicacy_s_dare.png
similarity index 100%
rename from src/assets/thumbnails/bm_6_delicacy_s_dare.png
rename to src/assets/thumbnails/stories/bm_6_delicacy_s_dare.png
diff --git a/src/assets/thumbnails/bm_7_hungry_for_love.png b/src/assets/thumbnails/stories/bm_7_hungry_for_love.png
similarity index 100%
rename from src/assets/thumbnails/bm_7_hungry_for_love.png
rename to src/assets/thumbnails/stories/bm_7_hungry_for_love.png
diff --git a/src/assets/thumbnails/bm_8_flavorful_favor.png b/src/assets/thumbnails/stories/bm_8_flavorful_favor.png
similarity index 100%
rename from src/assets/thumbnails/bm_8_flavorful_favor.png
rename to src/assets/thumbnails/stories/bm_8_flavorful_favor.png
diff --git a/src/assets/thumbnails/bm_9_tasting_high_consequences.png b/src/assets/thumbnails/stories/bm_9_tasting_high_consequences.png
similarity index 100%
rename from src/assets/thumbnails/bm_9_tasting_high_consequences.png
rename to src/assets/thumbnails/stories/bm_9_tasting_high_consequences.png
diff --git a/src/assets/thumbnails/bm_comm_1_addictive_additions.png b/src/assets/thumbnails/stories/bm_comm_1_addictive_additions.png
similarity index 100%
rename from src/assets/thumbnails/bm_comm_1_addictive_additions.png
rename to src/assets/thumbnails/stories/bm_comm_1_addictive_additions.png
diff --git a/src/assets/thumbnails/bm_comm_2_better_in_bully_batter.png b/src/assets/thumbnails/stories/bm_comm_2_better_in_bully_batter.png
similarity index 100%
rename from src/assets/thumbnails/bm_comm_2_better_in_bully_batter.png
rename to src/assets/thumbnails/stories/bm_comm_2_better_in_bully_batter.png
diff --git a/src/assets/thumbnails/bm_comm_3_within_limits.png b/src/assets/thumbnails/stories/bm_comm_3_within_limits.png
similarity index 100%
rename from src/assets/thumbnails/bm_comm_3_within_limits.png
rename to src/assets/thumbnails/stories/bm_comm_3_within_limits.png
diff --git a/src/assets/thumbnails/bm_comm_4_team_building.png b/src/assets/thumbnails/stories/bm_comm_4_team_building.png
similarity index 100%
rename from src/assets/thumbnails/bm_comm_4_team_building.png
rename to src/assets/thumbnails/stories/bm_comm_4_team_building.png
diff --git a/src/assets/thumbnails/bm_comm_5_rose_s_binge.png b/src/assets/thumbnails/stories/bm_comm_5_rose_s_binge.png
similarity index 100%
rename from src/assets/thumbnails/bm_comm_5_rose_s_binge.png
rename to src/assets/thumbnails/stories/bm_comm_5_rose_s_binge.png
diff --git a/src/assets/thumbnails/bm_ff_10_ruffling_some_feathers.png b/src/assets/thumbnails/stories/bm_ff_10_ruffling_some_feathers.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_10_ruffling_some_feathers.png
rename to src/assets/thumbnails/stories/bm_ff_10_ruffling_some_feathers.png
diff --git a/src/assets/thumbnails/bm_ff_11_butting_into_their_plans.png b/src/assets/thumbnails/stories/bm_ff_11_butting_into_their_plans.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_11_butting_into_their_plans.png
rename to src/assets/thumbnails/stories/bm_ff_11_butting_into_their_plans.png
diff --git a/src/assets/thumbnails/bm_ff_12_hate_to_sea_it.png b/src/assets/thumbnails/stories/bm_ff_12_hate_to_sea_it.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_12_hate_to_sea_it.png
rename to src/assets/thumbnails/stories/bm_ff_12_hate_to_sea_it.png
diff --git a/src/assets/thumbnails/bm_ff_13_tomo_moku.png b/src/assets/thumbnails/stories/bm_ff_13_tomo_moku.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_13_tomo_moku.png
rename to src/assets/thumbnails/stories/bm_ff_13_tomo_moku.png
diff --git a/src/assets/thumbnails/bm_ff_14_part_of_the_show.png b/src/assets/thumbnails/stories/bm_ff_14_part_of_the_show.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_14_part_of_the_show.png
rename to src/assets/thumbnails/stories/bm_ff_14_part_of_the_show.png
diff --git a/src/assets/thumbnails/bm_ff_15_bladder_filler.png b/src/assets/thumbnails/stories/bm_ff_15_bladder_filler.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_15_bladder_filler.png
rename to src/assets/thumbnails/stories/bm_ff_15_bladder_filler.png
diff --git a/src/assets/thumbnails/bm_ff_1_hyper_hunger.png b/src/assets/thumbnails/stories/bm_ff_1_hyper_hunger.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_1_hyper_hunger.png
rename to src/assets/thumbnails/stories/bm_ff_1_hyper_hunger.png
diff --git a/src/assets/thumbnails/bm_ff_2_never_too_late.png b/src/assets/thumbnails/stories/bm_ff_2_never_too_late.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_2_never_too_late.png
rename to src/assets/thumbnails/stories/bm_ff_2_never_too_late.png
diff --git a/src/assets/thumbnails/bm_ff_3_insistence_and_assistance.png b/src/assets/thumbnails/stories/bm_ff_3_insistence_and_assistance.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_3_insistence_and_assistance.png
rename to src/assets/thumbnails/stories/bm_ff_3_insistence_and_assistance.png
diff --git a/src/assets/thumbnails/bm_ff_4_the_last_livestream.png b/src/assets/thumbnails/stories/bm_ff_4_the_last_livestream.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_4_the_last_livestream.png
rename to src/assets/thumbnails/stories/bm_ff_4_the_last_livestream.png
diff --git a/src/assets/thumbnails/bm_ff_5_latest_catch.png b/src/assets/thumbnails/stories/bm_ff_5_latest_catch.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_5_latest_catch.png
rename to src/assets/thumbnails/stories/bm_ff_5_latest_catch.png
diff --git a/src/assets/thumbnails/bm_ff_6_lactation_action.png b/src/assets/thumbnails/stories/bm_ff_6_lactation_action.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_6_lactation_action.png
rename to src/assets/thumbnails/stories/bm_ff_6_lactation_action.png
diff --git a/src/assets/thumbnails/bm_ff_7_for_the_night.png b/src/assets/thumbnails/stories/bm_ff_7_for_the_night.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_7_for_the_night.png
rename to src/assets/thumbnails/stories/bm_ff_7_for_the_night.png
diff --git a/src/assets/thumbnails/bm_ff_8_spontaneous_sleepover.png b/src/assets/thumbnails/stories/bm_ff_8_spontaneous_sleepover.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_8_spontaneous_sleepover.png
rename to src/assets/thumbnails/stories/bm_ff_8_spontaneous_sleepover.png
diff --git a/src/assets/thumbnails/bm_ff_9_reaching_for_the_full_moon.png b/src/assets/thumbnails/stories/bm_ff_9_reaching_for_the_full_moon.png
similarity index 100%
rename from src/assets/thumbnails/bm_ff_9_reaching_for_the_full_moon.png
rename to src/assets/thumbnails/stories/bm_ff_9_reaching_for_the_full_moon.png
diff --git a/src/assets/thumbnails/bm_r_1_overzealous_zenko.png b/src/assets/thumbnails/stories/bm_r_1_overzealous_zenko.png
similarity index 100%
rename from src/assets/thumbnails/bm_r_1_overzealous_zenko.png
rename to src/assets/thumbnails/stories/bm_r_1_overzealous_zenko.png
diff --git a/src/assets/thumbnails/bm_r_2_team_effort.png b/src/assets/thumbnails/stories/bm_r_2_team_effort.png
similarity index 100%
rename from src/assets/thumbnails/bm_r_2_team_effort.png
rename to src/assets/thumbnails/stories/bm_r_2_team_effort.png
diff --git a/src/assets/thumbnails/bm_r_3_warped_friendship.png b/src/assets/thumbnails/stories/bm_r_3_warped_friendship.png
similarity index 100%
rename from src/assets/thumbnails/bm_r_3_warped_friendship.png
rename to src/assets/thumbnails/stories/bm_r_3_warped_friendship.png
diff --git a/src/assets/thumbnails/tlotm_bonus_1.png b/src/assets/thumbnails/stories/tlotm_bonus_1.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_bonus_1.png
rename to src/assets/thumbnails/stories/tlotm_bonus_1.png
diff --git a/src/assets/thumbnails/tlotm_ch1.png b/src/assets/thumbnails/stories/tlotm_ch1.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_ch1.png
rename to src/assets/thumbnails/stories/tlotm_ch1.png
diff --git a/src/assets/thumbnails/tlotm_ch10.png b/src/assets/thumbnails/stories/tlotm_ch10.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_ch10.png
rename to src/assets/thumbnails/stories/tlotm_ch10.png
diff --git a/src/assets/thumbnails/tlotm_ch11.png b/src/assets/thumbnails/stories/tlotm_ch11.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_ch11.png
rename to src/assets/thumbnails/stories/tlotm_ch11.png
diff --git a/src/assets/thumbnails/tlotm_ch2.png b/src/assets/thumbnails/stories/tlotm_ch2.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_ch2.png
rename to src/assets/thumbnails/stories/tlotm_ch2.png
diff --git a/src/assets/thumbnails/tlotm_ch3.png b/src/assets/thumbnails/stories/tlotm_ch3.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_ch3.png
rename to src/assets/thumbnails/stories/tlotm_ch3.png
diff --git a/src/assets/thumbnails/tlotm_ch4.png b/src/assets/thumbnails/stories/tlotm_ch4.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_ch4.png
rename to src/assets/thumbnails/stories/tlotm_ch4.png
diff --git a/src/assets/thumbnails/tlotm_ch5.png b/src/assets/thumbnails/stories/tlotm_ch5.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_ch5.png
rename to src/assets/thumbnails/stories/tlotm_ch5.png
diff --git a/src/assets/thumbnails/tlotm_ch6.png b/src/assets/thumbnails/stories/tlotm_ch6.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_ch6.png
rename to src/assets/thumbnails/stories/tlotm_ch6.png
diff --git a/src/assets/thumbnails/tlotm_ch7.png b/src/assets/thumbnails/stories/tlotm_ch7.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_ch7.png
rename to src/assets/thumbnails/stories/tlotm_ch7.png
diff --git a/src/assets/thumbnails/tlotm_ch8.png b/src/assets/thumbnails/stories/tlotm_ch8.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_ch8.png
rename to src/assets/thumbnails/stories/tlotm_ch8.png
diff --git a/src/assets/thumbnails/tlotm_ch9.png b/src/assets/thumbnails/stories/tlotm_ch9.png
similarity index 100%
rename from src/assets/thumbnails/tlotm_ch9.png
rename to src/assets/thumbnails/stories/tlotm_ch9.png
diff --git a/src/components/TocMdx.astro b/src/components/TocMdx.astro
index 84a4f9e..182e28d 100644
--- a/src/components/TocMdx.astro
+++ b/src/components/TocMdx.astro
@@ -21,16 +21,19 @@ const nestedHeadings = headings.reduce((acc, heading) => {
     let nextParent: NestedHeading = acc[acc.length - 1];
     while (nextParent.depth < heading.depth) {
       parent = nextParent;
-      if (!nextParent.children) {
-        nextParent.children = [];
+      if (!parent.children) {
         break;
       }
-      nextParent = nextParent.children[nextParent.children.length - 1];
+      nextParent = parent.children[parent.children.length - 1];
     }
     if (parent === null) {
       acc.push({ ...heading });
     } else {
-      parent.children!.push({ ...heading });
+      if (parent.children) {
+        parent.children.push({ ...heading });
+      } else {
+        parent.children = [{ ...heading }];
+      }
     }
   }
   return acc;
diff --git a/src/components/TocMdxHeading.astro b/src/components/TocMdxHeading.astro
index b48f6e9..4cab0de 100644
--- a/src/components/TocMdxHeading.astro
+++ b/src/components/TocMdxHeading.astro
@@ -12,7 +12,7 @@ const { slug, text, children } = Astro.props;
 <li>
   <a href={`#${slug}`}>{text}</a>
   {
-    children ? (
+    children?.length ? (
       <ul>
         {children.map((child) => (
           <Astro.self {...child} />
diff --git a/src/content/blog/crossing-over-postmortem.mdx b/src/content/blog/crossing-over-postmortem.mdx
index e4c946b..944bb83 100644
--- a/src/content/blog/crossing-over-postmortem.mdx
+++ b/src/content/blog/crossing-over-postmortem.mdx
@@ -3,7 +3,7 @@ title: "Jamming Over: A Postmortem"
 pubDate: 2024-03-26
 isAgeRestricted: true
 authors: bad-manners
-thumbnail: /src/assets/thumbnails/other/crossing_over_retrospective.png
+thumbnail: /src/assets/thumbnails/blog/crossing_over_retrospective.png
 description: |
   A retrospective about my first vore game, [Crossing Over](/games/crossing-over) – albeit more of an assortment of random thoughts than an actual postmortem. **Spoilers for Crossing Over ahead!**
 posts:
diff --git a/src/content/blog/ssh-all-the-way-down.mdx b/src/content/blog/ssh-all-the-way-down.mdx
index 667831b..a73d421 100644
--- a/src/content/blog/ssh-all-the-way-down.mdx
+++ b/src/content/blog/ssh-all-the-way-down.mdx
@@ -4,7 +4,7 @@ title: SSH all the way down!
 pubDate: 2024-09-22
 isAgeRestricted: false
 authors: bad-manners
-thumbnail: /src/assets/thumbnails/other/ssh_all_the_way_down.png
+thumbnail: /src/assets/thumbnails/blog/ssh_all_the_way_down.png
 description: |
   A long investigation on how reverse port forwarding works in SSH; for fun at first, and then, fully embracing it.
 next: supercharged-ssh-apps-on-sish
diff --git a/src/content/blog/supercharged-ssh-apps-on-sish.mdx b/src/content/blog/supercharged-ssh-apps-on-sish.mdx
index edb1805..8ff9bea 100644
--- a/src/content/blog/supercharged-ssh-apps-on-sish.mdx
+++ b/src/content/blog/supercharged-ssh-apps-on-sish.mdx
@@ -1,11 +1,10 @@
 ---
 slug: supercharged-ssh-apps-on-sish
 title: Supercharged SSH applications on sish
-# pubDate: 2024-09-23
-isDraft: true
+pubDate: 2024-09-23
 isAgeRestricted: false
 authors: bad-manners
-# thumbnail: /src/assets/thumbnails/drafts/ssh_all_the_way_down.png
+thumbnail: /src/assets/thumbnails/blog/supercharged_ssh_apps_on_sish.png
 description: |
   After discovering the joys of a reverse port forwarding-based architecture, I dig even deeper into SSH itself to make the most of it.
 prev: ssh-all-the-way-down
@@ -26,7 +25,7 @@ import imageRusshAxum from "@assets/images/ssh/russh_axum.png";
 import imageCheckboxes from "@assets/images/ssh/checkboxes.png";
 import imageMultipaintByNumbers from "@assets/images/ssh/multipaint_by_numbers.png";
 
-This is my second post investigating SSH, learning all that it has to offer.
+This is my second post investigating SSH, and learning what it has to offer.
 
 <TocMdx headings={getHeadings()} />
 
@@ -39,7 +38,10 @@ In my [last post](/blog/ssh-all-the-way-down), I went over a saga of trying to s
     src={imageSishPublic}
     alt="Diagram entitled 'sish public', showing that Eric's machine with a service exposed on localhost port 3000 connects to sish via the command (ssh -R eric:80:localhost:3000 tuns.sh). This creates a bi-directional tunnel and exposes https://eric.tuns.sh to the Internet, which Tony accesses from a separate device."
   />
-  <figcaption>With a simple reverse shell command, we can expose anything to the Internet. © Antonio Mika</figcaption>
+  <figcaption>
+    With a simple reverse shell command, and a configured sish instance, we can expose anything to the Internet. ©
+    Antonio Mika
+  </figcaption>
 </figure>
 
 In fact, we can host anything TCP-based as long as we can create a secure shell to sish, even if it's running on the same host machine.
@@ -60,7 +62,7 @@ In a way, this is a two-fold solution. sish provides us with:
 1. A [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy), which will handle and route any incoming traffic to the correct applications.
 2. A [hole-punching technique](<https://en.wikipedia.org/wiki/Hole_punching_(networking)>), letting us overcome any limitations that NAT imposes.
 
-But as of now, our applications all look something like this (in [Docker Compose](https://docs.docker.com/compose/) configuration):
+But as of now, all of our applications look something like this (in [Docker Compose](https://docs.docker.com/compose/) configuration):
 
 <figure>
 
@@ -95,7 +97,11 @@ services:
   <figcaption>A basic server connecting to sish via a persistent reverse SSH tunnel.</figcaption>
 </figure>
 
-It makes sense to have this separation into two images, if our application only uses an HTTP socket and isn't aware of the SSH tunneling shenanigans going on... which is most of the applications. But if we build our _own_ application, we could interact directly with SSH instead. In that case, how deep can we really integrate it with the existing architecture...?
+We have two images running for our application. One (`server`) is the actual webserver, while the other (`autosish`) handles the reverse port forwarding for us.
+
+It makes sense to have this separation into two images, if our application only uses an HTTP socket, and if it isn't aware of the SSH tunneling shenanigans going on... which is most of the applications.
+
+But if we build our _own_ application, we could interact directly with SSH instead. In that case, how deep can we really integrate it with the existing architecture...?
 
 In this post, we will work on a tunnel-aware application, and finding out more about the SSH protocol. Be forewarned that there will be plenty of [Rust](https://www.rust-lang.org/) code ahead!
 
@@ -105,7 +111,9 @@ But first of all, how does remote port forwarding through an SSH tunnel work?
 
 Better yet, how does _an SSH tunnel_ even work?
 
-In the previous post, I explained that SSH is an [application-layer protocol](https://en.wikipedia.org/wiki/Application_layer) that runs on top of TCP. Our SSH server is listening on a port (usually 22) and we are able to connect to it. It has its own protocols and implementations, but as long as it's implemented properly by a library, we are able to connect without the default SSH client.
+In the previous post, I explained that SSH is an [application-layer protocol](https://en.wikipedia.org/wiki/Application_layer) that runs on top of TCP. Our SSH server is listening on a port (usually 22) and we are able to connect to it.
+
+SSH has its own protocols and implementations as expected, but so long as it's implemented properly by a library, we should be able to connect without the default SSH client.
 
 <figure>
   <Image
@@ -153,9 +161,9 @@ async fn main() -> Result<()> {
 }
 ```
 
-If you're unfamiliar with Rust, this might be a lot at once. In summary, we're just importing some dependencies at the top, and at the bottom, inside of `fn main()`, we're setting up a client connection with `client::connect()`.
+If you're unfamiliar with Rust, this might be a lot at once. In summary, we're doing two things: at the top, we import any dependencies we need; and at the bottom, inside of `async fn main()`, we're setting up a client connection with `client::connect()`.
 
-Aside from this code, we also need to define the `Client` struct, which will be responsible for answering messages created by the server. This will implement the `russh::client::Handler` async trait, responsible for linking our methods to the ones that the library knows to call.
+Aside from this code, we also need to define the `Client` struct, which will be responsible for answering messages created by the server. This will implement the `russh::client::Handler` async trait, responsible for exposing our user-defined methods to the ones that the library knows to call.
 
 ```rust
 use async_trait::async_trait;
@@ -183,21 +191,21 @@ With these two pieces, we can try running our program with cargo like this:
 cargo run -- -R test -p 80 -i ~/.ssh/id_ed25519 sish.top
 ```
 
-We're using an argument parser to read the flags, [clap](https://docs.rs/clap/latest/clap/). The way it's set up, you can read this as being equivalent to the other command that we've seen so far:
+We're using [clap](https://docs.rs/clap/latest/clap/), an argument parser, to read the flags that are passed. The way it's set up, you can read this as being equivalent to the other command that we've seen so far:
 
 ```sh
 ssh -R test:80:localhost:???? -i ~/.ssh/id_ed25519 sish.top
 ```
 
-(Notice that we no longer specify the port in the localhost; we'll get to that later.)
+(Notice that we no longer specify the local port to forward remote connections to; we'll get to that later.)
 
 When we run this, it prints `Connected.` on our terminal, then immediately exits the program.
 
 At least we're doing...not nothing.
 
-You might've realized that we aren't actually doing anything with the connection when we create it. The `client::connect()` function simply establishes the TCP socket and checks for the server's key fingerprint, through the single method that we've implemented on the `Client` struct.
+You might've realized that the connection is being ignored right after we create it. The `client::connect()` function simply establishes the TCP socket and checks for the server's key fingerprint, through the single method that we've implemented on the `Client` struct.
 
-As you may have guessed from the identity file being passed to the command, we'll have to use that to authenticate now. So after we create a connection, we'll immediately call `session.authenticate_publickey()` to do so via public key cryptography. I've cut the repeated portion of the program below with a `// ... snip ...`:
+As you may have guessed from the identity file being passed to the command, we'll have to use that to authenticate now. So after we create a connection, we'll immediately call `session.authenticate_publickey()` to do so via public key cryptography. I've cut the repeated portion of the program below with a `snip`:
 
 ```rust
 use std::path::PathBuf;
@@ -211,11 +219,11 @@ async fn main() -> Result<()> {
     let secret_key = fs::read_to_string(args.identity_file)
         .await
         .with_context(|| "Failed to open identity file")?;
-    let secret_key =
-        Arc::new(decode_secret_key(&secret_key, None).with_context(|| "Invalid secret key")?);
+    let secret_key = decode_secret_key(&secret_key, None)
+        .with_context(|| "Invalid secret key")?;
 
     if session
-        .authenticate_publickey(args.login_name, secret_key)
+        .authenticate_publickey(args.login_name, Arc::new(secret_key))
         .await
         .with_context(|| "Error while authenticating with public key.")?
     {
@@ -230,9 +238,9 @@ async fn main() -> Result<()> {
 
 If your key is authorized with the given server, this will print `Public key authentication succeeded!` after connecting, then immediately exit again. Not a lot of progress, but bear with me.
 
-So connecting and authenticating is straightforward enough. You might draw a parallel with connecting to a website, then logging in with your credentials. What comes after you log in is a bit more freeform, and depends on what you intend to do on the website.
+So connecting and authenticating is straightforward enough. You might draw a parallel with first connecting to a website, then logging in with your credentials. What comes after you log in is a bit more freeform, and depends on what you intend to do on the website.
 
-With SSH, the analogy still holds. After creating a session, there are many options for what we can do next ([many of them available in russh](https://docs.rs/russh/0.45.0/russh/client/struct.Session.html)):
+With SSH, the analogy still holds. After creating a session, there are many options for what we can do next ([all of them available in russh](https://docs.rs/russh/0.45.0/russh/client/struct.Session.html)):
 
 - `request_pty()`: Request that an interactive [pseudoterminal](https://en.wikipedia.org/wiki/Pseudoterminal) is created by the server, allowing us to enter commands over a remote shell session. This is the most common usecase for SSH.
 - `request_x11()`: Request that an [X11 connection](https://en.wikipedia.org/wiki/X_Window_System) is displayed over the Internet. This lets us access graphical applications through SSH!
@@ -255,9 +263,9 @@ async fn main() -> Result<()> {
 }
 ```
 
-Once again, it succeeds and binds on what we'd expect, then exits immediately. I'm seeing a pattern here...
+Once again, it succeeds and binds on the provided host and port, then exits immediately. I'm seeing a pattern here...
 
-It turns out that there's one missing piece here, and that is to create an open-session channel. We'll get into the reasons why in the next section, but for now, let's push on with some more code.
+It turns out that there's one missing piece here, and that is to create an open-session channel. We'll get into the reason why in the next subsection, but for now, let's push on with some more code.
 
 ```rust
 use russh::{ChannelMsg, Disconnect};
@@ -271,6 +279,7 @@ async fn main() -> Result<()> {
         .channel_open_session()
         .await
         .with_context(|| "channel_open_session error.")?;
+    println!("Created open session channel.");
     let mut stdout = stdout();
     let mut stderr = stderr();
     let code = loop {
@@ -309,7 +318,7 @@ async fn main() -> Result<()> {
 }
 ```
 
-There's a lot going on in this one. First we create a channel with `session.channel_open_session()`, then we set up a `loop` that will read every message that we eventually get through this channel (by reading it with `channel.wait().await`) and handle it appropriately. Then, we try to close the session cleanly if possible.
+There's a lot going on in this one. First we create a channel with `session.channel_open_session()`, then we set up a `loop` that will read every message that we eventually get through this channel (by reading it with `channel.wait().await`). We have to handle each message type appropriately. Then, once the channel closes, we try to close the session cleanly if possible.
 
 When we run it this time, we expect it to simply exit after printing the–
 
@@ -323,43 +332,49 @@ HTTPS: https://test.sish.top
 
 Wait, the session is actually persisting?! And we even got some data from sish in the process!
 
-When we created the channel through `session.channel_open_session()`, the server automatically knew that it could send information to us through it. It is just a two-way tunnel where every message is secure, so we can read data and even send some back if we want (for example, with [`stdin`](https://en.wikipedia.org/wiki/Standard_streams)).
+When we created the channel through `session.channel_open_session()`, the server automatically knew that it could send us information through it. It is just a two-way tunnel where every message is secure, so we can read data, and even send some back if we want (for example, with [`stdin`](https://en.wikipedia.org/wiki/Standard_streams) if we're making a terminal application).
 
 That's cool and all, but more important is how it gave us a URL for our service – with automatic HTTPS, even! I wonder what happens if I try to open that link in my browser...
 
 ...
 
-...It just times out with "bad gateway", causing our SSH program to exit.
+...It just times out after a while with "bad gateway", causing our SSH program to exit.
 
-Well, that's less exciting. But at least it's doing _something_. Besides, if we never touch the link that it passed us, we can stay connected indefinitely. And as soon as we open the link anywhere, we consistently get disconnected from the SSH server. That's proof that there's a relation between what the browser sees and our little program.
+Well, that's less exciting. But at least it's doing _something_. Besides, if we never touch the link that it passed us, we can stay connected indefinitely. And as soon as we open the link on any device, we consistently get disconnected from the SSH server. That's proof that there's a relation between what the browser sees and our little Rust program.
 
-The reason why we get disconnected is because we aren't handling any requests that come in. Right now, there's no way to read HTTP requests, or send HTTP responses.
+The reason why we get disconnected is because we aren't handling any requests that come in. Right now, there's no way to read HTTP requests, even less sending HTTP responses.
 
 But I thought `channel_open_session()` was already doing that? Not really – it only serves to transfer messages between the client and the server. Instead, to handle each new connection, we need to use a new channel.
 
 Sounds simple enough. Then how do we create these channels? The answer is also simple: We don't.
 
-## Changing channels
+### Changing channels
 
 At this point, it's worth taking a detour from all of the code and explain how an SSH session actually works.
 
-[RFC 4254](https://datatracker.ietf.org/doc/html/rfc4254) is the document that dictates how SSH connections are supposed to work on a higher level. There are some interesting details, but most importantly for us is [5 - Channel Mechanism section](https://datatracker.ietf.org/doc/html/rfc4254#section-5):
+[RFC 4254](https://datatracker.ietf.org/doc/html/rfc4254) is the document that dictates how SSH connections are supposed to work on a higher level. There are some interesting details, but most importantly for us is the ["5. Channel Mechanism"](https://datatracker.ietf.org/doc/html/rfc4254#section-5) section:
 
 > All terminal sessions, forwarded connections, etc., are channels. Either side may open a channel. Multiple channels are multiplexed into a single connection.
 
 In other words, a connection can have multiple channels, each responsible for a part of the system. This explicitly includes forwarded connections, such as the ones we are looking for.
 
-Specifically, in [7.1 - Request Port Forwarding section](https://datatracker.ietf.org/doc/html/rfc4254#section-7.1), the mechanism for requesting a port is specified. It's exactly what we're doing right now. In the next section, [7.2 - TCP/IP Forwarding Channels](https://datatracker.ietf.org/doc/html/rfc4254#section-7.2), the expected behavior of the server is explained:
+Even more so, either side can open channels. Earlier, we opened one with `session.channel_open_session()` from the client-side, but the server is also allowed to open them if necessary.
+
+Specifically, we see that in the ["7.1. Request Port Forwarding"](https://datatracker.ietf.org/doc/html/rfc4254#section-7.1) section, the mechanism for requesting a port is specified. It's exactly what we're doing right now, using `session.tcpip_forward()` and what not.
+
+In the next section, ["7.2. TCP/IP Forwarding Channels"](https://datatracker.ietf.org/doc/html/rfc4254#section-7.2), the expected behavior of the server is explained:
 
 > When a connection comes to a port for which remote forwarding has been requested, a channel is opened to forward the port to the other side.
 
-So a new channel is being created and opened _for_ us. The channel is labeled `forwarded-tcpip`, which corresponds with the `server_channel_open_forwarded_tcpip()` method of `russh::client::Handler`. Previously, we only added a method to our `Client` struct that accepted all of the key fingerprints that the server provides, so we're gonna add a second one to handle the forwarding.
+So a new channel is being created and opened _for_ us. The channel is labeled `forwarded-tcpip`, which corresponds with the `server_channel_open_forwarded_tcpip()` method of `russh::client::Handler`.
 
-Remember, forwarded connections are channels, so we can use them just like the channel we get from `session.channel_open_session()`. And thankfully, Tokio has some utilities to make their usage trivial for our case.
+Previously, that `Handler` only had one defined method by our `Client` struct, which accepted all of the key fingerprints that the server provides. So we gotta add a second one to handle any received forwarding channels.
 
-## Back in session
+Remember, forwarded connections are channels, so we can use them just like the channel we get from `session.channel_open_session()`. And thankfully, as you'll see, Tokio has some utilities to make their usage trivial for our case.
 
-If I understood the documentation correctly, then we should be pretty close to actually getting something to the HTTP endpoint! We just need to create an HTTP server on our side to handle everything for us, then connect it to the data channel that we receive from the server.
+### Back in session
+
+If I understood the documentation correctly, then we should be pretty close to actually getting something to the HTTP endpoint! We just need to create an HTTP server on our side to handle everything for us, then connect it to the data channels that we receive from the server.
 
 First, let's make the simplest HTTP server with global state that we can:
 
@@ -376,25 +391,25 @@ struct AppState {
     data: Arc<AtomicUsize>,
 }
 
+/// A basic example endpoint that includes shared state.
+async fn hello(State(state): State<AppState>) -> String {
+    let request_id = state.data.fetch_add(1, Ordering::AcqRel);
+    format!("Hello, request #{}!", request_id)
+}
+
 /// A lazily-created Router, to be used by the SSH client tunnels.
 static ROUTER: LazyLock<Router> = LazyLock::new(|| {
     Router::new().route("/", get(hello)).with_state(AppState {
         data: Arc::new(AtomicUsize::new(1)),
     })
 });
-
-/// A basic example endpoint that includes shared state.
-async fn hello(State(state): State<AppState>) -> String {
-    let request_id = state.data.fetch_add(1, Ordering::AcqRel);
-    format!("Hello, request #{}!", request_id)
-}
 ```
 
-If you're unfamiliar with Rust's [synchronization primitives](https://doc.rust-lang.org/std/sync/index.html), this may be a bit hard to read. But all this does is create a lazily-evaluated HTTP server that responds to each subsequent request with a global counter (starting on 1, 2, 3, and so on).
+If you're unfamiliar with Rust's [synchronization primitives](https://doc.rust-lang.org/std/sync/index.html), this may be a bit hard to read. But all this does is create a lazily-evaluated HTTP server on `ROUTER` that responds to each subsequent request with a global counter (starting on 1, 2, 3, and so on).
 
-Remember that our goal is to serve this router to any channels received through `server_channel_open_forwarded_tcpip()` on our `Client`. If we were in the C world, we'd need to reference the channel directly by its ID – but in `russh`, a struct representing the channel is already given to us, abstracting that detail away.
+Remember that our goal is to serve this router to any channels received through `server_channel_open_forwarded_tcpip()`. If we were in the C world, we'd need to reference the channel directly by its ID – but in `russh`, a struct representing the channel is already given to us, abstracting that detail away and avoiding any mistakes on the programmer's part.
 
-In order to connect both parts, we'll turn the provided SSH channel into a stream, then use some magic with Hyper and Tower to be able to serve HTTP responses as if that stream were a TCP socket:
+In order to connect the `ROUTER` and the channel together, we'll turn the provided SSH channel into a [stream](https://tokio.rs/tokio/tutorial/streams), then use some magic with Hyper and Tower to be able to serve HTTP responses as if that stream were a TCP socket:
 
 ```rust
 use hyper::service::service_fn;
@@ -423,11 +438,13 @@ impl client::Handler for Client {
         session: &mut Session,
     ) -> Result<(), Self::Error> {
         let router = &*ROUTER;
-        let service = service_fn(move |req| router.clone().call(req));
+        let service = service_fn(
+            move |req| router.clone().call(req));
         let server = Builder::new(TokioExecutor::new());
         tokio::spawn(async move {
             server
-                .serve_connection_with_upgrades(TokioIo::new(channel.into_stream()), service)
+                .serve_connection_with_upgrades(
+                    TokioIo::new(channel.into_stream()), service)
                 .await
                 .expect("Invalid request");
         });
@@ -451,9 +468,9 @@ And, as you may have noticed, we never used a single TCP socket, other than to c
 
 But then, why does the SSH client require that you specify a numbered port for remote forwarding if it's unnecessary? I imagine that the reason for it is convenience. It's easier to map one socket (the remote one) to another (your local one), and ends up being what you want to do in the majority of cases anyway.
 
-However, you can see that the underlying SSH protocol is not too complicated, at least through the interfaces it exposes. We only had to write less than 200 lines of Rust code, and we're already doing things that the regular SSH client can't.
+However, you can see that the underlying SSH protocol is not too complicated, at least through the interfaces it exposes. We only had to write less than 200 lines of Rust code, and we're already doing things that the regular SSH client can't alone.
 
-In summary, this is what the code does:
+To summarize, this is what the code does:
 
 1. Starts a connection with the SSH server.
 2. Authenticates via public key.
@@ -465,23 +482,23 @@ If you want to see the final code, with some additional functionality for runnin
 
 ## Painting the bigger picture
 
-But a simple HTTP server isn't that interesting by itself, even though it's running just over SSH. Can we do better?
+But a simple HTTP server isn't that interesting by itself, even though it's running over SSH instead of a socket. Can we do better?
 
-Yes, we can. We get all the features that we'd expect, including support for WebSockets – but that's beyond the scope of this project.
+Yes, we can. We get all of the nice HTTP features that we'd expect, including support for [WebSocket](https://en.wikipedia.org/wiki/WebSocket) – but that's beyond the scope of this post.
 
-What I'm more interested in is pushing the limits of this solution in terms of simple HTTP. And since I was just starting to learn [htmx](https://htmx.org/), it seemed like the perfect opportunity to put it to the proof.
+What I'm more interested in is pushing the limits of this solution in terms of simple HTTP. And since I was just starting to learn [htmx](https://htmx.org/), it seemed like the perfect opportunity to put it to the test.
 
-At first, I made a simple application that stores a bunch of checkboxes, updates them when you click them, and then periodically polls the server to grab modifications done by other users. It was a dumb but easy idea to implement, but it didn't stop there.
+At first, I made a simple application that stores data for a bunch of checkboxes, then updates them when you click them, and periodically polls the server to grab modifications done by other users. It was a dumb but easy idea to implement, but I didn't stop there.
 
 <figure>
   <Image
     src={imageCheckboxes}
     alt="A screenshot of a browser with a Web 1.0-looking picross or nonogram grid, entitled Multipaint by Numbers, and containing instructions on how to play, as well as multiple colorful cursors."
   />
-  <figcaption>Running a poor man's clone of A Million Checkboxes.</figcaption>
+  <figcaption>Running a poor man's clone of One Million Checkboxes.</figcaption>
 </figure>
 
-Seeing the checkboxes getting filled and emptied in a grid reminded me a lot of nonograms. You might know them by picross or paint by numbers or not know them at all, but they are a kind of puzzle made by filling cells in a grid in order to reveal a pixelated image. So I thought, why not make a multiplayer version of it? And I called it Multipaint by Numbers, and worked on it over a few days.
+Seeing the checkboxes getting filled and emptied in a grid reminded me a lot of [nonograms](https://en.wikipedia.org/wiki/Nonogram). You might know them by "picross" or "paint by numbers", or not know them at all, but they are a kind of puzzle made by filling cells in a grid in order to reveal a pixelated image. So I thought, why not make a multiplayer version of it? And I called it Multipaint by Numbers, and worked on it over the next several days.
 
 <figure>
   <Image
@@ -491,25 +508,39 @@ Seeing the checkboxes getting filled and emptied in a grid reminded me a lot of
   <figcaption>A screenshot of me playing Multipaint by Numbers together with myself.</figcaption>
 </figure>
 
-It's a janky mess, but it technically works. It has click-and-drag, it has flagging, and it even has cursors that (try to) match those of other people currently playing as well! It's certainly janky, as HTMX wasn't designed for highly interactive applications like this one, but it was also quite a breath of fresh air compared to writing Javascript. In general, it was a fun learning experience.
+It's a janky mess, sure, but it definitely works! It has click-and-drag, it has flagging, and it even has cursors that (try to) match those of other people currently playing as well! It definitely feels buggy (rather than _actually_ being buggy) and unresponsive, since HTMX wasn't designed for highly interactive applications like this one. But it was quite a fun learning experience! If you've dabbled in full-stack development, I highly recommend checking out HTMX if you haven't – compared to JS, it's like a breath of fresh air.
 
-It's available to play on https://multipaint.sish.top, and you can see the source code [here](https://github.com/BadMannersXYZ/htmx-ssh-games).
+But if you just wanna play it yourself, Multipaint by Numbers is available to play on https://multipaint.sish.top, and you can find the source code [here](https://github.com/BadMannersXYZ/htmx-ssh-games).
 
 ## Awaiting the future
 
 Of course, none of these projects do anything special. It'd be better to just make them as plain HTTP applications, after all. Why go through such a roundabout way to make webapps?
 
-But there was reason. These were just toy projects to learn the basics about russh, Axum, and HTMX. And I was hoping to go in a new direction with this knowledge.
+But there was a reason. Both "400 Checkboxes" and "Multipaint by Numbers" were just toy projects to learn the basics about russh, Axum, and HTMX. And I was hoping to go in a new direction with this knowledge.
 
 Recall from my previous post the motivation for looking at SSH reverse port forwarding, in the first place: I wanted to expose services from my home network that would otherwise get blocked by NAT.
 
-This ties in with an idea I've had for a game for a while. It's supposed to run on on your computer, but is controlled remotely through the phone (or a separate browser window), with interactivity that connects and synchronizes both ends. They could be running on the same network, but maybe people use their phone on cellular data, with a wildly different [ASN](<https://en.wikipedia.org/wiki/Autonomous_system_(Internet)>) backing it up.
+This ties in with an idea I've had for a game for a while. It's supposed to run on on your computer, but is controlled remotely through the phone (or a separate browser window), with interactivity that connects and synchronizes both ends. They could be running on the same network, but maybe people use their phone on cellular data, therefore having a completely different [ASN](<https://en.wikipedia.org/wiki/Autonomous_system_(Internet)>) backing it up.
 
-If you are familiar with [WebRTC](https://en.wikipedia.org/wiki/WebRTC), you might be thinking that this isn't so different from a [TURN server](https://webrtc.org/getting-started/turn-server), and it's definitely similar! But for my project, I think that an SSH-based solution might work better:
+Well, what if you could connect your phone to your computer without worrying about NAT? What if it was as simple as accessing a web page? What if the phone interactions were as simple as touching buttons in a mobile-first webapp?
+
+If you've played any of the [Jackbox Party games](https://en.wikipedia.org/wiki/The_Jackbox_Party_Pack), you're already familiar with this kind of architecture (and it was one of the inspirations for this idea!). The only difference is that a single device will connect to the game, instead of multiple players.
+
+On the other hand, if you are familiar with [WebRTC](https://en.wikipedia.org/wiki/WebRTC), you might be thinking that this isn't so different from a [TURN server](https://webrtc.org/getting-started/turn-server), and it's definitely similar! But for my project, I think that an SSH-based solution might work better:
 
 - Setting up a new sish instance for my project is not very complicated, whereas WebRTC makes me want to pull my hair.
-- I was already planning on using HTML for the mobile interface (instead of a native app, for instance), so a hypermedia-driven library like HTMX may suit my needs better than translating the plain data that WebRTC sends.
+- I was already planning on using HTML for the mobile interface (instead of, say, [a native app](https://aws.amazon.com/compare/the-difference-between-web-apps-native-apps-and-hybrid-apps/)), so a hypermedia-driven library like HTMX may suit my needs better than translating the plain data that WebRTC sends.
   - However, it'd still require some Javascript on the mobile end of it, for things like [rollback netcode](https://en.wikipedia.org/wiki/Netcode#Rollback).
-- SSH already comes with built-in authentication and encryption mechanisms, meaning that I wouldn't have to roll out my own. (In fact, the people who are behind sish and tuns.sh leverage this – plus _forward_ TCP connections – [to create SSH tunnel-based logins for services](https://pico.sh/tunnels).)
+- SSH already comes with built-in authentication and encryption mechanisms, meaning that I wouldn't have to roll out my own. (In fact, the people behind sish and tuns.sh leverage this feature of SSH – plus _forward_ TCP connections – to create [tunnel-based logins for services](https://pico.sh/tunnels).)
+- The dependency on SSH is transparent, letting me work on the communication channel as if it were a plain webserver – or if it were any other application, for that matter. There is no lock-in to a specific technology like there is with WebRTC.
+- Since I plan on having a web interface on the mobile device anyway, this scheme avoids adding extra logic for a separate webserver. The sish proxy will only handle upgrading our HTTP connection to HTTPS essentially, and the webserver can be embedded on the computer application, similarly to a [thin client](https://en.wikipedia.org/wiki/Thin_client).
 
-TO-DO
+With that said, there's nothing inherently wrong with WebRTC though (other than it being [a complex mess of protocols](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols)), and I'm not dismissing it straight away for this project.
+
+Our chosen path still has disadvantages too, one of them being that I'd be forced to use [TCP](https://en.wikipedia.org/wiki/Transmission_Control_Protocol) for communication. But since having a web interface on the remote controller was already part of the initial idea, that'd be unavoidable even if I picked WebRTC for the project. Another challenge is the added overhead of the proxy server, but with proper latency-based rollback, this can be mitigated – and isn't so different from what would happen with a TURN server, really.
+
+One final bonus that we get over a traditional client-server architecture is keeping the responsibilities as they should actually be. Normally, this kind of game would require a central server that coordinates with two or more clients – the computer running the game, and the mobile phone(s) running the controller. With remote port forwarding, the computer **will** be the server, exposed directly through the Internet. The mobile phone will be a regular client of that server, and there's no opaque abstraction over their communication other than HTTP itself.
+
+I've had this idea for a while now, but I was struggling to make it work with a traditional server-side architecture. It turns out that I don't need to implement anything myself. sish is configurable enough that it can serve many purposes, be it hosting multiple services, or managing multiple game connections. And for my project, it's definitely a viable solution that I'll look more into.
+
+But for now, that's all I have to say about it. I hope that this blog post has given a good insight about the inner workings of SSH, and perhaps even gave you ideas to try out yourself!
diff --git a/src/content/blog/taken-in-breakdown.mdx b/src/content/blog/taken-in-breakdown.mdx
index cb7695d..16b2c3b 100644
--- a/src/content/blog/taken-in-breakdown.mdx
+++ b/src/content/blog/taken-in-breakdown.mdx
@@ -3,7 +3,7 @@ title: "Taken In: Story breakdown!"
 pubDate: 2024-01-23
 isAgeRestricted: true
 authors: bad-manners
-thumbnail: /src/assets/thumbnails/other/taken_in_breakdown.png
+thumbnail: /src/assets/thumbnails/blog/taken_in_breakdown.png
 description: |
   First time annotating a vore story; in this case, [Taken In](/stories/taken-in). Here, I go over my writing process while offering additional tidbits of information.
 posts:
diff --git a/src/content/games/crossing-over.md b/src/content/games/crossing-over.md
index a8f7a70..e48b519 100644
--- a/src/content/games/crossing-over.md
+++ b/src/content/games/crossing-over.md
@@ -4,7 +4,7 @@ pubDate: 2024-02-28
 authors: bad-manners
 contentWarning: >
   This visual novel is a game about death, fishing, and vore. It contains purely fictional content deemed inappropriate for minors. It also deals with heavy subject matters like depression, abuse, and suicide, which may be unsuitable for some audiences. If you continue, you acknowledge that you're an adult, and accept responsibility for your actions.
-thumbnail: /src/assets/thumbnails/game_crossing_over_cover.png
+thumbnail: /src/assets/thumbnails/games/crossing_over_cover.png
 description: |
   [**Crossing Over**](https://bad-manners.itch.io/crossing-over) is the story of a soul and their journey towards the Hereafter. With the help of Marco, a soul ferrier, they will remember their past and fish unexpected objects out of the ethereal river.
 
diff --git a/src/content/stories/accommodation.md b/src/content/stories/accommodation.md
index 3ebdd19..0566b10 100644
--- a/src/content/stories/accommodation.md
+++ b/src/content/stories/accommodation.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 4800
 contentWarning: >
   Contains: Non-fatal size difference anal vore, with unwilling to willing female okapi predator, unwilling to willing male gray wolf prey, and long-term endosoma. Also includes straight sexual situations.
-thumbnail: /src/assets/thumbnails/bm_5_accommodation.png
+thumbnail: /src/assets/thumbnails/stories/bm_5_accommodation.png
 description: |
   Cynthia accidentally ends up with what initially seems like a pain in the butt, but turns out to be the opposite.
 
diff --git a/src/content/stories/addictive-additions.md b/src/content/stories/addictive-additions.md
index 8a12cd5..9d64be6 100644
--- a/src/content/stories/addictive-additions.md
+++ b/src/content/stories/addictive-additions.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 11200
 contentWarning: >
   Contains: Non-fatal oral vore and anal vore, with willing pred and multiple consensual similar sized prey (both willing, and semi-willing to unwilling in partial vore), and implied perma endo with trait theft. Also includes consensual sexual situations (M/M, M/F), hyper, male pregnancy worship, marriage play, clothing play, and semi-public lewdness.
-thumbnail: /src/assets/thumbnails/bm_comm_1_addictive_additions.png
+thumbnail: /src/assets/thumbnails/stories/bm_comm_1_addictive_additions.png
 description: |
   A couple meets a new, kinky acquaintance at a party, who's more than happy to take control for them.
 
diff --git a/src/content/stories/annivoresary.md b/src/content/stories/annivoresary.md
index 38fb640..6597171 100644
--- a/src/content/stories/annivoresary.md
+++ b/src/content/stories/annivoresary.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 3000
 contentWarning: >
   Contains: willing, non-fatal oral vore, with smaller male anthro fox pred, and larger male anthro wolf prey. Also includes bondage and sexual situations.
-thumbnail: /src/assets/thumbnails/bm_3_annivoresary.png
+thumbnail: /src/assets/thumbnails/stories/bm_3_annivoresary.png
 description: |
   Happy Vore Day! These two boyfriends certainly have been awaiting this date eagerly...
 posts:
diff --git a/src/content/stories/better-in-bully-batter.md b/src/content/stories/better-in-bully-batter.md
index 238f6e9..b7132ce 100644
--- a/src/content/stories/better-in-bully-batter.md
+++ b/src/content/stories/better-in-bully-batter.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 19100
 contentWarning: >
   Contains: Non-fatal similar size cock vore, with willing pred, multiple unwilling/semi-willing prey, and implied perma endo with trait theft. Also includes consensual sexual situations (M/M, M/F), hyper, cock sizeplay, netorare/cuckoldry and marriage play, cum inflation and weight gain, auto-fellatio, public sexual situations, and public vore.
-thumbnail: /src/assets/thumbnails/bm_comm_2_better_in_bully_batter.png
+thumbnail: /src/assets/thumbnails/stories/bm_comm_2_better_in_bully_batter.png
 description: |
   Years after graduating, a whole week to meet with former high-school classmates can lead to certain developments. Some more salacious, and others completely unexpected. And for a former bully, it happens to be both.
 
diff --git a/src/content/stories/big-haul.md b/src/content/stories/big-haul.md
index a5c4492..1662dde 100644
--- a/src/content/stories/big-haul.md
+++ b/src/content/stories/big-haul.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 9100
 contentWarning: >
   Contains: Non-fatal size difference unbirth, with semi-willing trans male anthro lemur pred, and unwilling cis male anthro sergal prey. Also includes gay sex with trans and cis characters, fatfur, daddy play, implied long-term endosoma, booze, and rudeness.
-thumbnail: /src/assets/thumbnails/bm_16_big_haul.png
+thumbnail: /src/assets/thumbnails/stories/bm_16_big_haul.png
 description: |
   Coming back empty-handed from his latest heist, a space pirate ends up getting his hands on an even better haul.
 posts:
diff --git a/src/content/stories/birdroom.md b/src/content/stories/birdroom.md
index 07cafa7..fb1c8e1 100644
--- a/src/content/stories/birdroom.md
+++ b/src/content/stories/birdroom.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 3000
 contentWarning: >
   Contains: non-fatal similar size anal vore, willing male feral gryphon pred, willing male anthro mimic hybrid prey, gay sex.
-thumbnail: /src/assets/thumbnails/bm_10_birdroom.png
+thumbnail: /src/assets/thumbnails/stories/bm_10_birdroom.png
 description: |
   Beetle finds an odd-shaped friend deep in his work, and does what he does best: be a messy distraction.
 
diff --git a/src/content/stories/bladder-filler.md b/src/content/stories/bladder-filler.md
index 0aa8b21..c57deda 100644
--- a/src/content/stories/bladder-filler.md
+++ b/src/content/stories/bladder-filler.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1900
 contentWarning: >
   Contains: non-fatal size difference cock vore to clean bladder, with willing male feral gryphon pred, and willing 2nd person PoV feral dragon prey. Also includes implied long-term endosoma.
-thumbnail: /src/assets/thumbnails/bm_ff_15_bladder_filler.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_15_bladder_filler.png
 description: |
   Always watch what you wish for... Blather the right thing to the right gryphon, and you might wash up in a bladder!
 
diff --git a/src/content/stories/bottom-of-the-food-chain.md b/src/content/stories/bottom-of-the-food-chain.md
index f3d9ac4..2d7fb35 100644
--- a/src/content/stories/bottom-of-the-food-chain.md
+++ b/src/content/stories/bottom-of-the-food-chain.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 4500
 contentWarning: >
   Contains: Non-fatal size difference oral vore, with semi-willing male feral snake pred and semi-willing feral vole prey.
-thumbnail: /src/assets/thumbnails/bm_14_bottom_of_the_food_chain.png
+thumbnail: /src/assets/thumbnails/stories/bm_14_bottom_of_the_food_chain.png
 description: |
   A unique opportunity falls onto Muno's coils, but he's not too pleased about it...
 
diff --git a/src/content/stories/butting-into-their-plans.md b/src/content/stories/butting-into-their-plans.md
index a0f078b..0ce8246 100644
--- a/src/content/stories/butting-into-their-plans.md
+++ b/src/content/stories/butting-into-their-plans.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1400
 contentWarning: >
   Contains: first person point-of-view, non-fatal size difference anal vore, willing male feral drake pred, willing PoV ambiguous anthro prey, rimming.
-thumbnail: /src/assets/thumbnails/bm_ff_11_butting_into_their_plans.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_11_butting_into_their_plans.png
 description: |
   With other plans for tonight, a drake unwittingly ends up becoming the main attraction for someone else... Not that he'd complain.
 
diff --git a/src/content/stories/delicacy-s-dare.md b/src/content/stories/delicacy-s-dare.md
index 3b793bd..633da56 100644
--- a/src/content/stories/delicacy-s-dare.md
+++ b/src/content/stories/delicacy-s-dare.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 8000
 contentWarning: >
   Contains: Non-fatal micro oral vore, with willing male deer predator, willing to unwilling male dragon prey, and messy stomach with food digestion.
-thumbnail: /src/assets/thumbnails/bm_6_delicacy_s_dare.png
+thumbnail: /src/assets/thumbnails/stories/bm_6_delicacy_s_dare.png
 description: |
   Alqpra is willing to get his hands dirty in order to settle a score with Sonos... And a lot more than just his hands.
 
diff --git a/src/content/stories/eggs-for-months.md b/src/content/stories/eggs-for-months.md
index 2000639..0e35ea1 100644
--- a/src/content/stories/eggs-for-months.md
+++ b/src/content/stories/eggs-for-months.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 7700
 contentWarning: >
   Contains: size difference, non-fatal sheath vore, with male feral gryphon pred and female anthro crow prey. Also includes dubious consent sex scenes, and lots of egg play and insertion (oral, cock, vaginal).
-thumbnail: /src/assets/thumbnails/bm_1_eggs_for_months.png
+thumbnail: /src/assets/thumbnails/stories/bm_1_eggs_for_months.png
 description: |
   Avelia needs help with a curse, but things quickly take a weird turn. Egg shenanigans ensue.
 posts:
diff --git a/src/content/stories/engaging-contacts.md b/src/content/stories/engaging-contacts.md
index 32ba22f..5fe4a67 100644
--- a/src/content/stories/engaging-contacts.md
+++ b/src/content/stories/engaging-contacts.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 6000
 contentWarning: >
   Contains: Non-fatal size difference oral vore and unbirth, with willing female wyvern pred and multiple unwilling/semi-willing/willing female anthro prey. Also includes lesbian sex, pet play, and implied full tour.
-thumbnail: /src/assets/thumbnails/bm_11_engaging_contacts.png
+thumbnail: /src/assets/thumbnails/stories/bm_11_engaging_contacts.png
 description: |
   An innocuous message might have a more obscure purpose than it initially appears. But Sally is not the first one to make that mistake tonight...
 
diff --git a/src/content/stories/flavorful-favor.md b/src/content/stories/flavorful-favor.md
index f234fd4..a21c23f 100644
--- a/src/content/stories/flavorful-favor.md
+++ b/src/content/stories/flavorful-favor.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 9400
 contentWarning: >
   Contains: Non-fatal oral vore, cock vore, and unbirth, with willing male gryphon predator, willing/semi-willing smaller female kobold predator/prey, and unwilling/semi-willing micro male mouse prey. Also includes full tour, prey transfer (cock to womb), and straight/gay sexual situations.
-thumbnail: /src/assets/thumbnails/bm_8_flavorful_favor.png
+thumbnail: /src/assets/thumbnails/stories/bm_8_flavorful_favor.png
 description: |
   One, fearful and furry; one, formidable and feathery; and one, fired-up for the follow-up. A threesome fatefully forced into a fantastical face-to-face.
 
diff --git a/src/content/stories/for-the-night.md b/src/content/stories/for-the-night.md
index 12d5189..53b4bc3 100644
--- a/src/content/stories/for-the-night.md
+++ b/src/content/stories/for-the-night.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1500
 contentWarning: >
   Contains: non-fatal same size anal vore, willing anthro male dog pred, willing anthro female pony prey, straight anal sex, threesome, sexuality play.
-thumbnail: /src/assets/thumbnails/bm_ff_7_for_the_night.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_7_for_the_night.png
 description: |
   Sometimes, what a couple needs is a third-party to spice things up. And Brand will make sure that this night is unforgettable.
 posts:
diff --git a/src/content/stories/gentle-and-cruel.md b/src/content/stories/gentle-and-cruel.md
index ad36fc9..04a81ba 100644
--- a/src/content/stories/gentle-and-cruel.md
+++ b/src/content/stories/gentle-and-cruel.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 5200
 contentWarning: >
   Contains: Non-fatal size difference oral vore, with gentle male anthro badger pred, cruel monster pred, and willing to unwilling male anthro lynx prey. Also includes regurgitation, aftercare, thriller/horror scenes, and implied transformation.
-thumbnail: /src/assets/thumbnails/bm_15_gentle_and_cruel.png
+thumbnail: /src/assets/thumbnails/stories/bm_15_gentle_and_cruel.png
 description: |
   Jack celebrates a very special date with his boyfriend, but his secret might put Abdis in jeopardy...
 
diff --git a/src/content/stories/hate-to-sea-it.md b/src/content/stories/hate-to-sea-it.md
index ff5fb7a..60dae3d 100644
--- a/src/content/stories/hate-to-sea-it.md
+++ b/src/content/stories/hate-to-sea-it.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1200
 contentWarning: >
   Contains: non-fatal size difference unbirth, willing female feral orca pred, unwilling to semi-willing male feral dolphin prey, straight sex, hate sex.
-thumbnail: /src/assets/thumbnails/bm_ff_12_hate_to_sea_it.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_12_hate_to_sea_it.png
 description: |
   Sometimes, a date simply doesn't work out at all. And sometimes, they're both too petty and stubborn to simply give up on it.
 
diff --git a/src/content/stories/hungry-for-love.md b/src/content/stories/hungry-for-love.md
index bcfabb7..b797d02 100644
--- a/src/content/stories/hungry-for-love.md
+++ b/src/content/stories/hungry-for-love.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 5900
 contentWarning: >
   Contains: Non-fatal size difference oral vore, with willing male spider predator, and willing female badger prey. Also includes straight sexual situations.
-thumbnail: /src/assets/thumbnails/bm_7_hungry_for_love.png
+thumbnail: /src/assets/thumbnails/stories/bm_7_hungry_for_love.png
 description: |
   Aloe has been bitten by the Valentine's Day bug, though her plans for some kinky fun go through an unforeseen change.
 posts:
diff --git a/src/content/stories/hyper-hunger.md b/src/content/stories/hyper-hunger.md
index 1ed13a4..0627292 100644
--- a/src/content/stories/hyper-hunger.md
+++ b/src/content/stories/hyper-hunger.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1300
 contentWarning: >
   Contains: non-fatal size difference oral vore, willing anthro ambiguous male pred, unwilling feral dog prey, food stuffing, messy stomach with smells, hyper cock, auto-fellatio.
-thumbnail: /src/assets/thumbnails/bm_ff_1_hyper_hunger.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_1_hyper_hunger.png
 description: |
   Fulfilling some hungers sometimes requires some additional, unwilling help.
 posts:
diff --git a/src/content/stories/insistence-and-assistance.md b/src/content/stories/insistence-and-assistance.md
index 4edeb9c..09fee5f 100644
--- a/src/content/stories/insistence-and-assistance.md
+++ b/src/content/stories/insistence-and-assistance.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1200
 contentWarning: >
   Contains: non-fatal same size oral vore, semi-willing anthro male cat pred, unwilling anthro male mouse prey, burping, regurgitation, force feeding, voyeurism.
-thumbnail: /src/assets/thumbnails/bm_ff_3_insistence_and_assistance.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_3_insistence_and_assistance.png
 description: |
   Some are predators, some are prey. And some want to keep things that way.
 posts:
diff --git a/src/content/stories/lactation-action.md b/src/content/stories/lactation-action.md
index 0f827fc..7bcc085 100644
--- a/src/content/stories/lactation-action.md
+++ b/src/content/stories/lactation-action.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1400
 contentWarning: >
   Contains: non-fatal micro nipple vore and oral vore, willing anthro female ferret pred, willing anthro male brown bear prey/pred, willing/asleep anthro female seagull prey, breast play, shrinking and growing, lactation, breastfeeding, prey transfer, growing in stomach to same size, burping.
-thumbnail: /src/assets/thumbnails/bm_ff_6_lactation_action.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_6_lactation_action.png
 description: |
   Amy doesn't shirk from her friend's shrinking plans, hoping to milk as much fun as possible.
 posts:
diff --git a/src/content/stories/latest-catch.md b/src/content/stories/latest-catch.md
index 47f2322..ebca4c0 100644
--- a/src/content/stories/latest-catch.md
+++ b/src/content/stories/latest-catch.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1500
 contentWarning: >
   Contains: non-fatal size difference cock vore, willing to semi-willing anthro non-binary rabbit pred, willing feral snake prey, masturbation, mouthplay, implied perma endo.
-thumbnail: /src/assets/thumbnails/bm_ff_5_latest_catch.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_5_latest_catch.png
 description: |
   A predatory rabbit likes snakes...perhaps a bit too much.
 posts:
diff --git a/src/content/stories/never-too-late.md b/src/content/stories/never-too-late.md
index 0c8178f..823f9d8 100644
--- a/src/content/stories/never-too-late.md
+++ b/src/content/stories/never-too-late.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1100
 contentWarning: >
   Contains: non-fatal same size cock vore, asleep anthro male horse pred, willing anthro female aardwolf prey, masturbation, fellatio, alcohol.
-thumbnail: /src/assets/thumbnails/bm_ff_2_never_too_late.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_2_never_too_late.png
 description: |
   After a night full of fun, Mirembe tries to squeeze out the last pleasures she can.
 posts:
diff --git a/src/content/stories/noble-fire.md b/src/content/stories/noble-fire.md
index a63f1c3..1216f74 100644
--- a/src/content/stories/noble-fire.md
+++ b/src/content/stories/noble-fire.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 6900
 contentWarning: >
   Contains: Non-fatal same size oral vore, with willing male anthro lion pred and semi-willing male anthro dog prey. Also includes sexual nudity and heavy themes like violence, blood, trauma, abuse, and fear.
-thumbnail: /src/assets/thumbnails/bm_12_noble_fire.png
+thumbnail: /src/assets/thumbnails/stories/bm_12_noble_fire.png
 description: |
   Blume brings his new acquaintance to his secret retreat, escaping from both the frost and the past snapping at their heels.
 
diff --git a/src/content/stories/overzealous-zenko.md b/src/content/stories/overzealous-zenko.md
index f9baa7a..62afd62 100644
--- a/src/content/stories/overzealous-zenko.md
+++ b/src/content/stories/overzealous-zenko.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 4900
 contentWarning: >
   Contains: Non-fatal size difference chest maw vore, with willing male kitsune-human centaur pred, unwilling female human prey, and implied perma endo.
-thumbnail: /src/assets/thumbnails/bm_r_1_overzealous_zenko.png
+thumbnail: /src/assets/thumbnails/stories/bm_r_1_overzealous_zenko.png
 description: |
   Under the pressure of following in his father's footsteps, Kuronosuke finds an unfortunate soul and offers his hospitality, whether she wants it or not.
 
diff --git a/src/content/stories/part-of-the-show.md b/src/content/stories/part-of-the-show.md
index 6d75547..0ecc69e 100644
--- a/src/content/stories/part-of-the-show.md
+++ b/src/content/stories/part-of-the-show.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 2000
 contentWarning: >
   Contains: non-fatal same size public oral vore, with willing male anthro mimic/maned wolf hybrid pred, semi-willing 2nd person PoV anthro prey. Also includes pole-dancing and mentions of alcohol.
-thumbnail: /src/assets/thumbnails/bm_ff_14_part_of_the_show.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_14_part_of_the_show.png
 description: |
   You attend a show, unaware of how personal it can turn out...
 
diff --git a/src/content/stories/pet-sit-saturday.md b/src/content/stories/pet-sit-saturday.md
index ad4e42f..49602b3 100644
--- a/src/content/stories/pet-sit-saturday.md
+++ b/src/content/stories/pet-sit-saturday.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 11000
 contentWarning: >
   Contains: same size, non-fatal anal vore, with female anthro elephant pred, female anthro anteater unwilling prey, and female feral zorgoia pred/willing prey. Also includes object vore (anal), prey transfer, and masturbation.
-thumbnail: /src/assets/thumbnails/bm_2_pet_sit_saturday.png
+thumbnail: /src/assets/thumbnails/stories/bm_2_pet_sit_saturday.png
 description: |
   It's Hepje's first time pet-sitting, and a huge zorgoia might be too much for the inexperienced anteater...
 posts:
diff --git a/src/content/stories/playing-it-safe.md b/src/content/stories/playing-it-safe.md
index fb5bc2c..c2c9b84 100644
--- a/src/content/stories/playing-it-safe.md
+++ b/src/content/stories/playing-it-safe.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 9900
 contentWarning: >
   Contains: Size difference safe vore/endosoma (cock vore, unbirth, oral vore), with willing male anthro rhinoceros predator, willing female anthro beaver predator, and mostly willing male anthro squirrel prey. Also includes straight sex, prey transfer, condom filling with inflation and vore, implied full tour, office setting, and many puns.
-thumbnail: /src/assets/thumbnails/bm_20_playing_it_safe.png
+thumbnail: /src/assets/thumbnails/stories/bm_20_playing_it_safe.png
 description: |
   A white-collar squirrel asks his boss about a raise, but he gets far more than he bargained for.
 
diff --git a/src/content/stories/reaching-for-the-full-moon.md b/src/content/stories/reaching-for-the-full-moon.md
index a09fc5d..deebcc2 100644
--- a/src/content/stories/reaching-for-the-full-moon.md
+++ b/src/content/stories/reaching-for-the-full-moon.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1300
 contentWarning: >
   Contains: non-fatal oral vore, smaller unwilling anthro male rabbit pred, bigger willing werewolf prey, forced vore, role reversal.
-thumbnail: /src/assets/thumbnails/bm_ff_9_reaching_for_the_full_moon.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_9_reaching_for_the_full_moon.png
 description: |
   Mark finds himself on both ends of a monstrous hunt.
 
diff --git a/src/content/stories/rose-s-binge.md b/src/content/stories/rose-s-binge.md
index e4d907b..506f9a7 100644
--- a/src/content/stories/rose-s-binge.md
+++ b/src/content/stories/rose-s-binge.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 7000
 contentWarning: >
   Contains: Non-fatal oral vore, anal vore, and partial cock vore, with willing and cruel androgynous anthro plushie dragon predator, and multiple smaller unwilling human preys. Also includes perma endo, mass vore, limited hammerspace vore, hyper cock/balls/breasts, non-human genitalia, autofellatio, and fatfur.
-thumbnail: /src/assets/thumbnails/bm_comm_5_rose_s_binge.png
+thumbnail: /src/assets/thumbnails/stories/bm_comm_5_rose_s_binge.png
 description: |
   A seemingly insatiable plushie makes short work of an entire town, and the last few people left will try their best to escape from their binge.
 
diff --git a/src/content/stories/ruffling-some-feathers.md b/src/content/stories/ruffling-some-feathers.md
index 388335c..1f0684d 100644
--- a/src/content/stories/ruffling-some-feathers.md
+++ b/src/content/stories/ruffling-some-feathers.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1000
 contentWarning: >
   Contains: non-fatal size difference oral vore, willing feral male owl pred, semi-willing feral male snake prey.
-thumbnail: /src/assets/thumbnails/bm_ff_10_ruffling_some_feathers.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_10_ruffling_some_feathers.png
 description: |
   A sneaky snake switches to sudden snack, since he stopped Sovinne's sleep.
 
diff --git a/src/content/stories/spontaneous-sleepover.md b/src/content/stories/spontaneous-sleepover.md
index c045913..e00743b 100644
--- a/src/content/stories/spontaneous-sleepover.md
+++ b/src/content/stories/spontaneous-sleepover.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1300
 contentWarning: >
   Contains: non-fatal same size tail vore, willing anthro male squirrel pred, willing anthro female stoat prey, unwilling anthro female pangolin prey, public vore.
-thumbnail: /src/assets/thumbnails/bm_ff_8_spontaneous_sleepover.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_8_spontaneous_sleepover.png
 description: |
   Akene is tired after a long trip, and her new acquaintance is happy to provide a comfy bed - despite their mutual friend's protestations.
 posts:
diff --git a/src/content/stories/taken-in.md b/src/content/stories/taken-in.md
index 800a457..be487e7 100644
--- a/src/content/stories/taken-in.md
+++ b/src/content/stories/taken-in.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 5900
 contentWarning: >
   Contains: Non-fatal same size oral vore, with willing male feral hybrid pred (mimic x maned wolf), unwilling PoV anthro prey, and full tour.
-thumbnail: /src/assets/thumbnails/bm_17_taken_in.png
+thumbnail: /src/assets/thumbnails/stories/bm_17_taken_in.png
 description: |
   A silly little story where I re-imagine my sona as a feral! It was a fun concept to play with. One more quick PoV story before I go back to the usual 3rd person narration style. I hope you still enjoy it!
 posts:
diff --git a/src/content/stories/tasting-high-consequences.md b/src/content/stories/tasting-high-consequences.md
index e169d28..78a5b9b 100644
--- a/src/content/stories/tasting-high-consequences.md
+++ b/src/content/stories/tasting-high-consequences.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 6000
 contentWarning: >
   Contains: non-fatal oral vore, with willing feral female boar pred, unwilling similar size anthro female moth-dragon hybrid prey, and unwilling micro anthro female serpent prey. Also includes object vore, fantasy combat, and cannabis.
-thumbnail: /src/assets/thumbnails/bm_9_tasting_high_consequences.png
+thumbnail: /src/assets/thumbnails/stories/bm_9_tasting_high_consequences.png
 description: |
   Cabira's plans for a blazing night go up in smoke.
 
diff --git a/src/content/stories/team-building.md b/src/content/stories/team-building.md
index 12c9a9d..bb667fe 100644
--- a/src/content/stories/team-building.md
+++ b/src/content/stories/team-building.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 15100
 contentWarning: >
   Contains: Non-fatal same size cock vore and anal vore, with willing male anthro monkey pred, willing male anthro gorilla pred, multiple same-size willing male anthro prey. Also includes casual public mass vore, prey transfer, long-term endosoma, hyper cock and balls, hyper and muscle growth, hyper cum inflation, cock worship, casual public gay sex, size difference play, bench-pressing, and voyeurism.
-thumbnail: /src/assets/thumbnails/bm_comm_4_team_building.png
+thumbnail: /src/assets/thumbnails/stories/bm_comm_4_team_building.png
 description: |
   After another semester in college, Yolk finds new opportunities to surpass his limits and grow closer to his friends than ever.
 
diff --git a/src/content/stories/team-effort.md b/src/content/stories/team-effort.md
index 3c11d9e..0a6828d 100644
--- a/src/content/stories/team-effort.md
+++ b/src/content/stories/team-effort.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 11600
 contentWarning: >
   Contains: Non-fatal same size cock vore, with semi-willing to willing male anthro monkey pred, multiple willing male anthro prey, and long-term endosoma. Also includes hyper cock growth, cock worship, hyper cum inflation, public vore, casual gay sex (oral and anal sex; same size, size difference), and public sex.
-thumbnail: /src/assets/thumbnails/bm_r_2_team_effort.png
+thumbnail: /src/assets/thumbnails/stories/bm_r_2_team_effort.png
 description: |
   Yolk is ready to kick back and relax during the winter break, but his friends really want to keep him company.
 
diff --git a/src/content/stories/the-last-livestream.md b/src/content/stories/the-last-livestream.md
index 62dd7a2..cd4a03e 100644
--- a/src/content/stories/the-last-livestream.md
+++ b/src/content/stories/the-last-livestream.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1400
 contentWarning: >
   Contains: non-fatal similar size unbirth, willing anthro female coatimundi pred, willing anthro female fennec fox prey, masturbation, livestreamed vore.
-thumbnail: /src/assets/thumbnails/bm_ff_4_the_last_livestream.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_4_the_last_livestream.png
 description: |
   Happy Vore Day! These two boyfriends certainly have been awaiting this date eagerly...
 posts:
diff --git a/src/content/stories/the-lost-of-the-marshes/bonus-1-quince-s-fantasy.md b/src/content/stories/the-lost-of-the-marshes/bonus-1-quince-s-fantasy.md
index 6d7dece..3c6754b 100644
--- a/src/content/stories/the-lost-of-the-marshes/bonus-1-quince-s-fantasy.md
+++ b/src/content/stories/the-lost-of-the-marshes/bonus-1-quince-s-fantasy.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 5800
 contentWarning: >
   Contains: macro and size difference, non-fatal oral vore. Also includes dream scenarios, role reversal, and self-vore.
-thumbnail: /src/assets/thumbnails/tlotm_bonus_1.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_bonus_1.png
 description: |
   This is a bonus chapter of The Lost of the Marshes, set between [Chapter 4 – Change](/stories/the-lost-of-the-marshes/chapter-4) and [Chapter 5 – Intersection](/stories/the-lost-of-the-marshes/chapter-5).
 
diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-1.md b/src/content/stories/the-lost-of-the-marshes/chapter-1.md
index 64b1705..393767a 100644
--- a/src/content/stories/the-lost-of-the-marshes/chapter-1.md
+++ b/src/content/stories/the-lost-of-the-marshes/chapter-1.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 7300
 contentWarning: >
   Contains: macro, non-fatal oral vore.
-thumbnail: /src/assets/thumbnails/tlotm_ch1.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_ch1.png
 description: |
   Hey! This is my first vore story, and the first story I've written in years. It's also the first chapter in a vore series, The Lost of the Marshes, focusing on non-fatal vore. I've always wanted to do a project like this and I finally decided to actually do it, so enjoy!
 posts:
diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-10.md b/src/content/stories/the-lost-of-the-marshes/chapter-10.md
index 086b422..00168a7 100644
--- a/src/content/stories/the-lost-of-the-marshes/chapter-10.md
+++ b/src/content/stories/the-lost-of-the-marshes/chapter-10.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 14600
 contentWarning: >
   Contains: macro with non-fatal oral vore, anal vore, cock vore, and slit vore. Also includes sexual situations, slight blood, and heavy themes.
-thumbnail: /src/assets/thumbnails/tlotm_ch10.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_ch10.png
 description: |
   Extreme circumstances lead the trio far away from Logas, where a life and the truth are both at stake.
 
diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-11.md b/src/content/stories/the-lost-of-the-marshes/chapter-11.md
index b5cdd71..d6bdbe2 100644
--- a/src/content/stories/the-lost-of-the-marshes/chapter-11.md
+++ b/src/content/stories/the-lost-of-the-marshes/chapter-11.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 13600
 contentWarning: >
   Contains: non-fatal oral vore and cock vore, with size difference and macro. Also includes gay sexual situations.
-thumbnail: /src/assets/thumbnails/tlotm_ch11.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_ch11.png
 description: |
   Given a respite from their pursuers while they stay in Saisa, the gang reflects on what they themselves seek to pursue.
 posts:
diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-2.md b/src/content/stories/the-lost-of-the-marshes/chapter-2.md
index 9696b31..774b688 100644
--- a/src/content/stories/the-lost-of-the-marshes/chapter-2.md
+++ b/src/content/stories/the-lost-of-the-marshes/chapter-2.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 6900
 contentWarning: >
   Contains: macro, non-fatal oral vore, minor nudity.
-thumbnail: /src/assets/thumbnails/tlotm_ch2.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_ch2.png
 description: |
   Nikili and Quince get in some dragon-related antics once again, and their friendship gets tested.
 
diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-3.md b/src/content/stories/the-lost-of-the-marshes/chapter-3.md
index f33488b..e1d0141 100644
--- a/src/content/stories/the-lost-of-the-marshes/chapter-3.md
+++ b/src/content/stories/the-lost-of-the-marshes/chapter-3.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 10800
 contentWarning: >
   Contains: macro and size difference, non-fatal oral vore, role reversal.
-thumbnail: /src/assets/thumbnails/tlotm_ch3.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_ch3.png
 description: |
   Nikili and Quince make their way back home, but one of them decides to bite off more than they can chew.
 
diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-4.md b/src/content/stories/the-lost-of-the-marshes/chapter-4.md
index 482bf57..8a3c640 100644
--- a/src/content/stories/the-lost-of-the-marshes/chapter-4.md
+++ b/src/content/stories/the-lost-of-the-marshes/chapter-4.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 12000
 contentWarning: >
   Contains: size difference, non-fatal oral vore, live feeding, sexual nudity.
-thumbnail: /src/assets/thumbnails/tlotm_ch4.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_ch4.png
 description: |
   How hard is it to sneak a giant dragon into a village? Quince and Nikili might learn the answer sooner rather than later...
 
diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-5.md b/src/content/stories/the-lost-of-the-marshes/chapter-5.md
index 2e75939..480f26c 100644
--- a/src/content/stories/the-lost-of-the-marshes/chapter-5.md
+++ b/src/content/stories/the-lost-of-the-marshes/chapter-5.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 8600
 contentWarning: >
   Contains: size difference, non-fatal oral vore. Also includes heavy themes.
-thumbnail: /src/assets/thumbnails/tlotm_ch5.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_ch5.png
 description: |
   In a distant place full of passing faces, our protagonists are forced to face their demons, both without and within.
 
diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-6.md b/src/content/stories/the-lost-of-the-marshes/chapter-6.md
index 2237ff2..fadde3b 100644
--- a/src/content/stories/the-lost-of-the-marshes/chapter-6.md
+++ b/src/content/stories/the-lost-of-the-marshes/chapter-6.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 9000
 contentWarning: >
   Contains: size difference, non-fatal oral vore and unbirth, gay sex, masturbation.
-thumbnail: /src/assets/thumbnails/tlotm_ch6.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_ch6.png
 description: |
   Despite the chaos of their situations, the crew finds a brief reprieve in Kuir.
 
diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-7.md b/src/content/stories/the-lost-of-the-marshes/chapter-7.md
index 4845b5e..05a84ab 100644
--- a/src/content/stories/the-lost-of-the-marshes/chapter-7.md
+++ b/src/content/stories/the-lost-of-the-marshes/chapter-7.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 6500
 contentWarning: >
   Contains: macro and size difference, non-fatal oral and anal vore, gay sex.
-thumbnail: /src/assets/thumbnails/tlotm_ch7.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_ch7.png
 description: |
   Some mongooses, cats, and dragons just seem fated to attract trouble. And when the dust settles, emotions run high.
 
diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-8.md b/src/content/stories/the-lost-of-the-marshes/chapter-8.md
index 5f1b9f9..555e804 100644
--- a/src/content/stories/the-lost-of-the-marshes/chapter-8.md
+++ b/src/content/stories/the-lost-of-the-marshes/chapter-8.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 7500
 contentWarning: >
   Contains: macro and size difference, non-fatal oral and anal vore.
-thumbnail: /src/assets/thumbnails/tlotm_ch8.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_ch8.png
 description: |
   Nurta is forced to confront the ghosts of her past, once and for all.
 posts:
diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-9.md b/src/content/stories/the-lost-of-the-marshes/chapter-9.md
index 41bdc26..0564f33 100644
--- a/src/content/stories/the-lost-of-the-marshes/chapter-9.md
+++ b/src/content/stories/the-lost-of-the-marshes/chapter-9.md
@@ -6,7 +6,7 @@ authors: bad-manners
 wordCount: 11100
 contentWarning: >
   Contains: macro and size difference, non-fatal oral vore and slit vore. Also includes gay sexual situations, slight vomit, slight blood, and heavy themes.
-thumbnail: /src/assets/thumbnails/tlotm_ch9.png
+thumbnail: /src/assets/thumbnails/stories/tlotm_ch9.png
 description: |
   A whole week away from Kaati causes some uncertainties among the recluse trio.
 posts:
diff --git a/src/content/stories/tiny-accident.md b/src/content/stories/tiny-accident.md
index 087ecab..18db70f 100644
--- a/src/content/stories/tiny-accident.md
+++ b/src/content/stories/tiny-accident.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 3000
 contentWarning: >
   Contains: Non-fatal oral vore, with unwilling to willing anthro male rat predator, and unwilling micro anthro male wolf prey. Also includes implied regurgitation, masturbation, sizeplay, and unexpected micro groping.
-thumbnail: /src/assets/thumbnails/bm_18_tiny_accident.png
+thumbnail: /src/assets/thumbnails/stories/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...
 
diff --git a/src/content/stories/tomo-moku.md b/src/content/stories/tomo-moku.md
index 4a0f471..c704c35 100644
--- a/src/content/stories/tomo-moku.md
+++ b/src/content/stories/tomo-moku.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 1200
 contentWarning: >
   nanpa nimi li mute li kijetesantakalu kijetesantakalu kijetesantakalu kijetesantakalu kijetesantakalu. lipu li jo e ijo tu tu ni: moku musi pi moli ala kepeken uta; akesi li moku musi e soweli; akesi li wile e ni; soweli li wile ala e ni.
-thumbnail: /src/assets/thumbnails/bm_ff_13_tomo_moku.png
+thumbnail: /src/assets/thumbnails/stories/bm_ff_13_tomo_moku.png
 description: |
   soweli Lijan li kama sona e ni: ma tomo li kama jo e tomo moku sin. taso... moku li seme?
 
diff --git a/src/content/stories/trouble-sleeping.md b/src/content/stories/trouble-sleeping.md
index d319d47..9a6f221 100644
--- a/src/content/stories/trouble-sleeping.md
+++ b/src/content/stories/trouble-sleeping.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 4800
 contentWarning: >
   Contains: Non-fatal size difference unbirth, with asleep female anthro wolf pred and willing male feral sparrow prey. Also includes straight sex and accidental long-term endo.
-thumbnail: /src/assets/thumbnails/bm_13_trouble_sleeping.png
+thumbnail: /src/assets/thumbnails/stories/bm_13_trouble_sleeping.png
 description: |
   An aflutter sparrow can't get any rest, and seeks an unconventional refuge in his lupine crush.
 posts:
diff --git a/src/content/stories/warped-friendship.md b/src/content/stories/warped-friendship.md
index 3e9e992..60b300f 100644
--- a/src/content/stories/warped-friendship.md
+++ b/src/content/stories/warped-friendship.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 7800
 contentWarning: >
   Contains: Non-fatal same size oral vore, with willing male anthro fennec fox pred, unwilling to willing male anthro red panda prey, and long-term endo.
-thumbnail: /src/assets/thumbnails/bm_r_3_warped_friendship.png
+thumbnail: /src/assets/thumbnails/stories/bm_r_3_warped_friendship.png
 description: |
   Avour is sent to investigate a disturbance, but his finicky strategy is thwarted by a fennec-y trickster.
 
diff --git a/src/content/stories/within-limits.md b/src/content/stories/within-limits.md
index 63da3ff..36f361d 100644
--- a/src/content/stories/within-limits.md
+++ b/src/content/stories/within-limits.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 14500
 contentWarning: >
   Contains: non-fatal vore, with female taur unbirth (mass vore, hammerspace), male anthro cock vore, and multiple anthro + human willing prey. Also includes bigger prey, similar size prey, size difference, nested vore, prey transfer, hyper genitals, alien genitals, and a sci-fi orgy setting.
-thumbnail: /src/assets/thumbnails/bm_comm_3_within_limits.png
+thumbnail: /src/assets/thumbnails/stories/bm_comm_3_within_limits.png
 description: |
   Ushitora tries out her latest invention on herself, with a bunch of lewd and eager participants to help her out.
 posts:
diff --git a/src/content/stories/woofer-exploration.md b/src/content/stories/woofer-exploration.md
index 16ce4a4..4ecd731 100644
--- a/src/content/stories/woofer-exploration.md
+++ b/src/content/stories/woofer-exploration.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 2600
 contentWarning: >
   Contains: Non-fatal unbirth and oral vore, with willing anthro male maned wolf predator and willing micro anthro male mimic x maned wolf hybrid prey. Also includes gay sex, masturbation, and sleep play.
-thumbnail: /src/assets/thumbnails/bm_19_woofer_exploration.png
+thumbnail: /src/assets/thumbnails/stories/bm_19_woofer_exploration.png
 description: |
   The Director wakes up in the middle of the night to a little intruder, and decides to have some fun with him.
 
diff --git a/src/content/stories/you-re-home.md b/src/content/stories/you-re-home.md
index f06fd69..82cf586 100644
--- a/src/content/stories/you-re-home.md
+++ b/src/content/stories/you-re-home.md
@@ -5,7 +5,7 @@ authors: bad-manners
 wordCount: 11300
 contentWarning: >
   Contains: Unwilling, non-fatal oral vore, with similar size preds/preys, and implied permanent endosoma. Also includes sexual situations and masturbation, slight description of vomit, and a bunch of social anxiety.
-thumbnail: /src/assets/thumbnails/bm_4_you_re_home.png
+thumbnail: /src/assets/thumbnails/stories/bm_4_you_re_home.png
 description: |
   Vesper finds himself perplexed with his home situation, but when a friend offers to help, things quickly spiral out of control.
 posts:
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro
index 4e4190f..a0f0ffc 100644
--- a/src/pages/blog/index.astro
+++ b/src/pages/blog/index.astro
@@ -24,7 +24,7 @@ const posts = await Promise.all(
   <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
   <div class="my-4 flex w-full">
     <p class="p-summary grow">Posts on whatever has been rattling in my head as of late.</p>
-    <a class="u-url text-link ml-2 mr-10 p-2" href="/blog/feed.xml" rel="alternate" title="RSS feed" data-tooltip>
+    <a class="u-url text-link ml-2 p-1 md:mr-5" href="/blog/feed.xml" rel="alternate" title="RSS feed" data-tooltip>
       <IconSquareRSS width="2rem" height="2rem" />
       <span class="sr-only">RSS feed</span>
     </a>
diff --git a/src/pages/games/index.astro b/src/pages/games/index.astro
index 0df3c1b..3d0b6ec 100644
--- a/src/pages/games/index.astro
+++ b/src/pages/games/index.astro
@@ -24,7 +24,7 @@ const games = await Promise.all(
   <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
   <div class="my-4 flex w-full">
     <p class="p-summary grow">A game that I've gone and done.</p>
-    <a class="u-url text-link ml-2 mr-10 p-2" href="/games/feed.xml" rel="alternate" title="RSS feed" data-tooltip>
+    <a class="u-url text-link ml-2 p-1 md:mr-5" href="/games/feed.xml" rel="alternate" title="RSS feed" data-tooltip>
       <IconSquareRSS width="2rem" height="2rem" />
       <span class="sr-only">RSS feed</span>
     </a>
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 2777fa5..4b0ccf4 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -105,7 +105,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
         content that I'll make. You can check the latest uploads below, or use the navigation bar to dig through all of
         my content.
       </p>
-      <a class="u-url text-link ml-2 mr-10 p-2" href="/feed.xml" rel="alternate" title="RSS feed" data-tooltip>
+      <a class="u-url text-link ml-2 p-1 md:mr-5" href="/feed.xml" rel="alternate" title="RSS feed" data-tooltip>
         <IconSquareRSS width="2rem" height="2rem" />
         <span class="sr-only">RSS feed</span>
       </a>
diff --git a/src/pages/stories/[...page].astro b/src/pages/stories/[...page].astro
index aff14b2..9e2ab30 100644
--- a/src/pages/stories/[...page].astro
+++ b/src/pages/stories/[...page].astro
@@ -35,7 +35,7 @@ const totalPages = Math.ceil(page.total / page.size);
   <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
   <div class="my-4 flex">
     <p class="p-summary grow">The bulk of my content!</p>
-    <a class="u-url text-link ml-2 mr-10 p-2" href="/stories/feed.xml" rel="alternate" title="RSS feed" data-tooltip>
+    <a class="u-url text-link ml-2 p-1 md:mr-5" href="/stories/feed.xml" rel="alternate" title="RSS feed" data-tooltip>
       <IconSquareRSS width="2rem" height="2rem" />
       <span class="sr-only">RSS feed</span>
     </a>