From f763d5dc62a4ffe3a72c7f8ffe58de4a1f35a6bb Mon Sep 17 00:00:00 2001
From: MareStare <mare.stare.official@gmail.com>
Date: Sun, 16 Mar 2025 23:19:58 +0000
Subject: [PATCH 1/5] Remove the deprecated eslint and stylelint prettier
 plugins. Move prettier to the top level

---
 assets/.prettierrc.yml => .prettierrc.yml |   0
 assets/.stylelintrc.yml                   |   3 -
 assets/eslint.config.js                   |   2 -
 assets/js/boorujs.js                      |  28 ++---
 assets/js/shortcuts.ts                    |   5 +-
 assets/js/tags.ts                         |  38 +++++--
 assets/package-lock.json                  | 129 ----------------------
 assets/package.json                       |   4 -
 package-lock.json                         |  27 +++++
 package.json                              |   9 ++
 10 files changed, 79 insertions(+), 166 deletions(-)
 rename assets/.prettierrc.yml => .prettierrc.yml (100%)
 create mode 100644 package-lock.json
 create mode 100644 package.json

diff --git a/assets/.prettierrc.yml b/.prettierrc.yml
similarity index 100%
rename from assets/.prettierrc.yml
rename to .prettierrc.yml
diff --git a/assets/.stylelintrc.yml b/assets/.stylelintrc.yml
index c4eba511..78227ced 100644
--- a/assets/.stylelintrc.yml
+++ b/assets/.stylelintrc.yml
@@ -1,7 +1,5 @@
 ---
 extends: stylelint-config-recommended
-plugins:
-  - stylelint-prettier
 rules:
   block-no-empty: true
   at-rule-no-unknown:
@@ -75,4 +73,3 @@ rules:
   declaration-block-no-redundant-longhand-properties: true
   shorthand-property-no-redundant-values: true
   comment-whitespace-inside: always
-  prettier/prettier: true
diff --git a/assets/eslint.config.js b/assets/eslint.config.js
index aaae607d..1c3f2b75 100644
--- a/assets/eslint.config.js
+++ b/assets/eslint.config.js
@@ -1,11 +1,9 @@
 import tsEslint from 'typescript-eslint';
 import vitestPlugin from 'eslint-plugin-vitest';
-import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
 import globals from 'globals';
 
 export default tsEslint.config(
   ...tsEslint.configs.recommended,
-  eslintPluginPrettierRecommended,
   {
     name: 'PhilomenaConfig',
     files: ['**/*.js', '**/*.ts'],
diff --git a/assets/js/boorujs.js b/assets/js/boorujs.js
index 86d9901b..b60e2217 100644
--- a/assets/js/boorujs.js
+++ b/assets/js/boorujs.js
@@ -9,24 +9,22 @@ import { fetchHtml, handleError } from './utils/requests';
 import { showBlock } from './utils/image';
 import { addTag } from './tagsinput';
 
-/* eslint-disable prettier/prettier */
-
 // Event types and any qualifying conditions - return true to not run action
 const types = {
-  click(event)    { return event.button !== 0; /* Left-click only */ },
-  change()        { /* No qualifier */ },
+  click(event) { return event.button !== 0; /* Left-click only */ },
+  change() { /* No qualifier */ },
   fetchcomplete() { /* No qualifier */ },
 };
 
 const actions = {
-  hide(data)       { selectorCb(data.base, data.value, el => el.classList.add('hidden')); },
-  show(data)       { selectorCb(data.base, data.value, el => el.classList.remove('hidden')); },
-  toggle(data)     { selectorCb(data.base, data.value, el => el.classList.toggle('hidden')); },
-  submit(data)     { selectorCb(data.base, data.value, el => el.submit()); },
-  disable(data)    { selectorCb(data.base, data.value, el => el.disabled = true); },
-  focus(data)      { document.querySelector(data.value).focus(); },
-  unfilter(data)   { showBlock(data.el.closest('.image-show-container')); },
-  tabHide(data)    { selectorCbChildren(data.base, data.value, el => el.classList.add('hidden')); },
+  hide(data) { selectorCb(data.base, data.value, el => el.classList.add('hidden')); },
+  show(data) { selectorCb(data.base, data.value, el => el.classList.remove('hidden')); },
+  toggle(data) { selectorCb(data.base, data.value, el => el.classList.toggle('hidden')); },
+  submit(data) { selectorCb(data.base, data.value, el => el.submit()); },
+  disable(data) { selectorCb(data.base, data.value, el => el.disabled = true); },
+  focus(data) { document.querySelector(data.value).focus(); },
+  unfilter(data) { showBlock(data.el.closest('.image-show-container')); },
+  tabHide(data) { selectorCbChildren(data.base, data.value, el => el.classList.add('hidden')); },
   preventdefault() { /* The existence of this entry is enough */ },
 
   copy(data) {
@@ -61,8 +59,8 @@ const actions = {
 
   tab(data) {
     const block = data.el.parentNode.parentNode,
-          newTab = $(`.block__tab[data-tab="${data.value}"]`),
-          loadTab = data.el.dataset.loadTab;
+      newTab = $(`.block__tab[data-tab="${data.value}"]`),
+      loadTab = data.el.dataset.loadTab;
 
     // Switch tab
     const selectedTab = block.querySelector('.selected');
@@ -87,8 +85,6 @@ const actions = {
   },
 };
 
-/* eslint-enable prettier/prettier */
-
 // Use this function to apply a callback to elements matching the selectors
 function selectorCb(base = document, selector, cb) {
   [].forEach.call(base.querySelectorAll(selector), cb);
diff --git a/assets/js/shortcuts.ts b/assets/js/shortcuts.ts
index 3de21c89..52071b31 100644
--- a/assets/js/shortcuts.ts
+++ b/assets/js/shortcuts.ts
@@ -47,8 +47,7 @@ function isOK(event: KeyboardEvent): boolean {
   );
 }
 
-/* eslint-disable prettier/prettier */
-
+// prettier-ignore
 const keyCodes: ShortcutKeyMap = {
   74() { click('.js-prev');             }, // J - go to previous image
   73() { click('.js-up');               }, // I - go to index page
@@ -68,8 +67,6 @@ const keyCodes: ShortcutKeyMap = {
   },
 };
 
-/* eslint-enable prettier/prettier */
-
 export function listenForKeys() {
   document.addEventListener('keydown', (event: KeyboardEvent) => {
     if (isOK(event) && keyCodes[event.keyCode]) {
diff --git a/assets/js/tags.ts b/assets/js/tags.ts
index 8a0568fc..deb3c7bd 100644
--- a/assets/js/tags.ts
+++ b/assets/js/tags.ts
@@ -26,18 +26,40 @@ function createTagDropdown(tag: HTMLSpanElement) {
   const [unwatched, watched, spoilered, hidden] = $$<HTMLSpanElement>('.tag__state', tag);
   const tagId = parseInt(assertNotUndefined(tag.dataset.tagId), 10);
 
-  /* eslint-disable prettier/prettier */
   const actions: TagDropdownActionList = {
-    unwatch()   { hideEl(unwatch, watched);     showEl(watch, unwatched);     removeTag(tagId, watchedTagList);   },
-    watch()     { hideEl(watch, unwatched);     showEl(unwatch, watched);     addTag(tagId, watchedTagList);      },
+    unwatch() {
+      hideEl(unwatch, watched);
+      showEl(watch, unwatched);
+      removeTag(tagId, watchedTagList);
+    },
+    watch() {
+      hideEl(watch, unwatched);
+      showEl(unwatch, watched);
+      addTag(tagId, watchedTagList);
+    },
 
-    unspoiler() { hideEl(unspoiler, spoilered); showEl(spoiler);              removeTag(tagId, spoileredTagList); },
-    spoiler()   { hideEl(spoiler);              showEl(unspoiler, spoilered); addTag(tagId, spoileredTagList);    },
+    unspoiler() {
+      hideEl(unspoiler, spoilered);
+      showEl(spoiler);
+      removeTag(tagId, spoileredTagList);
+    },
+    spoiler() {
+      hideEl(spoiler);
+      showEl(unspoiler, spoilered);
+      addTag(tagId, spoileredTagList);
+    },
 
-    unhide()    { hideEl(unhide, hidden);       showEl(hide);                 removeTag(tagId, hiddenTagList);    },
-    hide()      { hideEl(hide);                 showEl(unhide, hidden);       addTag(tagId, hiddenTagList);       },
+    unhide() {
+      hideEl(unhide, hidden);
+      showEl(hide);
+      removeTag(tagId, hiddenTagList);
+    },
+    hide() {
+      hideEl(hide);
+      showEl(unhide, hidden);
+      addTag(tagId, hiddenTagList);
+    },
   };
-  /* eslint-enable prettier/prettier */
 
   const tagIsWatched = watchedTagList.includes(tagId);
   const tagIsSpoilered = spoileredTagList.includes(tagId);
diff --git a/assets/package-lock.json b/assets/package-lock.json
index 412aaa4f..52ea697b 100644
--- a/assets/package-lock.json
+++ b/assets/package-lock.json
@@ -22,13 +22,9 @@
         "@testing-library/jest-dom": "^6.6.3",
         "@vitest/coverage-v8": "^2.1.9",
         "eslint": "^9.16.0",
-        "eslint-config-prettier": "^9.1.0",
-        "eslint-plugin-prettier": "^5.2.1",
         "eslint-plugin-vitest": "^0.5.4",
-        "prettier": "^3.4.2",
         "stylelint": "^16.11.0",
         "stylelint-config-standard": "^36.0.1",
-        "stylelint-prettier": "^5.0.2",
         "typescript-eslint": "8.17.0",
         "vitest": "^2.1.9",
         "vitest-fetch-mock": "^0.4.2"
@@ -1120,18 +1116,6 @@
         "node": ">=14"
       }
     },
-    "node_modules/@pkgr/core": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
-      "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
-      "dev": true,
-      "engines": {
-        "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/unts"
-      }
-    },
     "node_modules/@rollup/rollup-android-arm-eabi": {
       "version": "4.28.1",
       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
@@ -2517,48 +2501,6 @@
         }
       }
     },
-    "node_modules/eslint-config-prettier": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
-      "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
-      "dev": true,
-      "bin": {
-        "eslint-config-prettier": "bin/cli.js"
-      },
-      "peerDependencies": {
-        "eslint": ">=7.0.0"
-      }
-    },
-    "node_modules/eslint-plugin-prettier": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz",
-      "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==",
-      "dev": true,
-      "dependencies": {
-        "prettier-linter-helpers": "^1.0.0",
-        "synckit": "^0.9.1"
-      },
-      "engines": {
-        "node": "^14.18.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint-plugin-prettier"
-      },
-      "peerDependencies": {
-        "@types/eslint": ">=8.0.0",
-        "eslint": ">=8.0.0",
-        "eslint-config-prettier": "*",
-        "prettier": ">=3.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/eslint": {
-          "optional": true
-        },
-        "eslint-config-prettier": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/eslint-plugin-vitest": {
       "version": "0.5.4",
       "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.5.4.tgz",
@@ -2828,12 +2770,6 @@
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
       "dev": true
     },
-    "node_modules/fast-diff": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
-      "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
-      "dev": true
-    },
     "node_modules/fast-glob": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -4121,33 +4057,6 @@
         "node": ">= 0.8.0"
       }
     },
-    "node_modules/prettier": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
-      "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
-      "dev": true,
-      "bin": {
-        "prettier": "bin/prettier.cjs"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/prettier/prettier?sponsor=1"
-      }
-    },
-    "node_modules/prettier-linter-helpers": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
-      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
-      "dev": true,
-      "dependencies": {
-        "fast-diff": "^1.1.2"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
     "node_modules/pretty-format": {
       "version": "27.5.1",
       "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
@@ -4628,22 +4537,6 @@
         "stylelint": "^16.1.0"
       }
     },
-    "node_modules/stylelint-prettier": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/stylelint-prettier/-/stylelint-prettier-5.0.2.tgz",
-      "integrity": "sha512-qJ+BN+1T2ZcKz9WIrv0x+eFGHzSUnXfXd5gL///T6XoJvr3D8/ztzz2fhtmXef7Vb8P33zBXmLTTveByr0nwBw==",
-      "dev": true,
-      "dependencies": {
-        "prettier-linter-helpers": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=18.12.0"
-      },
-      "peerDependencies": {
-        "prettier": ">=3.0.0",
-        "stylelint": ">=16.0.0"
-      }
-    },
     "node_modules/stylelint/node_modules/balanced-match": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
@@ -4750,22 +4643,6 @@
       "optional": true,
       "peer": true
     },
-    "node_modules/synckit": {
-      "version": "0.9.2",
-      "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
-      "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
-      "dev": true,
-      "dependencies": {
-        "@pkgr/core": "^0.1.0",
-        "tslib": "^2.6.2"
-      },
-      "engines": {
-        "node": "^14.18.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/unts"
-      }
-    },
     "node_modules/table": {
       "version": "6.9.0",
       "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz",
@@ -4992,12 +4869,6 @@
         "typescript": ">=4.2.0"
       }
     },
-    "node_modules/tslib": {
-      "version": "2.8.1",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
-      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
-      "dev": true
-    },
     "node_modules/type-check": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
diff --git a/assets/package.json b/assets/package.json
index 41b85269..a2cf1ae2 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -27,13 +27,9 @@
     "@testing-library/jest-dom": "^6.6.3",
     "@vitest/coverage-v8": "^2.1.9",
     "eslint": "^9.16.0",
-    "eslint-config-prettier": "^9.1.0",
-    "eslint-plugin-prettier": "^5.2.1",
     "eslint-plugin-vitest": "^0.5.4",
-    "prettier": "^3.4.2",
     "stylelint": "^16.11.0",
     "stylelint-config-standard": "^36.0.1",
-    "stylelint-prettier": "^5.0.2",
     "typescript-eslint": "8.17.0",
     "vitest": "^2.1.9",
     "vitest-fetch-mock": "^0.4.2"
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..f7aea3ab
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,27 @@
+{
+  "name": "philomena",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "devDependencies": {
+        "prettier": "^3.5.3"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
+      "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..9528d2d8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,9 @@
+{
+  "scripts": {
+    "fmt": "prettier --write .",
+    "fmt-check": "prettier --check ."
+  },
+  "devDependencies": {
+    "prettier": "^3.5.3"
+  }
+}

From 5131451ecf56b43c1d7a15ee202fbc6ec8afb739 Mon Sep 17 00:00:00 2001
From: MareStare <mare.stare.official@gmail.com>
Date: Sun, 16 Mar 2025 23:26:18 +0000
Subject: [PATCH 2/5] Apply `npm run fmt` across the entire codebase

---
 .github/ISSUE_TEMPLATE/bug_report.md      |  18 +-
 .github/ISSUE_TEMPLATE/feature_request.md |   1 -
 .github/PULL_REQUEST_TEMPLATE.md          |   8 +-
 .prettierrc.yml                           |   2 +-
 README.md                                 |   5 +
 assets/.stylelintrc.yml                   |  14 +-
 assets/css/application.css                |   2 +-
 assets/css/themes/base/dark.css           |   8 +-
 assets/js/boorujs.js                      |  54 +++--
 assets/tsconfig.json                      |   6 +-
 config/avatar.json                        | 195 +++------------
 config/footer.json                        |   6 +-
 config/quick_tag_table.json               | 274 ++++++++++------------
 priv/repo/seeds.json                      |  67 +++---
 priv/repo/seeds_development.json          |  68 +++---
 15 files changed, 284 insertions(+), 444 deletions(-)

diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index f3d5c415..5ad98142 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -4,7 +4,6 @@ about: Create a report to help us improve
 title: ''
 labels: bug
 assignees: ''
-
 ---
 
 **Describe the bug**
@@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
 
 **To Reproduce**
 Steps to reproduce the behavior:
+
 1. Go to '...'
 2. Click on '....'
 3. Scroll down to '....'
@@ -24,15 +24,17 @@ A clear and concise description of what you expected to happen.
 If applicable, add screenshots to help explain your problem.
 
 **Desktop (please complete the following information):**
- - OS: [e.g. iOS]
- - Browser [e.g. chrome, safari]
- - Version [e.g. 22]
+
+- OS: [e.g. iOS]
+- Browser [e.g. chrome, safari]
+- Version [e.g. 22]
 
 **Smartphone (please complete the following information):**
- - Device: [e.g. iPhone6]
- - OS: [e.g. iOS8.1]
- - Browser [e.g. stock browser, safari]
- - Version [e.g. 22]
+
+- Device: [e.g. iPhone6]
+- OS: [e.g. iOS8.1]
+- Browser [e.g. stock browser, safari]
+- Version [e.g. 22]
 
 **Additional context**
 Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 11fc491e..5f0a04ce 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -4,7 +4,6 @@ about: Suggest an idea for this project
 title: ''
 labels: enhancement
 assignees: ''
-
 ---
 
 **Is your feature request related to a problem? Please describe.**
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 652369bb..4f3922bb 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,10 +1,10 @@
 ### Before you begin
 
-* I understand my contributions may be rejected for any reason
-* I understand my contributions are for the benefit of Derpibooru and/or the Philomena software
-* I understand my contributions are licensed under the GNU AGPLv3
+- I understand my contributions may be rejected for any reason
+- I understand my contributions are for the benefit of Derpibooru and/or the Philomena software
+- I understand my contributions are licensed under the GNU AGPLv3
 
-- [ ] I understand all of the above
+* [ ] I understand all of the above
 
 ---
 
diff --git a/.prettierrc.yml b/.prettierrc.yml
index 83cfc971..c658d5c1 100644
--- a/.prettierrc.yml
+++ b/.prettierrc.yml
@@ -9,6 +9,6 @@ quoteProps: as-needed
 trailingComma: all
 arrowParens: avoid
 overrides:
-  - files: "*.css"
+  - files: '*.css'
     options:
       singleQuote: false
diff --git a/README.md b/README.md
index 966d5e06..c0ae0caf 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
 # Philomena
+
 ![Philomena](/assets/static/images/phoenix.svg)
 
 ## Getting started
+
 On systems with `docker` and `docker compose` installed, the process should be as simple as:
 
 ```
@@ -23,11 +25,13 @@ Once the application has started, navigate to http://localhost:8080 and login wi
 If you are running Docker on Windows and the application crashes immediately upon startup, please ensure that `autocrlf` is set to `false` in your Git config, and then re-clone the repository. Additionally, it is recommended that you allocate at least 4GB of RAM to your Docker VM.
 
 If you run into an OpenSearch bootstrap error, you may need to increase your `max_map_count` on the host as follows:
+
 ```
 sudo sysctl -w vm.max_map_count=262144
 ```
 
 If you have SELinux enforcing (Fedora, Arch, others; manifests as a `Could not find a Mix.Project` error), you should run the following in the application directory on the host before proceeding:
+
 ```
 chcon -Rt svirt_sandbox_file_t .
 ```
@@ -37,6 +41,7 @@ This allows Docker or Podman to bind mount the application directory into the co
 If you are using a platform which uses cgroups v2 by default (Fedora 31+), use `podman` and `podman-compose`.
 
 ## Deployment
+
 You need a key installed on the server you target, and the git remote installed in your ssh configuration.
 
     git remote add production philomena@<serverip>:philomena/
diff --git a/assets/.stylelintrc.yml b/assets/.stylelintrc.yml
index 78227ced..e513ddac 100644
--- a/assets/.stylelintrc.yml
+++ b/assets/.stylelintrc.yml
@@ -5,14 +5,14 @@ rules:
   at-rule-no-unknown:
     - true
     - ignoreAtRules:
-      - mixin
-      - define-mixin
+        - mixin
+        - define-mixin
   media-query-no-invalid:
   rule-empty-line-before:
     - always-multi-line
     - except:
-      - after-single-line-comment
-      - first-nested
+        - after-single-line-comment
+        - first-nested
   declaration-block-no-duplicate-custom-properties: true
   declaration-block-no-duplicate-properties: true
   font-family-no-duplicate-names: true
@@ -45,10 +45,10 @@ rules:
   at-rule-empty-line-before:
     - always
     - except:
-      - first-nested
+        - first-nested
       ignore:
-      - after-comment
-      - blockless-after-blockless
+        - after-comment
+        - blockless-after-blockless
   custom-property-empty-line-before: never
   declaration-empty-line-before: never
   declaration-block-single-line-max-declarations: 3
diff --git a/assets/css/application.css b/assets/css/application.css
index 31c177a9..a6ae2827 100644
--- a/assets/css/application.css
+++ b/assets/css/application.css
@@ -56,4 +56,4 @@
 @import "views/search";
 @import "views/staff";
 @import "views/stats";
-@import "views/tags";
\ No newline at end of file
+@import "views/tags";
diff --git a/assets/css/themes/base/dark.css b/assets/css/themes/base/dark.css
index a459e5c8..8821c5cd 100644
--- a/assets/css/themes/base/dark.css
+++ b/assets/css/themes/base/dark.css
@@ -66,7 +66,9 @@
     --block-header-link-text-hover-color: hsl(from $block-header-link-text-color calc(h + 6) calc(s - 20) calc(l - 3));
 
     --block-header-light-hover-color: hsl(from $block-header-light-color calc(h - 4) calc(s + 10) calc(l - 4));
-    --block-header-light-link-text-hover-color: hsl(from $block-header-light-link-text-color calc(h + 8) calc(s - 10) calc(l - 2));
+    --block-header-light-link-text-hover-color: hsl(
+      from $block-header-light-link-text-color calc(h + 8) calc(s - 10) calc(l - 2)
+    );
 
     --media-box-hover-color: hsl(from $media-box-color h s calc(l - 4));
     --media-box-header-link-text-hover-color: hsl(from $link-color h calc(s - 18) calc(l - 3));
@@ -121,7 +123,9 @@
     --tag-category-error-border: hsl(from $tag-category-error-color h s calc(l - 22));
     --tag-category-character-background: hsl(from $tag-category-character-color h s calc(l - 33));
     --tag-category-character-border: hsl(from $tag-category-character-color h s calc(l - 20));
-    --tag-category-content-official-background: hsl(from $tag-category-content-official-color h calc(s - 2) calc(l - 29));
+    --tag-category-content-official-background: hsl(
+      from $tag-category-content-official-color h calc(s - 2) calc(l - 29)
+    );
     --tag-category-content-official-border: hsl(from $tag-category-content-official-color h s calc(l - 20));
     --tag-category-content-fanmade-background: hsl(from $tag-category-content-fanmade-color h s calc(l - 40));
     --tag-category-content-fanmade-border: hsl(from $tag-category-content-fanmade-color h calc(s - 10) calc(l - 20));
diff --git a/assets/js/boorujs.js b/assets/js/boorujs.js
index b60e2217..87e05003 100644
--- a/assets/js/boorujs.js
+++ b/assets/js/boorujs.js
@@ -11,21 +11,45 @@ import { addTag } from './tagsinput';
 
 // Event types and any qualifying conditions - return true to not run action
 const types = {
-  click(event) { return event.button !== 0; /* Left-click only */ },
-  change() { /* No qualifier */ },
-  fetchcomplete() { /* No qualifier */ },
+  click(event) {
+    return event.button !== 0; /* Left-click only */
+  },
+  change() {
+    /* No qualifier */
+  },
+  fetchcomplete() {
+    /* No qualifier */
+  },
 };
 
 const actions = {
-  hide(data) { selectorCb(data.base, data.value, el => el.classList.add('hidden')); },
-  show(data) { selectorCb(data.base, data.value, el => el.classList.remove('hidden')); },
-  toggle(data) { selectorCb(data.base, data.value, el => el.classList.toggle('hidden')); },
-  submit(data) { selectorCb(data.base, data.value, el => el.submit()); },
-  disable(data) { selectorCb(data.base, data.value, el => el.disabled = true); },
-  focus(data) { document.querySelector(data.value).focus(); },
-  unfilter(data) { showBlock(data.el.closest('.image-show-container')); },
-  tabHide(data) { selectorCbChildren(data.base, data.value, el => el.classList.add('hidden')); },
-  preventdefault() { /* The existence of this entry is enough */ },
+  hide(data) {
+    selectorCb(data.base, data.value, el => el.classList.add('hidden'));
+  },
+  show(data) {
+    selectorCb(data.base, data.value, el => el.classList.remove('hidden'));
+  },
+  toggle(data) {
+    selectorCb(data.base, data.value, el => el.classList.toggle('hidden'));
+  },
+  submit(data) {
+    selectorCb(data.base, data.value, el => el.submit());
+  },
+  disable(data) {
+    selectorCb(data.base, data.value, el => (el.disabled = true));
+  },
+  focus(data) {
+    document.querySelector(data.value).focus();
+  },
+  unfilter(data) {
+    showBlock(data.el.closest('.image-show-container'));
+  },
+  tabHide(data) {
+    selectorCbChildren(data.base, data.value, el => el.classList.add('hidden'));
+  },
+  preventdefault() {
+    /* The existence of this entry is enough */
+  },
 
   copy(data) {
     document.querySelector(data.value).select();
@@ -78,9 +102,9 @@ const actions = {
       fetchHtml(loadTab)
         .then(handleError)
         .then(response => response.text())
-        .then(response => newTab.innerHTML = response)
-        .then(() => newTab.dataset.loaded = true)
-        .catch(() => newTab.textContent = 'Error!');
+        .then(response => (newTab.innerHTML = response))
+        .then(() => (newTab.dataset.loaded = true))
+        .catch(() => (newTab.textContent = 'Error!'));
     }
   },
 };
diff --git a/assets/tsconfig.json b/assets/tsconfig.json
index e825a6a0..83fba68a 100644
--- a/assets/tsconfig.json
+++ b/assets/tsconfig.json
@@ -6,11 +6,7 @@
     "esModuleInterop": true,
     "allowJs": true,
     "skipLibCheck": true,
-    "lib": [
-      "ES2016",
-      "DOM",
-      "DOM.Iterable"
-    ],
+    "lib": ["ES2016", "DOM", "DOM.Iterable"],
     "moduleResolution": "bundler",
     "allowImportingTsExtensions": true,
     "resolveJsonModule": true,
diff --git a/config/avatar.json b/config/avatar.json
index 577db737..baa37b40 100644
--- a/config/avatar.json
+++ b/config/avatar.json
@@ -1,264 +1,129 @@
 {
   "header": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"125\" height=\"125\" viewBox=\"0 0 125 125\" class=\"avatar-svg\">",
   "background": "<rect width=\"125\" height=\"125\" fill=\"#c6dff2\"/>",
-  "species": [
-    "unicorn",
-    "pegasus",
-    "earthpony"
-  ],
+  "species": ["unicorn", "pegasus", "earthpony"],
   "body_shapes": [
     {
       "shape": "<path d=\"M73.054 24.46c25.886 0 39.144 26.39 28.916 44.95 1.263.38 4.924 2.274 3.41 4.8-1.516 2.525-7.577 16.288-27.78 14.773-1.01 6.44-.33 12.613 1.642 22.854 1.39 7.224-.632 14.648-.632 14.648s-47.785.216-73.74-.127c-1.883-6.387 8.964-25.76 20.833-24.748 15.674 1.334 19.193 1.64 21.592-2.02 2.4-3.662 0-23.234-3.535-30.81-3.536-7.577-7.83-40.785 29.294-44.32z\" fill=\"#BODY_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     }
   ],
   "tail_shapes": [
     {
       "shape": "<path d=\"M15.456 109.15C12.02 97.805 6.44 95.036-.794 98.89v19.102c5.13-10.09 10.263-8.294 15.395-5.7\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     }
   ],
   "hair_shapes": [
     {
       "shape": "<path d=\"M76.48 11.35c2.215.014 4.26.185 6.02.518l4.624.875-4.67.657c-5.435.77-7.802 1.83-10.266 3.833 14.608-2.52 25.094 1.816 32.14 8.69 7.51 7.337 11.223 17.454 12.508 25.286l.363 2.236-1.8-1.548c-6.914-5.983-15.308-9.988-25.68-11.48 2.26 1.687 4.477 3.113 6.764 3.932l2.81 1.005-2.97.487c-7.684 1.264-15.37-2.687-22.687-6.52-7.162-3.754-13.99-7.357-19.843-6.578-3.708 5.568-5.84 11.828-5.882 17.63-.03 3.945.886 7.66 2.9 10.84l.37.47.022.027.018.028c.168.26.328.52.496.78.833 1.085 1.808 2.098 2.934 3.02l2.065 1.685-2.49-.3c3.97 7.734 5.85 15.703 6.24 22.54.24 4.19-.07 7.95-.825 10.985-.753 3.038-1.905 5.37-3.577 6.688l-1.516 1.195.105-1.823c.16-2.637.47-5.577.123-8.528-.38.702-.825 1.36-1.32 1.977-1.43 1.746-3.414 3.064-5.934 3.426-2.517.365-5.498-.217-8.956-2.01l-1.7-.884 1.87-.544c2.374-.69 4.147-1.565 5.465-2.575-.7.033-1.4 0-2.092-.09-4.135-.542-7.932-2.934-10.414-5.09l-1.19-1.038 1.612-.294c5.34-.965 9.6-3.222 11.54-6.996 1.945-3.775 1.67-9.254-2.587-16.895-2.502-4.487-4.24-8.59-5.29-12.398-.267-.588-.497-1.19-.69-1.802-.656-2.083-.868-4.297-.564-6.545-.022-1.894.146-3.72.496-5.49 1.48-7.405 6.09-13.753 12.596-20.25h-.003C51.55 16.54 57.96 14.01 64.518 12.62c4.104-.868 8.265-1.287 11.958-1.262z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M61.346 30.543s6.965 1.786 2.68 9.464c3.497-.7 27.61.684 44.068 3.636 1.028-5.24.59-5.866.262-8.384 2.537 2.828 3.104 5.216 3.53 9.122 2.952.627 5.52 1.313 7.496 2.054-.536-9.822-5.893-47.68-61.428-27.143-5.715 3.75-11.965 12.143-11.965 12.143s-8.394 11.07-5.536 28.928c1.62 10.127 6.428 21.43-1.608 40.715 2.5 2.14 19.108 5.534 26.072 4.463 1.607-9.107-2.617-39.046-9.107-45.536-4.464-4.464-6.445-10.326-4.106-16.786 3.75-10.356 9.642-12.677 9.642-12.677z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M58.93 30.714c4.463.536 8.75 2.59 4.64 12.68 7.144-.18 29.02-.358 33.305-6.608 0 0 1.07 5-1.43 6.07 10.766-1.87 24.773-9.743 16.274-19.82-12.5-14.822-40.827-12.143-55.827-2.68-15 9.465-20.068 23.473-17.143 36.25 3.393 14.823 14.378 36.825 1.7 50.218 8.213.714 21.546-12.46 21.693-22.18.178-11.787-5.357-24.287-10.18-29.823-4.82-5.534-3.392-17.856 6.966-24.106z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M54.3 19.6c19.19-14.947 44.49-12.68 62.386-4.014 4.697 2.275 11.857 12.1-3.583 12.05 10.746 3.893 11.87 22.562 6.702 24.558-5.956 2.3-10.71-7.422-10.227-13.637-2.522 11.215-8.36 22.893-13.84 18.31-4.597-3.846-4.192-8.617-.95-13.764-5.694 4.724-11.298 7.872-16.992 3.566-5.77 3.204-10.776 8.625-17.182 5.93-7.935-3.34-1.024-13.468 3.976-17.143-7.308-.314-9.815 3.454-14.432 12.895 2.963 17.85 19.438 32.202 18.517 49.25-.536 9.916-4.688 10.88-5.852 2.51 1.696 25.253-8.634 24.816-9.356 13.904-9.447 16.2-13.625 4.51-10.93-4.183 2.054-6.628 4.03-12.16 6.425-16.777-2.547 7.66-7.333 5.232-8.583 4.43-2.854-1.834-.855-12.302 4.035-19.33 8.3-11.93-23.73-30.172 2.47-53.197\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M64.335 34.675c3.358 1.584 6.716.908 10.073 1.043-.265 13.078 19.05 19.74 31.58 4.16 6.077 6.273 24.776 2.28 12.42-18.66-12.88-21.833-42.605-11.287-61-.5l-7.25 11c-29.918 14.92-16.418 45.666-.75 57.625-12.967 2.522-6.234 30.16 9.904 24.894 18.84-6.147-1.986-51.066-7.78-62.644l1.495-11.736z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M52.426 21.376c25.818-7.37 48.653-24.77 66.7 7.096 3.995 7.053 3.306 14.184-1.62 18.23-8.428 6.92-25.402 6.743-24.694-4.35-9.947 5.8-19.764 6.727-17.758-7.25-5.545 2.905-7.34 2.858-12.887-1.376L51.7 40.92l-.283 10.556C55.46 60.642 62.01 69.434 60.792 78.6c7.922.748 15.435 6.55 2 13.126 15.47 9.894 7.286 24.773-4.25 19.25 1.142 13.048-5.167 11.66-9 6.5-17.216-23.18-9.895-61.743-4.25-82z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M102.625 9.594c-.617.033-1.224.124-1.78.28-10.205 2.87-17.71 11.35-32.845 7.97-.096-.186-.132-.25-.23-.37-.294-.37-6.635-1.52-6.943-1.41-.21.074-.282.22-.375.34-21.85-18.666-57.55-7.878-33.796 56.534 2.593 7.032-8.706 5.65-9.25-2.312-5.552 4.567-4.144 16.383 5.313 16.188-1.03 14.91 4.02 9.98 8.78 9.28-.47 10.136 11.71 10.447 13.81-2.203 12.79 3.06 13.08-9.74 10.88-20.44-2.36-11.46-11.016-22.16-6.586-33.5l5.42-5.01c3.526-.72 9.956-3.402 9.413.298-2.856 9.612-10.034 5.077-10.1 10.924-.028 2.465 1.502 4.653 5.656 2.53-2.517 9.795 11.9 13.08 18.905 2.907 2.354 4.592 9.82 8.295 6.72-4.5 3.22 6.775 6.05 4.517 6.936 2.342 4.31 3.093 7.42 2.92 8.595-2.658 6.02 6.838 10.68 3.283 9.063-5.06 1.377.31 2.352-.472 2.842-2.595 1.148 2.375.496 5.903 2.53 6.874 4.66 2.224 11.55-1.366 3.657-14.97 7.64-9.13-7.368-21.936-16.626-21.436z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M60.982 35.02c13.642.26 19.686 5.237 29.97 13.01.136-5.54.728-6.83-4.33-12.368 11.895 2.714 24.29 8.52 35.945 18.718 2.125-26.222-15.928-43.006-31.905-41.724C94.884 7.638 95.28 4.836 93.54-.25c-4.477 11.725-29.89 7.512-39.19 17.063C42.782 25.81 36.323 36.35 36.764 52.875c.613 22.964 17.723 48.87.527 67.55 19.194 3.242 36.48-23.735 21.57-45.983 2.318.894 4.125.91 8.78-2.298-11.9-3.87-16.395-15.49-15.792-22.93l.717-7.538z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M63.62 35.025c11.562.736 19.798 3.434 34.567 11.597 25.383-12.243 16.01-35.524-.763-39.99-15.625-4.16-25.83-1.755-37-5.565 1.956 4.14 4.564 8.348 8 10.322-18.826-.18-28.113-3.676-42.75-7.05 2.95 5.29 9.994 11.52 13.25 13.884-12.083 5.094-20.916-.076-33-2.15 3.333 5.823 7.048 11.19 12.25 14.783-5 16.343 19.916 37.197 29.787 57.14 2.7-12.815 4.76-30.792 3.29-43.607z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M64.33 33.283c9.705 22.482 32.942 3.702 38.82 11.746 3.072 7.98 2.498 17.27-1.44 24.37 2.934 1.12 4.508 2.91 3.49 4.8-4.31 7.99-11.594 15.45-27.637 14.54-3.062 12.93 5.765 26.87-.126 40.64 11.173 8.95 42.412 16.97 38.09-13.953-10.32 13-15.464 2.34-13.707-3.42 4.16-13.63 31.874-51.83 17.542-74.7-12.647-20.18-40.576-27.73-69.92-13.164-29.344 14.568 5.794 91.68-8.67 116.406l26.737.356c-12.15-11.23-6.13-30.195-17.41-45.886 4.24 4.636 7.68 5.417 13.62 3.315-12.013-5.83-19.25-25.54-11.41-46.41l1.214-10.2z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony"]
     },
     {
       "shape": "<path d=\"M44.666 21.39c2.88-9.64 21.828-34.834 52.575-4.97 22.297-9.464 31.18 14.324 16.478 27.943 8.44 7.118-2.553 17.874-8.52 15.18.256-5.74-.34-12.306-2.865-16.798-7.614 17.73-27.62 10.2-38.18-8.388L52.11 39.335l-.06 12.682c-.614 7.093 17.074 16.908 13.22 23.89 2.52 3.48 15.83 20.395 3.97 27.226-9.958 37.27-37.444-2.66-21.166-8.785C32.694 78.324 28.346 70.315 29.7 48.306c.935-15.233 8.206-27.93 14.97-26.915z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M97.37 50.6c15.89 10.53 26.147-13.405 18.827-28.587.066 1.56-.416 2.486-.834 3.485-1.683-5.233-2.087-6.758-3.05-9.9-.65 5.35-.446 10.048-5.488 18.74-1.196-11.776-6.05-20.513-11.554-25.79.7 2.945.56 6.08-.32 9.025-15.752-23.3-38.63-12.01-46.413-5.528-29.76 24.786-16.288 62.293-9.69 75.682-.317-4.247-1.077-8.09-.063-11.67 2.004 3.86 4.007 6.256 6.01 8.838-1.955 7.063 3.654 10.892-.93 17.733 4.235-.512 11.51-8.156 9.15-17.557-2.954-11.753-.804-23.57-1.844-33.58l1.427-9.883 9.993-6.958c11.89-4.685 23.622 8.56 34.78 15.953z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M50.874 23.522c23.66-15.832 58.66-24.962 70.02 2.35 10.813 26-22.117 24.082-27.334 22.23 6.058-4.43 10.695-8.353 17.07-14.05-14.197 11.133-33.26 15.265-48.32 16.272 2.97-5.293 4.97-10.57 2.723-15.09L57.15 33.13l-6.094 18.71c48.018 71.35-18.295 80.53-19.233 64.863 27.224 12.403-3.396-65.87 13.624-87.52\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M52.325 51.93l-.063-12.07 10.32-5.327c16.453 11.482 33.565 4.952 40.567 10.496 3.07 7.98 2.49 17.27-1.44 24.37 2.93 1.12 4.5 2.91 3.49 4.8-3.67 6.8-10.19 14.44-25.51 14.68.07 5.13.47 10.23-.75 15.36 11.28 3.43 31.1 3.94 42.39 1.37-10.24-18.287-9.87-40.87-9.85-57.37 1.755-.198 4.4 1.69 8.275 3.663-13.94-37.69-52.24-41.51-69.037-29.156l-4.19 7.19C41.89 37 29.75 45.454 23.303 39.312c-.443 5.295 8.04 13.68 12.444 17.022-3.68.662-8.356 1.16-13.03-.785 8.12 11.47 13.312 29.02 25.21 31.97-.01-11.8-2.65-23.52 4.39-35.6z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M65.888 115.415C60.6 98.25 45.403 78.935 52.325 51.93l3.006-17.147c18.28-6.646 46.31 13.088 40.76 36.57 2.425 1.032 4.488 1.745 6.967 1.69-2.06 7.493-10.448 16.372-25.49 15.71-2.303 9.72 2.117 20.104 1.946 30.413 9.23 3.104 19.833 2.55 30.445 1.972 2.537-16.423 2.068-32.667 1.46-47.214 2.085.69 6.348-.022 8.358.24.904-12.23.92-24.644-3.66-34.595-9.958-21.64-37.62-37.768-65.378-16.81L46.792 34.2c-14.727 22.36-4.263 53.73-2.577 78.448 6.39-.468 12.547-1.325 21.673 2.767z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M55.33 34.783c8.91-5.593 22.85-3.56 30.63 6.92 5.854-1.71 12.958-2.89 17.19 3.326 3.07 7.982 2.498 17.277-1.44 24.377 2.934 1.124 4.508 2.915 3.49 4.8-4.31 7.993-11.594 15.457-27.637 14.544-3.062 12.93 5.765 26.87-.126 40.644l32.34 5.797c6.21-36.57 17.973-70.336 6.335-95.622-9.958-21.638-37.62-37.765-65.378-16.808L46.792 34.2c-19.9 30.21 6.194 76.877-5.52 101.353l16.987 5.857c8.434-28.756-22-46.724-5.936-89.48z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M92.715 36.313c8.637 2.465 10.713 4.464 25.17 16.612.3-5.667-1.5-11.334-5.556-17 3.29 1.666 6.39 3.333 8.82 5-3.59-5.917-.64-11.527-13.428-20.165 4.3 1.387 7.764-1.722 11.177-5.11-18.67-.334-27.47-19.297-46.66 3.65-.275-3.042 1.24-6.084 3.625-9.125-6.813 1.285-11.79 5.566-15.498 12-5.698-5.507-13.085 1.003-18.49 2.667-6.89-14.66-15.616-10.166-21.167-4.66-9.283 9.21-8.08 25.87-.303 46.64 2.105 5.622-5.684 4.734-8.597 2.206l1.35 7.647c-2.54-2.5-3.63-5.866-4.913-9.117-1.628 13.92 22.205 43.486 30.347-.926 1.6 3.075 4.044 5.322 6.28 4.588-1.566-8.808 3.46-24.512 7.917-29.176 5.368-4.292 16.9-13.79 29.33-11.75 4.02 1.358 7.968 2.813 10.6 6.02z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony"]
     },
     {
       "shape": "<path d=\"M56.16 28.044c17.344-13.22 56.258-29.205 63.063 3.847 2.52 12.248.225 13.76-6.188 17.91-7.79 5.045-17.386-1.37-15.05-6.662-8.652 7.707-15.484 10.624-23.12 9.85-9.167-.927-6.437-6.76-2.417-9.872 2.437-1.887 5.08-3.57 9.436-5.76-7.942 2.55-13.992 1.974-19.282-3.34l-10.947 5.55.015 11.542C53.3 64.17 62.758 80.81 63.912 93.42c.72 7.876-5.532 6.637-8.65 1.425 1.847 5.582 3.592 9.892 3.483 15.89-.13 7.178-8.386 11.54-12.047 1.098-7.505-21.405-12.965-51.97-.973-75.3z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M64.306 35.686C75.18 32.366 93.94 34.6 99.078 51.34c6.337 2.127 12.625 3.224 15.202-3.89-3.127.498-5.786.083-8.04-1.186C123 41.61 123.66 29.374 122.42 21.6c-2.037 3.04-6.775 4.266-10.11 4.348C118.174 7.128 103.16 6.76 94.1.66c2.573 3.79 4.323 6.698 5.105 10.49C90.527 5.044 78.69 1.94 67.1 1.197c6.55 3.24 10.68 6.743 15.12 10.52-18.077-6.532-34.664 10.644-48.495.853 1.91 5.032 2.298 9.83 7.413 15.355-3.47.772-10.105 2.726-13.43 1.29.557 2.41 1.36 4.87 4.782 7.808-5.045-.055-10.09-2.138-15.135-3.696C20.058 42.85 23.76 52.122 32.8 60.054c-3.718-1.25-8.08-3.69-12.356-5.97 3.248 4.72 7.48 9.14 14.208 12.794-3.305.483-5.89.304-8.648.284 8.55 7.333 15.247 9.858 22.72 27.618 6.01-12.27 3.08-31.184 1.815-43.933l1.125-10.842z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M92.906 14.75c-14.61.286-26.147 9.556-39.687 13.53l-5.565 8.595C43.81 30.5 36.58 28.752 32 32.812c-8.645 7.666-12.336 29.877-.75 42.344 5.224 5.622 10.25 5.16 13.406-2.72 2.242 4.647 2.416 9.293 2.72 13.94.6 9.248-3.733 10.614-6.75 4.5-2.905 16.18 15.08 28.385 19.5 6.124 2.096-10.564-10.178-34.003-7.866-45.145-1.19-12.517 6.56-14.07 8.24-18.417 4.955-.63 8.483 7.77 13.97 19.156l16.25 1.656c2.246-4.542 2.954-9.24 2.374-14.53 3.326 5.076 3.486 8.08 2.562 14.155l12.688-.406c.818-4.864 1.737-10.438 1.187-15.783 4.447 5.572 1.82 9.458 2.22 15.03 9.41-5.17 16.63-25.01.03-33.343-6.964-3.497-13.156-4.737-18.873-4.625z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M52.103 23.43C70.12 1.668 97.126 2.89 110.14 16.998c27.93 30.27-15.676 43.662-18.952 21.868-12.934-4.453-23.22-3.956-34.13-2.747L51.12 49.26c.557 23.24 17.787 26.876 22.887 41.315 4.124 11.677-5.325 14.075-9.404 12.656 4.622 17.668-12.022 24.3-20.003 7.4-1.797-3.8-2.81-14.813 3.672-14.51-3.182-16.784-17.45-38.285-2.43-63.75z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     },
     {
       "shape": "<path d=\"M64.268 33.955c.417 4.905.2 15.162 11.578 10.276 1.96 6.36 12.832 8.068 16.833 2.37.77 3.5 15.69 10.525 17.49 1.676 19.792 5.17 10.453-26.42-12.985-31.76-14.75-3.36-32.286-5.9-48.993 5.386-28.595 19.318-3.18 39.89-5.026 56-4.353 38.03 12.04 46.05 14.974 20.586 18-.676 5.704-14.656-2.884-22.693C70.602 72.203 42.78 56.16 49.2 43.46z\" fill=\"#HAIR_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     }
   ],
   "extra_shapes": [
     {
       "shape": "<path d=\"M92.752 36.834s9.092-19.572 6.06-22.73c-3.03-3.156-15.277 11.492-16.92 16.542 2.02.505 8.082 2.273 10.86 6.188z\" fill=\"#BODY_FILL\"/>",
-      "species": [
-        "unicorn"
-      ]
+      "species": ["unicorn"]
     },
     {
       "shape": "<path d=\"M43.267 107.324s-6.825-14.137-7.64-30.166c-.817-16.03-4.197-31.468-10.55-40.688-6.354-9.22-13.272-9.73-11.997-3.982 1.275 5.748 11.123 33.016 12.128 35.954C23.042 65.648 7.038 41.11-.43 37.222c-7.47-3.886-8.96.346-6.892 5.885 2.068 5.54 18.507 30.844 20.886 33.502-2.738-1.685-12.256-9.036-16.997-8.996-4.742.04-4.91 5.366-2.617 8.526 2.292 3.162 20.912 19.173 25.15 20.945-5.35.28-10.384 1.996-9.186 6.004 1.2 4.006 11.384 14.063 28.53 12.377 2.576-2.834 4.823-8.143 4.823-8.143z\" fill=\"#BODY_FILL\"/>",
-      "species": [
-        "pegasus"
-      ]
+      "species": ["pegasus"]
     },
     {
       "shape": "<path d=\"M1.63 34.003s-8.46-5.35-14.8-5.626c5.125 4.444 24.008 19.984.12 34.287C-2.694 67.754 14.656 74.77.435 89.662c12.087 4.118 22.935 10.387 24.452 17.605 6.935-2.58 14.536-2.557 14.536-2.557L27.868 92.518 24.7 79.98l4.45-11.208 1.148-9.387L12.646 41.1 1.63 34.003\" fill=\"#74879c\"/><path d=\"M43.074 105.257c-7.347-4.89-14.894-13.1-14.524-21.41.37-8.31 6.128-16.213 8.922-20.638 2.794-4.426-.414-7.708-1.693-8.64.352 2.842-1.043 5.233-2.608 6.677-1.638-4.25-16.28-22.455-31.543-27.244C9.31 40.313 28.07 58.7 27.685 65.706c-.386 7.005-6.082 7.433-11.51 3.943-5.43-3.49-11.346-8.044-29.227-6.986 5.558 1.234 23.802 3.85 30.857 10.76 7.056 6.91 8.737 14.602.825 15.058-7.912.457-18.196 1.18-18.196 1.18s12.823-.28 19.82 2.003c6.997 2.284 13.553 7.303 4.632 15.602 14.536-2.557 15.073 2.3 17.602 1.912 2.53-.39.584-3.923.584-3.923z\" fill=\"#4f538d\"/>",
-      "species": [
-        "batpony"
-      ]
+      "species": ["batpony"]
     },
     {
       "shape": "<path d=\"M54.878 16.157c-2.55-4.117-4.816-7.13-6.17-6.475-1.357.655.865 7.59 3.092 11.277-4.267-3.22-7.552-4.118-7.552-4.118-3.34.21 4.375 8.207 4.375 8.207s6.52 1.44 6.255-8.893z\" fill=\"#BODY_FILL\"/>",
-      "species": [
-        "batpony"
-      ]
+      "species": ["batpony"]
     },
     {
       "shape": "<path d=\"M64.342 35.57s3.283-8.08-7.324-19.318c-1.768-1.768-3.03-2.273-4.672-.758-1.64 1.515-17.046 16.036.253 38.26.504-2.4 1.135-9.597 1.135-9.597z\" fill=\"#BODY_FILL\"/>",
-      "species": [
-        "unicorn",
-        "pegasus",
-        "earthpony",
-        "batpony"
-      ]
+      "species": ["unicorn", "pegasus", "earthpony", "batpony"]
     }
   ],
   "footer": "</svg>"
diff --git a/config/footer.json b/config/footer.json
index d875df97..24102ca3 100644
--- a/config/footer.json
+++ b/config/footer.json
@@ -1,9 +1,5 @@
 {
-  "cols": [
-    "Site Resources",
-    "Help & Information",
-    "Community"
-  ],
+  "cols": ["Site Resources", "Help & Information", "Community"],
   "Site Resources": [
     {
       "title": "Site Rules",
diff --git a/config/quick_tag_table.json b/config/quick_tag_table.json
index 6810305e..90911eae 100644
--- a/config/quick_tag_table.json
+++ b/config/quick_tag_table.json
@@ -44,21 +44,8 @@
     "1": "season"
   },
   "Main": {
-    "Ratings": [
-      "safe",
-      "suggestive",
-      "questionable",
-      "explicit",
-      "semi-grimdark",
-      "grimdark",
-      "grotesque"
-    ],
-    "General Spoilers": [
-      "spoiler:comic",
-      "spoiler:g5",
-      "spoiler:pony life",
-      "spoilers for another series"
-    ],
+    "Ratings": ["safe", "suggestive", "questionable", "explicit", "semi-grimdark", "grimdark", "grotesque"],
+    "General Spoilers": ["spoiler:comic", "spoiler:g5", "spoiler:pony life", "spoilers for another series"],
     "Species": [
       "anthro",
       "equestria girls",
@@ -115,160 +102,145 @@
       "timber wolf",
       "windigo"
     ],
-    "Misc": [
-      "original species",
-      "hybrid"
-    ]
+    "Misc": ["original species", "hybrid"]
   },
   "Shorthands A": [
-    ["Mane Cast", [
-      ["m6", "mane six"],
-      ["pt", "princess twilight"],
-      ["ts", "twilight sparkle"],
-      ["rd", "rainbow dash"],
-      ["ry", "rarity"],
-      ["aj", "applejack"],
-      ["fs", "fluttershy"],
-      ["pp", "pinkie pie"],
-      ["sp", "spike"]
-    ]],
-    ["Secondary Cast", [
-      ["cmc", "cutie mark crusaders"],
-      ["ab", "apple bloom"],
-      ["sl", "scootaloo"],
-      ["sb", "sweetie belle"],
-      ["tia", "princess celestia"],
-      ["luna", "princess luna"],
-      ["pcd", "princess cadance"],
-      ["sa", "shining armor"],
-      ["sg", "starlight glimmer"]
-    ]],
-    ["More Ponies", [
-      ["tx", "trixie"],
-      ["sus", "sunset shimmer"],
-      ["sombra", "king sombra"],
-      ["qc", "queen chrysalis"],
-      ["dc", "discord"],
-      ["nmm", "nightmare moon"],
-      ["sf", "spitfire"],
-      ["sn", "soarin'"],
-      ["ld", "lightning dust"]
-    ]],
-    ["More Apples", [
-      ["bm", "big macintosh"],
-      ["gs", "granny smith"],
-      ["bb", "braeburn"],
-      ["bs", "babs seed"]
-    ]],
-    ["Fillies and Colts", [
-      ["ss", "silver spoon"],
-      ["dt", "diamond tiara"],
-      ["pfh", "princess flurry heart"]
-    ]]
+    [
+      "Mane Cast",
+      [
+        ["m6", "mane six"],
+        ["pt", "princess twilight"],
+        ["ts", "twilight sparkle"],
+        ["rd", "rainbow dash"],
+        ["ry", "rarity"],
+        ["aj", "applejack"],
+        ["fs", "fluttershy"],
+        ["pp", "pinkie pie"],
+        ["sp", "spike"]
+      ]
+    ],
+    [
+      "Secondary Cast",
+      [
+        ["cmc", "cutie mark crusaders"],
+        ["ab", "apple bloom"],
+        ["sl", "scootaloo"],
+        ["sb", "sweetie belle"],
+        ["tia", "princess celestia"],
+        ["luna", "princess luna"],
+        ["pcd", "princess cadance"],
+        ["sa", "shining armor"],
+        ["sg", "starlight glimmer"]
+      ]
+    ],
+    [
+      "More Ponies",
+      [
+        ["tx", "trixie"],
+        ["sus", "sunset shimmer"],
+        ["sombra", "king sombra"],
+        ["qc", "queen chrysalis"],
+        ["dc", "discord"],
+        ["nmm", "nightmare moon"],
+        ["sf", "spitfire"],
+        ["sn", "soarin'"],
+        ["ld", "lightning dust"]
+      ]
+    ],
+    [
+      "More Apples",
+      [
+        ["bm", "big macintosh"],
+        ["gs", "granny smith"],
+        ["bb", "braeburn"],
+        ["bs", "babs seed"]
+      ]
+    ],
+    [
+      "Fillies and Colts",
+      [
+        ["ss", "silver spoon"],
+        ["dt", "diamond tiara"],
+        ["pfh", "princess flurry heart"]
+      ]
+    ]
   ],
   "B": [
-    ["Background Ponies", [
-      ["dh", "derpy hooves"],
-      ["dw", "doctor whooves"],
-      ["cgt", "colgate"],
-      ["bon", "bon bon"],
-      ["oct", "octavia melody"],
-      ["dj", "vinyl scratch"],
-      ["bp", "berry punch"],
-      ["pbb", "prince blueblood"]
-    ]],
-    ["Student Six", [
-      ["s6", "student six"],
-      ["ga", "gallus"],
-      ["ols", "ocellus"],
-      ["snb", "sandbar"],
-      ["svs", "silverstream"],
-      ["sm", "smolder"],
-      ["yn", "yona"]
-    ]],
-    ["Uncategorized", [
-      ["maud", "maud pie"],
-      ["coco", "coco pommel"],
-      ["suri", "suri polomare"],
-      ["rg", "royal guard"],
-      ["za", "zecora"],
-      ["mm", "mayor mare"],
-      ["pdp", "pinkamena diane pie"],
-      ["owol", "owlowiscious"],
-      ["opal", "opalescence"]
-    ]],
-    ["Other Things", [
-      ["cm", "cutie mark"],
-      ["eoh", "elements of harmony"],
-      ["nmn", "nightmare night"]
-    ]]
+    [
+      "Background Ponies",
+      [
+        ["dh", "derpy hooves"],
+        ["dw", "doctor whooves"],
+        ["cgt", "colgate"],
+        ["bon", "bon bon"],
+        ["oct", "octavia melody"],
+        ["dj", "vinyl scratch"],
+        ["bp", "berry punch"],
+        ["pbb", "prince blueblood"]
+      ]
+    ],
+    [
+      "Student Six",
+      [
+        ["s6", "student six"],
+        ["ga", "gallus"],
+        ["ols", "ocellus"],
+        ["snb", "sandbar"],
+        ["svs", "silverstream"],
+        ["sm", "smolder"],
+        ["yn", "yona"]
+      ]
+    ],
+    [
+      "Uncategorized",
+      [
+        ["maud", "maud pie"],
+        ["coco", "coco pommel"],
+        ["suri", "suri polomare"],
+        ["rg", "royal guard"],
+        ["za", "zecora"],
+        ["mm", "mayor mare"],
+        ["pdp", "pinkamena diane pie"],
+        ["owol", "owlowiscious"],
+        ["opal", "opalescence"]
+      ]
+    ],
+    [
+      "Other Things",
+      [
+        ["cm", "cutie mark"],
+        ["eoh", "elements of harmony"],
+        ["nmn", "nightmare night"]
+      ]
+    ]
   ],
   "Ships ts": {
-    "implying": [
-      "shipping",
-      "twilight sparkle"
-    ],
-    "not_implying": [
-
-    ]
+    "implying": ["shipping", "twilight sparkle"],
+    "not_implying": []
   },
   "rd": {
-    "implying": [
-      "shipping",
-      "rainbow dash"
-    ],
-    "not_implying": [
-
-    ]
+    "implying": ["shipping", "rainbow dash"],
+    "not_implying": []
   },
   "ry": {
-    "implying": [
-      "shipping",
-      "rarity"
-    ],
-    "not_implying": [
-
-    ]
+    "implying": ["shipping", "rarity"],
+    "not_implying": []
   },
   "aj": {
-    "implying": [
-      "shipping",
-      "applejack"
-    ],
-    "not_implying": [
-
-    ]
+    "implying": ["shipping", "applejack"],
+    "not_implying": []
   },
   "fs": {
-    "implying": [
-      "shipping",
-      "fluttershy"
-    ],
-    "not_implying": [
-
-    ]
+    "implying": ["shipping", "fluttershy"],
+    "not_implying": []
   },
   "pp": {
-    "implying": [
-      "shipping",
-      "pinkie pie"
-    ],
-    "not_implying": [
-
-    ]
+    "implying": ["shipping", "pinkie pie"],
+    "not_implying": []
   },
   "misc": {
-    "implying": [
-      "shipping"
-    ],
-    "not_implying": [
-      "twilight sparkle",
-      "rainbow dash",
-      "rarity",
-      "applejack",
-      "fluttershy",
-      "pinkie pie"
-    ]
+    "implying": ["shipping"],
+    "not_implying": ["twilight sparkle", "rainbow dash", "rarity", "applejack", "fluttershy", "pinkie pie"]
   },
   "Season 9": [
     ["1-2", "the beginning of the end"],
diff --git a/priv/repo/seeds.json b/priv/repo/seeds.json
index 3c5f06d9..546d4706 100644
--- a/priv/repo/seeds.json
+++ b/priv/repo/seeds.json
@@ -1,14 +1,10 @@
 {
-  "system_filters": [{
+  "system_filters": [
+    {
       "name": "Default",
       "description": "The site's default filter.",
-      "hidden": [
-        "explicit",
-        "grotesque"
-      ],
-      "spoilered": [
-        "questionable"
-      ]
+      "hidden": ["explicit", "grotesque"],
+      "spoilered": ["questionable"]
     },
     {
       "name": "Everything",
@@ -17,7 +13,8 @@
       "spoilered": []
     }
   ],
-  "forums": [{
+  "forums": [
+    {
       "name": "General Discussion",
       "short_name": "dis",
       "description": "This is a discussion forum for everything unrelated to the show or other forums",
@@ -60,37 +57,31 @@
       "access_level": "staff"
     }
   ],
-  "users": [{
-    "name": "Administrator",
-    "email": "admin@example.com",
-    "password": "philomena123",
-    "role": "admin"
-  }],
-  "rating_tags": [
-    "safe",
-    "suggestive",
-    "questionable",
-    "explicit",
-    "semi-grimdark",
-    "grimdark",
-    "grotesque"
+  "users": [
+    {
+      "name": "Administrator",
+      "email": "admin@example.com",
+      "password": "philomena123",
+      "role": "admin"
+    }
   ],
+  "rating_tags": ["safe", "suggestive", "questionable", "explicit", "semi-grimdark", "grimdark", "grotesque"],
   "roles": [
-    {"name": "moderator", "resource_type": "Image"},
-    {"name": "moderator", "resource_type": "DuplicateReport"},
-    {"name": "moderator", "resource_type": "Comment"},
-    {"name": "moderator", "resource_type": "Tag"},
-    {"name": "moderator", "resource_type": "ArtistLink"},
-    {"name": "admin", "resource_type": "Tag"},
-    {"name": "moderator", "resource_type": "User"},
-    {"name": "admin", "resource_type": "SiteNotice"},
-    {"name": "admin", "resource_type": "Badge"},
-    {"name": "admin", "resource_type": "Role"},
-    {"name": "batch_update", "resource_type": "Tag"},
-    {"name": "moderator", "resource_type": "Topic"},
-    {"name": "admin", "resource_type": "Advert"},
-    {"name": "admin", "resource_type": "StaticPage"},
-    {"name": "admin", "resource_type": "Image"}
+    { "name": "moderator", "resource_type": "Image" },
+    { "name": "moderator", "resource_type": "DuplicateReport" },
+    { "name": "moderator", "resource_type": "Comment" },
+    { "name": "moderator", "resource_type": "Tag" },
+    { "name": "moderator", "resource_type": "ArtistLink" },
+    { "name": "admin", "resource_type": "Tag" },
+    { "name": "moderator", "resource_type": "User" },
+    { "name": "admin", "resource_type": "SiteNotice" },
+    { "name": "admin", "resource_type": "Badge" },
+    { "name": "admin", "resource_type": "Role" },
+    { "name": "batch_update", "resource_type": "Tag" },
+    { "name": "moderator", "resource_type": "Topic" },
+    { "name": "admin", "resource_type": "Advert" },
+    { "name": "admin", "resource_type": "StaticPage" },
+    { "name": "admin", "resource_type": "Image" }
   ],
   "pages": []
 }
diff --git a/priv/repo/seeds_development.json b/priv/repo/seeds_development.json
index 47625a51..4788b0f5 100644
--- a/priv/repo/seeds_development.json
+++ b/priv/repo/seeds_development.json
@@ -1,5 +1,6 @@
 {
-  "users": [{
+  "users": [
+    {
       "name": "Hot Pocket Consumer",
       "email": "moderator@example.com",
       "password": "philomena123",
@@ -21,24 +22,18 @@
   "remote_images": [
     {
       "url": "https://derpicdn.net/img/2015/9/26/988000/thumb.gif",
-      "sources": [
-        "https://derpibooru.org/988000"
-      ],
+      "sources": ["https://derpibooru.org/988000"],
       "description": "Fairly large GIF (~23MB), use to test WebM stuff.",
       "tag_input": "alicorn, angry, animated, art, artist:assasinmonkey, artist:equum_amici, badass, barrier, crying, dark, epic, female, fight, force field, glare, glow, good vs evil, lord tirek, low angle, magic, mare, messy mane, metal as fuck, perspective, plot, pony, raised hoof, safe, size difference, spread wings, stomping, twilight's kingdom, twilight sparkle, twilight sparkle (alicorn), twilight vs tirek, underhoof"
     },
     {
       "url": "https://derpicdn.net/img/2012/1/2/25/large.png",
-      "sources": [
-        "https://derpibooru.org/25"
-      ],
+      "sources": ["https://derpibooru.org/25"],
       "tag_input": "artist:moe, canterlot, castle, cliff, cloud, detailed background, fog, forest, grass, mountain, mountain range, nature, no pony, outdoors, path, river, safe, scenery, scenery porn, signature, source needed, sunset, technical advanced, town, tree, useless source url, water, waterfall, widescreen, wood"
     },
     {
       "url": "https://derpicdn.net/img/2018/6/28/1767886/full.webm",
-      "sources": [
-        "http://hydrusbeta.deviantart.com/art/Gleaming-in-the-Sun-Our-Colors-Shine-in-Every-Hue-611497309"
-      ],
+      "sources": ["http://hydrusbeta.deviantart.com/art/Gleaming-in-the-Sun-Our-Colors-Shine-in-Every-Hue-611497309"],
       "tag_input": "3d, animated, architecture, artist:hydrusbeta, castle, cloud, crystal empire, crystal palace, flag, flag waving, no pony, no sound, safe, scenery, webm"
     },
     {
@@ -50,16 +45,12 @@
     },
     {
       "url": "https://derpicdn.net/img/view/2016/3/17/1110529.jpg",
-      "sources": [
-        "https://www.deviantart.com/devinian/art/Commission-Crystals-of-thy-heart-511134926"
-      ],
+      "sources": ["https://www.deviantart.com/devinian/art/Commission-Crystals-of-thy-heart-511134926"],
       "tag_input": "artist:devinian, aurora crystialis, bridge, cloud, crepuscular rays, crystal empire, crystal palace, edit, flower, forest, grass, log, mountain, no pony, river, road, safe, scenery, scenery porn, source needed, stars, sunset, swing, tree, wallpaper"
     },
     {
       "url": "https://derpicdn.net/img/view/2019/6/16/2067468.svg",
-      "sources": [
-        "https://derpibooru.org/2067468"
-      ],
+      "sources": ["https://derpibooru.org/2067468"],
       "tag_input": "artist:cheezedoodle96, babs seed, bloom and gloom, cutie mark, cutie mark only, no pony, safe, scissors, simple background, svg, .svg available, transparent background, vector"
     }
   ],
@@ -69,33 +60,28 @@
     "embedded image inside a spoiler: ||who needs it anyway >>1s||",
     "spoilers inside of a table\n\nHello | World\n--- | ---:\n`||cool beans!||` | ||cool beans!||"
   ],
-  "forum_posts": [{
-    "forum": "dis",
-    "topics": [{
-      "title": "Example Topic",
-        "posts": [
-          "example post",
-          "yet another example post"
-        ]
-      },
-      {
-        "title": "Second Example Topic",
-        "posts": [
-          "post",
-          "post 2"
-        ]
-      }
-    ]},
+  "forum_posts": [
+    {
+      "forum": "dis",
+      "topics": [
+        {
+          "title": "Example Topic",
+          "posts": ["example post", "yet another example post"]
+        },
+        {
+          "title": "Second Example Topic",
+          "posts": ["post", "post 2"]
+        }
+      ]
+    },
     {
       "forum": "art",
-      "topics": [{
-        "title": "Embedded Images",
-        "posts": [
-          ">>1t >>1s >>1p",
-          ">>1",
-          "non-existent: >>1000t >>1000s >>1000p >>1000"
-        ]
-      }]
+      "topics": [
+        {
+          "title": "Embedded Images",
+          "posts": [">>1t >>1s >>1p", ">>1", "non-existent: >>1000t >>1000s >>1000p >>1000"]
+        }
+      ]
     }
   ]
 }

From f9cdfbd346b6fb2cf1b0969f47239a64139f0f81 Mon Sep 17 00:00:00 2001
From: MareStare <mare.stare.official@gmail.com>
Date: Mon, 17 Mar 2025 02:09:49 +0000
Subject: [PATCH 3/5] Remove the deprecated eslint rules with the help of the
 config inspector. These are mostly the formatting rules.

---
 assets/eslint.config.js | 80 +----------------------------------------
 1 file changed, 1 insertion(+), 79 deletions(-)

diff --git a/assets/eslint.config.js b/assets/eslint.config.js
index 1c3f2b75..85f7daeb 100644
--- a/assets/eslint.config.js
+++ b/assets/eslint.config.js
@@ -20,69 +20,37 @@ export default tsEslint.config(
     },
     rules: {
       'accessor-pairs': 2,
-      'array-bracket-spacing': 0,
       'array-callback-return': 2,
-      'arrow-body-style': 0,
-      'arrow-spacing': 2,
       'block-scoped-var': 2,
-      'block-spacing': 2,
-      'callback-return': 0,
       camelcase: [2, { allow: ['camo_url', 'spoiler_image_uri', 'image_ids'] }],
       'class-methods-use-this': 0,
-      'comma-dangle': [2, 'only-multiline'],
-      'comma-spacing': 2,
-      'comma-style': 2,
       complexity: 0,
-      'computed-property-spacing': [2, 'never'],
       'consistent-return': 0,
       'consistent-this': [2, 'that'],
       'constructor-super': 2,
       curly: [2, 'multi-line', 'consistent'],
       'default-case': 2,
-      'dot-location': [2, 'property'],
       'dot-notation': [2, { allowKeywords: true }],
-      'eol-last': 2,
       eqeqeq: 2,
-      'func-call-spacing': 0,
       'func-name-matching': 2,
       'func-names': 0,
       'func-style': 0,
-      'generator-star-spacing': 2,
-      'global-require': 2,
       'guard-for-in': 0,
-      'handle-callback-err': 2,
-      'id-blacklist': 0,
       'id-length': 0,
       'id-match': 2,
       'init-declarations': 0,
-      'jsx-quotes': 0,
-      'key-spacing': 0,
-      'keyword-spacing': 2,
-      'line-comment-position': 0,
-      'linebreak-style': [2, 'unix'],
-      'lines-around-comment': 0,
-      'lines-around-directive': 2,
       'max-depth': 0,
-      'max-len': 0,
       'max-lines': 0,
       'max-nested-callbacks': 0,
       'max-params': 0,
-      'max-statements-per-line': 0,
       'max-statements': 0,
-      'multiline-ternary': 0,
       'new-cap': 2,
-      'new-parens': 2,
-      'newline-after-var': 0,
-      'newline-before-return': 0,
-      'newline-per-chained-call': 0,
       'no-alert': 0,
       'no-array-constructor': 2,
       'no-caller': 2,
       'no-case-declarations': 2,
-      'no-catch-shadow': 2,
       'no-class-assign': 2,
       'no-cond-assign': 2,
-      'no-confusing-arrow': 2,
       'no-console': 0,
       'no-const-assign': 2,
       'no-constant-condition': 2,
@@ -107,10 +75,7 @@ export default tsEslint.config(
       'no-extra-bind': 2,
       'no-extra-boolean-cast': 2,
       'no-extra-label': 2,
-      'no-extra-parens': [2, 'all', { nestedBinaryExpressions: false }],
-      'no-extra-semi': 2,
       'no-fallthrough': 2,
-      'no-floating-decimal': 2,
       'no-func-assign': 2,
       'no-global-assign': 2,
       'no-implicit-coercion': 2,
@@ -128,37 +93,24 @@ export default tsEslint.config(
       'no-lonely-if': 0,
       'no-loop-func': 2,
       'no-magic-numbers': 0,
-      'no-mixed-operators': 0,
-      'no-mixed-requires': 0,
-      'no-mixed-spaces-and-tabs': 2,
-      'no-multi-spaces': 0,
       'no-multi-str': 2,
-      'no-multiple-empty-lines': [2, { max: 3, maxBOF: 0, maxEOF: 1 }],
-      'no-native-reassign': 2,
       'no-negated-condition': 0,
-      'no-negated-in-lhs': 2,
       'no-nested-ternary': 2,
       'no-new-func': 2,
-      'no-new-object': 2,
-      'no-new-require': 2,
-      'no-new-symbol': 2,
       'no-new-wrappers': 2,
       'no-new': 2,
       'no-obj-calls': 2,
+      'no-object-constructor': 2,
       'no-octal-escape': 2,
       'no-octal': 2,
       'no-param-reassign': 2,
-      'no-path-concat': 2,
       'no-plusplus': 0,
-      'no-process-env': 2,
-      'no-process-exit': 2,
       'no-proto': 2,
       'no-prototype-builtins': 0,
       'no-redeclare': 2,
       'no-regex-spaces': 2,
       'no-restricted-globals': [2, 'event'],
       'no-restricted-imports': 2,
-      'no-restricted-modules': 2,
       'no-restricted-properties': 0,
       'no-restricted-syntax': 2,
       'no-return-assign': 0,
@@ -167,16 +119,11 @@ export default tsEslint.config(
       'no-self-compare': 2,
       'no-sequences': 2,
       'no-shadow-restricted-names': 2,
-      'no-shadow': 2,
-      'no-spaced-func': 2,
       'no-sparse-arrays': 2,
-      'no-sync': 0,
-      'no-tabs': 2,
       'no-template-curly-in-string': 2,
       'no-ternary': 0,
       'no-this-before-super': 2,
       'no-throw-literal': 2,
-      'no-trailing-spaces': 2,
       'no-undef-init': 2,
       'no-undef': 2,
       'no-undefined': 0,
@@ -200,50 +147,29 @@ export default tsEslint.config(
       'no-var': 2,
       'no-void': 2,
       'no-warning-comments': 0,
-      'no-whitespace-before-property': 2,
       'no-with': 2,
-      'object-curly-newline': 0,
-      'object-curly-spacing': 0,
-      'object-property-newline': 0,
       'object-shorthand': 2,
-      'one-var-declaration-per-line': 0,
       'one-var': 0,
       'operator-assignment': [2, 'always'],
-      'operator-linebreak': 0,
-      'padded-blocks': 0,
       'prefer-arrow-callback': 2,
       'prefer-const': 2,
       'prefer-numeric-literals': 2,
-      'prefer-reflect': 0,
       'prefer-rest-params': 2,
       'prefer-spread': 0,
       'prefer-template': 2,
-      'quote-props': [2, 'as-needed'],
       radix: 2,
       'require-jsdoc': 0,
       'require-yield': 2,
-      'rest-spread-spacing': 2,
-      'semi-spacing': [2, { before: false, after: true }],
-      semi: 2,
       'sort-imports': 0,
       'sort-keys': 0,
       'sort-vars': 0,
-      'space-before-blocks': [2, 'always'],
-      'space-in-parens': [2, 'never'],
-      'space-infix-ops': 2,
-      'space-unary-ops': [2, { words: true, nonwords: false }],
-      'spaced-comment': 0,
       strict: [2, 'function'],
       'symbol-description': 2,
-      'template-curly-spacing': [2, 'never'],
       'unicode-bom': 2,
       'use-isnan': 2,
       'valid-jsdoc': 0,
       'valid-typeof': 2,
       'vars-on-top': 2,
-      'wrap-iife': 2,
-      'wrap-regex': 0,
-      'yield-star-spacing': 2,
       yoda: [2, 'never'],
     },
     ignores: ['js/vendor/*', 'vite.config.ts'],
@@ -264,10 +190,6 @@ export default tsEslint.config(
       'no-redeclare': 'off',
       'no-shadow': 'off',
 
-      // Often conflicts with prettier.
-      // TODO: prettier should just be enforced by CI.
-      'no-extra-parens': 'off',
-
       '@typescript-eslint/no-unused-vars': [
         2,
         { vars: 'all', args: 'after-used', varsIgnorePattern: '^_.*', argsIgnorePattern: '^_.*' },

From cb8de368ab3bf5ccc3d1d73e835846ac54163eb9 Mon Sep 17 00:00:00 2001
From: MareStare <mare.stare.official@gmail.com>
Date: Mon, 17 Mar 2025 02:53:22 +0000
Subject: [PATCH 4/5] Add a pre-commit hook for auto-formatting and lightweight
 checks

---
 .githooks/pre-commit | 57 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 57 insertions(+)
 create mode 100755 .githooks/pre-commit

diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 00000000..39ba0902
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,57 @@
+#!/usr/bin/env bash
+#
+# Pre-commit hook to run lightweight checks and auto-format the code. It's designed
+# to be blazingly fast, so it checks only changed files. Run the following command
+# to install this hook for yourself. It's a symlink, to make sure it stays always
+# up-to-date.
+#
+# ```bash
+# ln -s ../../.githooks/pre-commit .git/hooks/pre-commit
+# ```
+
+set -euxo pipefail
+
+function command_exists() {
+  bin_name=$(basename "$1")
+
+  if command -v "$1" &> /dev/null; then
+    printf "\e[0;32m[INFO] Using %s...\e[0m\n" "$bin_name"
+    return 0
+  fi
+
+  printf "\e[0;33m[WARN] %s CLI was not found. Ignoring it...\e[0m\n" "$bin_name" >&2
+  return 1
+}
+
+files=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g')
+
+if [[ -z "$files" ]]; then
+  echo "No files changed. Exiting the pre-commit hook..."
+  exit 0
+fi
+
+if command_exists typos; then
+  echo "$files" | xargs typos
+fi
+
+if command_exists ./node_modules/.bin/prettier; then
+  echo "$files" | xargs ./node_modules/.bin/prettier --ignore-unknown --write
+fi
+
+if command_exists cargo; then
+  # `rustfmt` doesn't ignore non-rust files automatically
+  rust_files=$(echo "$files" | { grep -E '\.rs$' || true; })
+
+  if [[ -n "$rust_files" ]]; then
+    echo "$rust_files" | xargs cargo fmt --manifest-path native/Cargo.toml --
+  fi
+fi
+
+if command_exists mix; then
+  echo "$files" | xargs mix format --check-formatted
+fi
+
+# Add the modified/prettified files to staging
+echo "$files" | xargs git add
+
+exit 0

From b9e368cd2b7143963f805d1917d7dee04d99601b Mon Sep 17 00:00:00 2001
From: MareStare <mare.stare.official@gmail.com>
Date: Mon, 17 Mar 2025 03:04:18 +0000
Subject: [PATCH 5/5] Add prettier to CI

---
 .github/workflows/elixir.yml | 26 +++++++++++++++-----------
 package.json                 |  3 +--
 2 files changed, 16 insertions(+), 13 deletions(-)

diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml
index a5099702..645897eb 100644
--- a/.github/workflows/elixir.yml
+++ b/.github/workflows/elixir.yml
@@ -59,6 +59,18 @@ jobs:
       - name: cargo test
         run: (cd native/philomena && cargo test)
 
+  prettier:
+    name: 'Prettier Formatting Check'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with:
+          node-version: '22'
+          cache: 'npm'
+      - run: npm ci --ignore-scripts
+      - run: npx prettier --check .
+
   lint-and-test:
     name: 'JavaScript Linting and Unit Tests'
     runs-on: ubuntu-latest
@@ -68,18 +80,10 @@ jobs:
       - name: Setup Node.js
         uses: actions/setup-node@v4
         with:
-          node-version: '20'
+          node-version: '22'
+          cache: 'npm'
 
-      - name: Cache node_modules
-        id: cache-node-modules
-        uses: actions/cache@v4
-        with:
-          path: ./assets/node_modules
-          key: node_modules-${{ hashFiles('./assets/package-lock.json') }}
-
-      - name: Install npm dependencies
-        if: steps.cache-node-modules.outputs.cache-hit != 'true'
-        run: npm ci --ignore-scripts
+      - run: npm ci --ignore-scripts
         working-directory: ./assets
 
       - run: npm run lint
diff --git a/package.json b/package.json
index 9528d2d8..76e7bb99 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,6 @@
 {
   "scripts": {
-    "fmt": "prettier --write .",
-    "fmt-check": "prettier --check ."
+    "fmt": "prettier --write ."
   },
   "devDependencies": {
     "prettier": "^3.5.3"