« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/upd8.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/upd8.js')
-rwxr-xr-xsrc/upd8.js3843
1 files changed, 2080 insertions, 1763 deletions
diff --git a/src/upd8.js b/src/upd8.js
index d9bca28..576166a 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -31,153 +31,145 @@
 // Oh yeah, like. Just run this through some relatively recent version of
 // node.js and you'll 8e fine. ...Within the project root. O8viously.
 
-import * as path from 'path';
-import { promisify } from 'util';
-import { fileURLToPath } from 'url';
+import * as path from "path";
+import { promisify } from "util";
+import { fileURLToPath } from "url";
 
 // I made this dependency myself! A long, long time ago. It is pro8a8ly my
 // most useful li8rary ever. I'm not sure 8esides me actually uses it, though.
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
 // Wait nevermind, I forgot a8out why-do-kids-love-the-taste-of-cinnamon-toast-
 // crunch. THAT is my 8est li8rary.
 
 // It stands for "HTML Entities", apparently. Cursed.
-import he from 'he';
+import he from "he";
 
 import {
-    copyFile,
-    mkdir,
-    readFile,
-    stat,
-    symlink,
-    writeFile,
-    unlink,
-} from 'fs/promises';
+  copyFile,
+  mkdir,
+  readFile,
+  stat,
+  symlink,
+  writeFile,
+  unlink,
+} from "fs/promises";
 
-import { inspect as nodeInspect } from 'util';
+import { inspect as nodeInspect } from "util";
 
-import genThumbs from './gen-thumbs.js';
-import { listingSpec, listingTargetSpec } from './listing-spec.js';
-import urlSpec from './url-spec.js';
-import * as pageSpecs from './page/index.js';
+import genThumbs from "./gen-thumbs.js";
+import { listingSpec, listingTargetSpec } from "./listing-spec.js";
+import urlSpec from "./url-spec.js";
+import * as pageSpecs from "./page/index.js";
 
-import find, { bindFind } from './util/find.js';
-import * as html from './util/html.js';
-import unbound_link, {getLinkThemeString} from './util/link.js';
-import { findFiles } from './util/io.js';
+import find, { bindFind } from "./util/find.js";
+import * as html from "./util/html.js";
+import unbound_link, { getLinkThemeString } from "./util/link.js";
+import { findFiles } from "./util/io.js";
 
-import CacheableObject from './data/cacheable-object.js';
+import CacheableObject from "./data/cacheable-object.js";
 
-import { serializeThings } from './data/serialize.js';
+import { serializeThings } from "./data/serialize.js";
 
-import {
-    Language,
-} from './data/things.js';
-
-import {
-    filterDuplicateDirectories,
-    filterReferenceErrors,
-    linkWikiDataArrays,
-    loadAndProcessDataDocuments,
-    sortWikiDataArrays,
-    WIKI_INFO_FILE,
-} from './data/yaml.js';
+import { Language } from "./data/things.js";
 
 import {
-    fancifyFlashURL,
-    fancifyURL,
-    generateAdditionalFilesShortcut,
-    generateAdditionalFilesList,
-    generateChronologyLinks,
-    generateCoverLink,
-    generateInfoGalleryLinks,
-    generatePreviousNextLinks,
-    generateTrackListDividedByGroups,
-    getAlbumGridHTML,
-    getAlbumStylesheet,
-    getArtistString,
-    getFlashGridHTML,
-    getFooterLocalizationLinks,
-    getGridHTML,
-    getRevealStringFromTags,
-    getRevealStringFromWarnings,
-    getThemeString,
-    iconifyURL
-} from './misc-templates.js';
+  filterDuplicateDirectories,
+  filterReferenceErrors,
+  linkWikiDataArrays,
+  loadAndProcessDataDocuments,
+  sortWikiDataArrays,
+  WIKI_INFO_FILE,
+} from "./data/yaml.js";
 
 import {
-    color,
-    decorateTime,
-    logWarn,
-    logInfo,
-    logError,
-    parseOptions,
-    progressPromiseAll,
-    ENABLE_COLOR
-} from './util/cli.js';
+  fancifyFlashURL,
+  fancifyURL,
+  generateAdditionalFilesShortcut,
+  generateAdditionalFilesList,
+  generateChronologyLinks,
+  generateCoverLink,
+  generateInfoGalleryLinks,
+  generatePreviousNextLinks,
+  generateTrackListDividedByGroups,
+  getAlbumGridHTML,
+  getAlbumStylesheet,
+  getArtistString,
+  getFlashGridHTML,
+  getFooterLocalizationLinks,
+  getGridHTML,
+  getRevealStringFromTags,
+  getRevealStringFromWarnings,
+  getThemeString,
+  iconifyURL,
+} from "./misc-templates.js";
 
 import {
-    validateReplacerSpec,
-    transformInline
-} from './util/replacer.js';
+  color,
+  decorateTime,
+  logWarn,
+  logInfo,
+  logError,
+  parseOptions,
+  progressPromiseAll,
+  ENABLE_COLOR,
+} from "./util/cli.js";
+
+import { validateReplacerSpec, transformInline } from "./util/replacer.js";
 
 import {
-    chunkByConditions,
-    chunkByProperties,
-    getAlbumCover,
-    getAlbumListTag,
-    getAllTracks,
-    getArtistAvatar,
-    getArtistNumContributions,
-    getFlashCover,
-    getKebabCase,
-    getTotalDuration,
-    getTrackCover,
-} from './util/wiki-data.js';
+  chunkByConditions,
+  chunkByProperties,
+  getAlbumCover,
+  getAlbumListTag,
+  getAllTracks,
+  getArtistAvatar,
+  getArtistNumContributions,
+  getFlashCover,
+  getKebabCase,
+  getTotalDuration,
+  getTrackCover,
+} from "./util/wiki-data.js";
 
 import {
-    serializeContribs,
-    serializeCover,
-    serializeGroupsForAlbum,
-    serializeGroupsForTrack,
-    serializeImagePaths,
-    serializeLink
-} from './util/serialize.js';
+  serializeContribs,
+  serializeCover,
+  serializeGroupsForAlbum,
+  serializeGroupsForTrack,
+  serializeImagePaths,
+  serializeLink,
+} from "./util/serialize.js";
 
 import {
-    bindOpts,
-    decorateErrorWithIndex,
-    filterAggregateAsync,
-    filterEmptyLines,
-    mapAggregate,
-    mapAggregateAsync,
-    openAggregate,
-    queue,
-    showAggregate,
-    splitArray,
-    unique,
-    withAggregate,
-    withEntries
-} from './util/sugar.js';
-
-import {
-    generateURLs,
-    thumb
-} from './util/urls.js';
+  bindOpts,
+  decorateErrorWithIndex,
+  filterAggregateAsync,
+  filterEmptyLines,
+  mapAggregate,
+  mapAggregateAsync,
+  openAggregate,
+  queue,
+  showAggregate,
+  splitArray,
+  unique,
+  withAggregate,
+  withEntries,
+} from "./util/sugar.js";
+
+import { generateURLs, thumb } from "./util/urls.js";
 
 // Pensive emoji!
 import {
-    FANDOM_GROUP_DIRECTORY,
-    OFFICIAL_GROUP_DIRECTORY
-} from './util/magic-constants.js';
+  FANDOM_GROUP_DIRECTORY,
+  OFFICIAL_GROUP_DIRECTORY,
+} from "./util/magic-constants.js";
 
-import FileSizePreloader from './file-size-preloader.js';
+import FileSizePreloader from "./file-size-preloader.js";
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 const CACHEBUST = 10;
 
-const DEFAULT_STRINGS_FILE = 'strings-default.json';
+const DEFAULT_STRINGS_FILE = "strings-default.json";
 
 // Code that's common 8etween the 8uild code (i.e. upd8.js) and gener8ted
 // site code should 8e put here. Which, uh, ~~only really means this one
@@ -186,20 +178,20 @@ const DEFAULT_STRINGS_FILE = 'strings-default.json';
 // Rather than hard code it, anything in this directory can 8e shared across
 // 8oth ends of the code8ase.
 // (This gets symlinked into the --data-path directory.)
-const UTILITY_DIRECTORY = 'util';
+const UTILITY_DIRECTORY = "util";
 
 // Code that's used only in the static site! CSS, cilent JS, etc.
 // (This gets symlinked into the --data-path directory.)
-const STATIC_DIRECTORY = 'static';
+const STATIC_DIRECTORY = "static";
 
 // This exists adjacent to index.html for any page with oEmbed metadata.
-const OEMBED_JSON_FILE = 'oembed.json';
+const OEMBED_JSON_FILE = "oembed.json";
 
 // Automatically copied (if present) from media directory to site root.
-const FAVICON_FILE = 'favicon.ico';
+const FAVICON_FILE = "favicon.ico";
 
 function inspect(value) {
-    return nodeInspect(value, {colors: ENABLE_COLOR});
+  return nodeInspect(value, { colors: ENABLE_COLOR });
 }
 
 // Shared varia8les! These are more efficient to access than a shared varia8le
@@ -223,556 +215,610 @@ let queueSize;
 const urls = generateURLs(urlSpec);
 
 function splitLines(text) {
-    return text.split(/\r\n|\r|\n/);
+  return text.split(/\r\n|\r|\n/);
 }
 
 const replacerSpec = {
-    'album': {
-        find: 'album',
-        link: 'album'
-    },
-    'album-commentary': {
-        find: 'album',
-        link: 'albumCommentary'
-    },
-    'artist': {
-        find: 'artist',
-        link: 'artist'
-    },
-    'artist-gallery': {
-        find: 'artist',
-        link: 'artistGallery'
-    },
-    'commentary-index': {
-        find: null,
-        link: 'commentaryIndex'
-    },
-    'date': {
-        find: null,
-        value: ref => new Date(ref),
-        html: (date, {language}) => `<time datetime="${date.toString()}">${language.formatDate(date)}</time>`
-    },
-    'flash': {
-        find: 'flash',
-        link: 'flash',
-        transformName(name, node, input) {
-            const nextCharacter = input[node.iEnd];
-            const lastCharacter = name[name.length - 1];
-            if (
-                ![' ', '\n', '<'].includes(nextCharacter) &&
-                lastCharacter === '.'
-            ) {
-                return name.slice(0, -1);
-            } else {
-                return name;
-            }
-        }
-    },
-    'group': {
-        find: 'group',
-        link: 'groupInfo'
-    },
-    'group-gallery': {
-        find: 'group',
-        link: 'groupGallery'
-    },
-    'home': {
-        find: null,
-        link: 'home'
-    },
-    'listing-index': {
-        find: null,
-        link: 'listingIndex'
-    },
-    'listing': {
-        find: 'listing',
-        link: 'listing'
-    },
-    'media': {
-        find: null,
-        link: 'media'
-    },
-    'news-index': {
-        find: null,
-        link: 'newsIndex'
-    },
-    'news-entry': {
-        find: 'newsEntry',
-        link: 'newsEntry'
-    },
-    'root': {
-        find: null,
-        link: 'root'
-    },
-    'site': {
-        find: null,
-        link: 'site'
-    },
-    'static': {
-        find: 'staticPage',
-        link: 'staticPage'
-    },
-    'string': {
-        find: null,
-        value: ref => ref,
-        html: (ref, {language, args}) => language.$(ref, args)
+  album: {
+    find: "album",
+    link: "album",
+  },
+  "album-commentary": {
+    find: "album",
+    link: "albumCommentary",
+  },
+  artist: {
+    find: "artist",
+    link: "artist",
+  },
+  "artist-gallery": {
+    find: "artist",
+    link: "artistGallery",
+  },
+  "commentary-index": {
+    find: null,
+    link: "commentaryIndex",
+  },
+  date: {
+    find: null,
+    value: (ref) => new Date(ref),
+    html: (date, { language }) =>
+      `<time datetime="${date.toString()}">${language.formatDate(date)}</time>`,
+  },
+  flash: {
+    find: "flash",
+    link: "flash",
+    transformName(name, node, input) {
+      const nextCharacter = input[node.iEnd];
+      const lastCharacter = name[name.length - 1];
+      if (![" ", "\n", "<"].includes(nextCharacter) && lastCharacter === ".") {
+        return name.slice(0, -1);
+      } else {
+        return name;
+      }
     },
-    'tag': {
-        find: 'artTag',
-        link: 'tag'
-    },
-    'track': {
-        find: 'track',
-        link: 'track'
-    }
+  },
+  group: {
+    find: "group",
+    link: "groupInfo",
+  },
+  "group-gallery": {
+    find: "group",
+    link: "groupGallery",
+  },
+  home: {
+    find: null,
+    link: "home",
+  },
+  "listing-index": {
+    find: null,
+    link: "listingIndex",
+  },
+  listing: {
+    find: "listing",
+    link: "listing",
+  },
+  media: {
+    find: null,
+    link: "media",
+  },
+  "news-index": {
+    find: null,
+    link: "newsIndex",
+  },
+  "news-entry": {
+    find: "newsEntry",
+    link: "newsEntry",
+  },
+  root: {
+    find: null,
+    link: "root",
+  },
+  site: {
+    find: null,
+    link: "site",
+  },
+  static: {
+    find: "staticPage",
+    link: "staticPage",
+  },
+  string: {
+    find: null,
+    value: (ref) => ref,
+    html: (ref, { language, args }) => language.$(ref, args),
+  },
+  tag: {
+    find: "artTag",
+    link: "tag",
+  },
+  track: {
+    find: "track",
+    link: "track",
+  },
 };
 
-if (!validateReplacerSpec(replacerSpec, {find, link: unbound_link})) {
-    process.exit();
+if (!validateReplacerSpec(replacerSpec, { find, link: unbound_link })) {
+  process.exit();
 }
 
-function parseAttributes(string, {to}) {
-    const attributes = Object.create(null);
-    const skipWhitespace = i => {
-        const ws = /\s/;
-        if (ws.test(string[i])) {
-            const match = string.slice(i).match(/[^\s]/);
-            if (match) {
-                return i + match.index;
-            } else {
-                return string.length;
-            }
-        } else {
-            return i;
-        }
-    };
-
-    for (let i = 0; i < string.length;) {
-        i = skipWhitespace(i);
-        const aStart = i;
-        const aEnd = i + string.slice(i).match(/[\s=]|$/).index;
-        const attribute = string.slice(aStart, aEnd);
-        i = skipWhitespace(aEnd);
-        if (string[i] === '=') {
-            i = skipWhitespace(i + 1);
-            let end, endOffset;
-            if (string[i] === '"' || string[i] === "'") {
-                end = string[i];
-                endOffset = 1;
-                i++;
-            } else {
-                end = '\\s';
-                endOffset = 0;
-            }
-            const vStart = i;
-            const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index;
-            const value = string.slice(vStart, vEnd);
-            i = vEnd + endOffset;
-            if (attribute === 'src' && value.startsWith('media/')) {
-                attributes[attribute] = to('media.path', value.slice('media/'.length));
-            } else {
-                attributes[attribute] = value;
-            }
-        } else {
-            attributes[attribute] = attribute;
-        }
+function parseAttributes(string, { to }) {
+  const attributes = Object.create(null);
+  const skipWhitespace = (i) => {
+    const ws = /\s/;
+    if (ws.test(string[i])) {
+      const match = string.slice(i).match(/[^\s]/);
+      if (match) {
+        return i + match.index;
+      } else {
+        return string.length;
+      }
+    } else {
+      return i;
     }
-    return Object.fromEntries(Object.entries(attributes).map(([ key, val ]) => [
-        key,
-        val === 'true' ? true :
-        val === 'false' ? false :
-        val === key ? true :
-        val
-    ]));
+  };
+
+  for (let i = 0; i < string.length; ) {
+    i = skipWhitespace(i);
+    const aStart = i;
+    const aEnd = i + string.slice(i).match(/[\s=]|$/).index;
+    const attribute = string.slice(aStart, aEnd);
+    i = skipWhitespace(aEnd);
+    if (string[i] === "=") {
+      i = skipWhitespace(i + 1);
+      let end, endOffset;
+      if (string[i] === '"' || string[i] === "'") {
+        end = string[i];
+        endOffset = 1;
+        i++;
+      } else {
+        end = "\\s";
+        endOffset = 0;
+      }
+      const vStart = i;
+      const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index;
+      const value = string.slice(vStart, vEnd);
+      i = vEnd + endOffset;
+      if (attribute === "src" && value.startsWith("media/")) {
+        attributes[attribute] = to("media.path", value.slice("media/".length));
+      } else {
+        attributes[attribute] = value;
+      }
+    } else {
+      attributes[attribute] = attribute;
+    }
+  }
+  return Object.fromEntries(
+    Object.entries(attributes).map(([key, val]) => [
+      key,
+      val === "true"
+        ? true
+        : val === "false"
+        ? false
+        : val === key
+        ? true
+        : val,
+    ])
+  );
 }
 
 function joinLineBreaks(sourceLines) {
-    const outLines = [];
-
-    let lineSoFar = '';
-    for (let i = 0; i < sourceLines.length; i++) {
-        const line = sourceLines[i];
-        lineSoFar += line;
-        if (!line.endsWith('<br>')) {
-            outLines.push(lineSoFar);
-            lineSoFar = '';
-        }
+  const outLines = [];
+
+  let lineSoFar = "";
+  for (let i = 0; i < sourceLines.length; i++) {
+    const line = sourceLines[i];
+    lineSoFar += line;
+    if (!line.endsWith("<br>")) {
+      outLines.push(lineSoFar);
+      lineSoFar = "";
     }
+  }
 
-    if (lineSoFar) {
-        outLines.push(lineSoFar);
-    }
+  if (lineSoFar) {
+    outLines.push(lineSoFar);
+  }
 
-    return outLines;
+  return outLines;
 }
 
-function transformMultiline(text, {
-    parseAttributes,
-    transformInline
-}) {
-    // Heck yes, HTML magics.
-
-    text = transformInline(text.trim());
-
-    const outLines = [];
-
-    const indentString = ' '.repeat(4);
-
-    let levelIndents = [];
-    const openLevel = indent => {
-        // opening a sublist is a pain: to be semantically *and* visually
-        // correct, we have to append the <ul> at the end of the existing
-        // previous <li>
-        const previousLine = outLines[outLines.length - 1];
-        if (previousLine?.endsWith('</li>')) {
-            // we will re-close the <li> later
-            outLines[outLines.length - 1] = previousLine.slice(0, -5) + ' <ul>';
-        } else {
-            // if the previous line isn't a list item, this is the opening of
-            // the first list level, so no need for indent
-            outLines.push('<ul>');
-        }
-        levelIndents.push(indent);
-    };
-    const closeLevel = () => {
-        levelIndents.pop();
-        if (levelIndents.length) {
-            // closing a sublist, so close the list item containing it too
-            outLines.push(indentString.repeat(levelIndents.length) + '</ul></li>');
-        } else {
-            // closing the final list level! no need for indent here
-            outLines.push('</ul>');
-        }
-    };
+function transformMultiline(text, { parseAttributes, transformInline }) {
+  // Heck yes, HTML magics.
 
-    // okay yes we should support nested formatting, more than one blockquote
-    // layer, etc, but hear me out here: making all that work would basically
-    // be the same as implementing an entire markdown converter, which im not
-    // interested in doing lol. sorry!!!
-    let inBlockquote = false;
-
-    let lines = splitLines(text);
-    lines = joinLineBreaks(lines);
-    for (let line of lines) {
-        const imageLine = line.startsWith('<img');
-        line = line.replace(/<img (.*?)>/g, (match, attributes) => img({
-            lazy: true,
-            link: true,
-            thumb: 'medium',
-            ...parseAttributes(attributes)
-        }));
-
-        let indentThisLine = 0;
-        let lineContent = line;
-        let lineTag = 'p';
-
-        const listMatch = line.match(/^( *)- *(.*)$/);
-        if (listMatch) {
-            // is a list item!
-            if (!levelIndents.length) {
-                // first level is always indent = 0, regardless of actual line
-                // content (this is to avoid going to a lesser indent than the
-                // initial level)
-                openLevel(0);
-            } else {
-                // find level corresponding to indent
-                const indent = listMatch[1].length;
-                let i;
-                for (i = levelIndents.length - 1; i >= 0; i--) {
-                    if (levelIndents[i] <= indent) break;
-                }
-                // note: i cannot equal -1 because the first indentation level
-                // is always 0, and the minimum indentation is also 0
-                if (levelIndents[i] === indent) {
-                    // same indent! return to that level
-                    while (levelIndents.length - 1 > i) closeLevel();
-                    // (if this is already the current level, the above loop
-                    // will do nothing)
-                } else if (levelIndents[i] < indent) {
-                    // lesser indent! branch based on index
-                    if (i === levelIndents.length - 1) {
-                        // top level is lesser: add a new level
-                        openLevel(indent);
-                    } else {
-                        // lower level is lesser: return to that level
-                        while (levelIndents.length - 1 > i) closeLevel();
-                    }
-                }
-            }
-            // finally, set variables for appending content line
-            indentThisLine = levelIndents.length;
-            lineContent = listMatch[2];
-            lineTag = 'li';
-        } else {
-            // not a list item! close any existing list levels
-            while (levelIndents.length) closeLevel();
-
-            // like i said, no nested shenanigans - quotes only appear outside
-            // of lists. sorry!
-            const quoteMatch = line.match(/^> *(.*)$/);
-            if (quoteMatch) {
-                // is a quote! open a blockquote tag if it doesnt already exist
-                if (!inBlockquote) {
-                    inBlockquote = true;
-                    outLines.push('<blockquote>');
-                }
-                indentThisLine = 1;
-                lineContent = quoteMatch[1];
-            } else if (inBlockquote) {
-                // not a quote! close a blockquote tag if it exists
-                inBlockquote = false;
-                outLines.push('</blockquote>');
-            }
+  text = transformInline(text.trim());
 
-            // let some escaped symbols display as the normal symbol, since the
-            // point of escaping them is just to avoid having them be treated as
-            // syntax markers!
-            if (lineContent.match(/( *)\\-/)) {
-                lineContent = lineContent.replace('\\-', '-');
-            } else if (lineContent.match(/( *)\\>/)) {
-                lineContent = lineContent.replace('\\>', '>');
-            }
-        }
+  const outLines = [];
 
-        if (lineTag === 'p') {
-            // certain inline element tags should still be postioned within a
-            // paragraph; other elements (e.g. headings) should be added as-is
-            const elementMatch = line.match(/^<(.*?)[ >]/);
-            if (elementMatch && !imageLine && !['a', 'abbr', 'b', 'bdo', 'br', 'cite', 'code', 'data', 'datalist', 'del', 'dfn', 'em', 'i', 'img', 'ins', 'kbd', 'mark', 'output', 'picture', 'q', 'ruby', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'svg', 'time', 'var', 'wbr'].includes(elementMatch[1])) {
-                lineTag = '';
-            }
-        }
+  const indentString = " ".repeat(4);
 
-        let pushString = indentString.repeat(indentThisLine);
-        if (lineTag) {
-            pushString += `<${lineTag}>${lineContent}</${lineTag}>`;
-        } else {
-            pushString += lineContent;
-        }
-        outLines.push(pushString);
+  let levelIndents = [];
+  const openLevel = (indent) => {
+    // opening a sublist is a pain: to be semantically *and* visually
+    // correct, we have to append the <ul> at the end of the existing
+    // previous <li>
+    const previousLine = outLines[outLines.length - 1];
+    if (previousLine?.endsWith("</li>")) {
+      // we will re-close the <li> later
+      outLines[outLines.length - 1] = previousLine.slice(0, -5) + " <ul>";
+    } else {
+      // if the previous line isn't a list item, this is the opening of
+      // the first list level, so no need for indent
+      outLines.push("<ul>");
     }
+    levelIndents.push(indent);
+  };
+  const closeLevel = () => {
+    levelIndents.pop();
+    if (levelIndents.length) {
+      // closing a sublist, so close the list item containing it too
+      outLines.push(indentString.repeat(levelIndents.length) + "</ul></li>");
+    } else {
+      // closing the final list level! no need for indent here
+      outLines.push("</ul>");
+    }
+  };
+
+  // okay yes we should support nested formatting, more than one blockquote
+  // layer, etc, but hear me out here: making all that work would basically
+  // be the same as implementing an entire markdown converter, which im not
+  // interested in doing lol. sorry!!!
+  let inBlockquote = false;
+
+  let lines = splitLines(text);
+  lines = joinLineBreaks(lines);
+  for (let line of lines) {
+    const imageLine = line.startsWith("<img");
+    line = line.replace(/<img (.*?)>/g, (match, attributes) =>
+      img({
+        lazy: true,
+        link: true,
+        thumb: "medium",
+        ...parseAttributes(attributes),
+      })
+    );
 
-    // after processing all lines...
-
-    // if still in a list, close all levels
-    while (levelIndents.length) closeLevel();
-
-    // if still in a blockquote, close its tag
-    if (inBlockquote) {
+    let indentThisLine = 0;
+    let lineContent = line;
+    let lineTag = "p";
+
+    const listMatch = line.match(/^( *)- *(.*)$/);
+    if (listMatch) {
+      // is a list item!
+      if (!levelIndents.length) {
+        // first level is always indent = 0, regardless of actual line
+        // content (this is to avoid going to a lesser indent than the
+        // initial level)
+        openLevel(0);
+      } else {
+        // find level corresponding to indent
+        const indent = listMatch[1].length;
+        let i;
+        for (i = levelIndents.length - 1; i >= 0; i--) {
+          if (levelIndents[i] <= indent) break;
+        }
+        // note: i cannot equal -1 because the first indentation level
+        // is always 0, and the minimum indentation is also 0
+        if (levelIndents[i] === indent) {
+          // same indent! return to that level
+          while (levelIndents.length - 1 > i) closeLevel();
+          // (if this is already the current level, the above loop
+          // will do nothing)
+        } else if (levelIndents[i] < indent) {
+          // lesser indent! branch based on index
+          if (i === levelIndents.length - 1) {
+            // top level is lesser: add a new level
+            openLevel(indent);
+          } else {
+            // lower level is lesser: return to that level
+            while (levelIndents.length - 1 > i) closeLevel();
+          }
+        }
+      }
+      // finally, set variables for appending content line
+      indentThisLine = levelIndents.length;
+      lineContent = listMatch[2];
+      lineTag = "li";
+    } else {
+      // not a list item! close any existing list levels
+      while (levelIndents.length) closeLevel();
+
+      // like i said, no nested shenanigans - quotes only appear outside
+      // of lists. sorry!
+      const quoteMatch = line.match(/^> *(.*)$/);
+      if (quoteMatch) {
+        // is a quote! open a blockquote tag if it doesnt already exist
+        if (!inBlockquote) {
+          inBlockquote = true;
+          outLines.push("<blockquote>");
+        }
+        indentThisLine = 1;
+        lineContent = quoteMatch[1];
+      } else if (inBlockquote) {
+        // not a quote! close a blockquote tag if it exists
         inBlockquote = false;
-        outLines.push('</blockquote>');
+        outLines.push("</blockquote>");
+      }
+
+      // let some escaped symbols display as the normal symbol, since the
+      // point of escaping them is just to avoid having them be treated as
+      // syntax markers!
+      if (lineContent.match(/( *)\\-/)) {
+        lineContent = lineContent.replace("\\-", "-");
+      } else if (lineContent.match(/( *)\\>/)) {
+        lineContent = lineContent.replace("\\>", ">");
+      }
     }
 
-    return outLines.join('\n');
-}
+    if (lineTag === "p") {
+      // certain inline element tags should still be postioned within a
+      // paragraph; other elements (e.g. headings) should be added as-is
+      const elementMatch = line.match(/^<(.*?)[ >]/);
+      if (
+        elementMatch &&
+        !imageLine &&
+        ![
+          "a",
+          "abbr",
+          "b",
+          "bdo",
+          "br",
+          "cite",
+          "code",
+          "data",
+          "datalist",
+          "del",
+          "dfn",
+          "em",
+          "i",
+          "img",
+          "ins",
+          "kbd",
+          "mark",
+          "output",
+          "picture",
+          "q",
+          "ruby",
+          "samp",
+          "small",
+          "span",
+          "strong",
+          "sub",
+          "sup",
+          "svg",
+          "time",
+          "var",
+          "wbr",
+        ].includes(elementMatch[1])
+      ) {
+        lineTag = "";
+      }
+    }
 
-function transformLyrics(text, {
-    transformInline,
-    transformMultiline
-}) {
-    // Different from transformMultiline 'cuz it joins multiple lines together
-    // with line 8reaks (<br>); transformMultiline treats each line as its own
-    // complete paragraph (or list, etc).
-
-    // If it looks like old data, then like, oh god.
-    // Use the normal transformMultiline tool.
-    if (text.includes('<br')) {
-        return transformMultiline(text);
+    let pushString = indentString.repeat(indentThisLine);
+    if (lineTag) {
+      pushString += `<${lineTag}>${lineContent}</${lineTag}>`;
+    } else {
+      pushString += lineContent;
     }
+    outLines.push(pushString);
+  }
 
-    text = transformInline(text.trim());
+  // after processing all lines...
 
-    let buildLine = '';
-    const addLine = () => outLines.push(`<p>${buildLine}</p>`);
-    const outLines = [];
-    for (const line of text.split('\n')) {
-        if (line.length) {
-            if (buildLine.length) {
-                buildLine += '<br>';
-            }
-            buildLine += line;
-        } else if (buildLine.length) {
-            addLine();
-            buildLine = '';
-        }
-    }
-    if (buildLine.length) {
-        addLine();
+  // if still in a list, close all levels
+  while (levelIndents.length) closeLevel();
+
+  // if still in a blockquote, close its tag
+  if (inBlockquote) {
+    inBlockquote = false;
+    outLines.push("</blockquote>");
+  }
+
+  return outLines.join("\n");
+}
+
+function transformLyrics(text, { transformInline, transformMultiline }) {
+  // Different from transformMultiline 'cuz it joins multiple lines together
+  // with line 8reaks (<br>); transformMultiline treats each line as its own
+  // complete paragraph (or list, etc).
+
+  // If it looks like old data, then like, oh god.
+  // Use the normal transformMultiline tool.
+  if (text.includes("<br")) {
+    return transformMultiline(text);
+  }
+
+  text = transformInline(text.trim());
+
+  let buildLine = "";
+  const addLine = () => outLines.push(`<p>${buildLine}</p>`);
+  const outLines = [];
+  for (const line of text.split("\n")) {
+    if (line.length) {
+      if (buildLine.length) {
+        buildLine += "<br>";
+      }
+      buildLine += line;
+    } else if (buildLine.length) {
+      addLine();
+      buildLine = "";
     }
-    return outLines.join('\n');
+  }
+  if (buildLine.length) {
+    addLine();
+  }
+  return outLines.join("\n");
 }
 
 function stringifyThings(thingData) {
-    return JSON.stringify(serializeThings(thingData));
+  return JSON.stringify(serializeThings(thingData));
 }
 
 function img({
-    src,
-    alt,
-    noSrcText = '',
-    thumb: thumbKey,
-    reveal,
-    id,
+  src,
+  alt,
+  noSrcText = "",
+  thumb: thumbKey,
+  reveal,
+  id,
+  class: className,
+  width,
+  height,
+  link = false,
+  lazy = false,
+  square = false,
+}) {
+  const willSquare = square;
+  const willLink = typeof link === "string" || link;
+
+  const originalSrc = src;
+  const thumbSrc = src && (thumbKey ? thumb[thumbKey](src) : src);
+
+  const imgAttributes = html.attributes({
+    id: link ? "" : id,
     class: className,
+    alt,
     width,
     height,
-    link = false,
-    lazy = false,
-    square = false
-}) {
-    const willSquare = square;
-    const willLink = typeof link === 'string' || link;
-
-    const originalSrc = src;
-    const thumbSrc = src && (thumbKey ? thumb[thumbKey](src) : src);
-
-    const imgAttributes = html.attributes({
-        id: link ? '' : id,
-        class: className,
-        alt,
-        width,
-        height
-    });
-
-    const noSrcHTML = !src && wrap(`<div class="image-text-area">${noSrcText}</div>`);
-    const nonlazyHTML = src && wrap(`<img src="${thumbSrc}" ${imgAttributes}>`);
-    const lazyHTML = src && lazy && wrap(`<img class="lazy" data-original="${thumbSrc}" ${imgAttributes}>`, true);
+  });
+
+  const noSrcHTML =
+    !src && wrap(`<div class="image-text-area">${noSrcText}</div>`);
+  const nonlazyHTML = src && wrap(`<img src="${thumbSrc}" ${imgAttributes}>`);
+  const lazyHTML =
+    src &&
+    lazy &&
+    wrap(
+      `<img class="lazy" data-original="${thumbSrc}" ${imgAttributes}>`,
+      true
+    );
 
-    if (!src) {
-        return noSrcHTML;
-    } else if (lazy) {
-        return fixWS`
+  if (!src) {
+    return noSrcHTML;
+  } else if (lazy) {
+    return fixWS`
             <noscript>${nonlazyHTML}</noscript>
             ${lazyHTML}
         `;
-    } else {
-        return nonlazyHTML;
-    }
+  } else {
+    return nonlazyHTML;
+  }
 
-    function wrap(input, hide = false) {
-        let wrapped = input;
+  function wrap(input, hide = false) {
+    let wrapped = input;
 
-        wrapped = `<div class="image-inner-area">${wrapped}</div>`;
-        wrapped = `<div class="image-container">${wrapped}</div>`;
+    wrapped = `<div class="image-inner-area">${wrapped}</div>`;
+    wrapped = `<div class="image-container">${wrapped}</div>`;
 
-        if (reveal) {
-            wrapped = fixWS`
+    if (reveal) {
+      wrapped = fixWS`
                 <div class="reveal">
                     ${wrapped}
                     <span class="reveal-text">${reveal}</span>
                 </div>
             `;
-        }
-
-        if (willSquare) {
-            wrapped = html.tag('div', {class: 'square-content'}, wrapped);
-            wrapped = html.tag('div', {class: ['square', hide && !willLink && 'js-hide']}, wrapped);
-        }
+    }
 
-        if (willLink) {
-            wrapped = html.tag('a', {
-                id,
-                class: ['box', hide && 'js-hide'],
-                href: typeof link === 'string' ? link : originalSrc
-            }, wrapped);
-        }
+    if (willSquare) {
+      wrapped = html.tag("div", { class: "square-content" }, wrapped);
+      wrapped = html.tag(
+        "div",
+        { class: ["square", hide && !willLink && "js-hide"] },
+        wrapped
+      );
+    }
 
-        return wrapped;
+    if (willLink) {
+      wrapped = html.tag(
+        "a",
+        {
+          id,
+          class: ["box", hide && "js-hide"],
+          href: typeof link === "string" ? link : originalSrc,
+        },
+        wrapped
+      );
     }
+
+    return wrapped;
+  }
 }
 
 function validateWritePath(path, urlGroup) {
-    if (!Array.isArray(path)) {
-        return {error: `Expected array, got ${path}`};
-    }
+  if (!Array.isArray(path)) {
+    return { error: `Expected array, got ${path}` };
+  }
 
-    const { paths } = urlGroup;
+  const { paths } = urlGroup;
 
-    const definedKeys = Object.keys(paths);
-    const specifiedKey = path[0];
+  const definedKeys = Object.keys(paths);
+  const specifiedKey = path[0];
 
-    if (!definedKeys.includes(specifiedKey)) {
-        return {error: `Specified key ${specifiedKey} isn't defined`};
-    }
+  if (!definedKeys.includes(specifiedKey)) {
+    return { error: `Specified key ${specifiedKey} isn't defined` };
+  }
 
-    const expectedArgs = paths[specifiedKey].match(/<>/g)?.length ?? 0;
-    const specifiedArgs = path.length - 1;
+  const expectedArgs = paths[specifiedKey].match(/<>/g)?.length ?? 0;
+  const specifiedArgs = path.length - 1;
 
-    if (specifiedArgs !== expectedArgs) {
-        return {error: `Expected ${expectedArgs} arguments, got ${specifiedArgs}`};
-    }
+  if (specifiedArgs !== expectedArgs) {
+    return {
+      error: `Expected ${expectedArgs} arguments, got ${specifiedArgs}`,
+    };
+  }
 
-    return {success: true};
+  return { success: true };
 }
 
 function validateWriteObject(obj) {
-    if (typeof obj !== 'object') {
-        return {error: `Expected object, got ${typeof obj}`};
-    }
+  if (typeof obj !== "object") {
+    return { error: `Expected object, got ${typeof obj}` };
+  }
 
-    if (typeof obj.type !== 'string') {
-        return {error: `Expected type to be string, got ${obj.type}`};
-    }
+  if (typeof obj.type !== "string") {
+    return { error: `Expected type to be string, got ${obj.type}` };
+  }
 
-    switch (obj.type) {
-        case 'legacy': {
-            if (typeof obj.write !== 'function') {
-                return {error: `Expected write to be string, got ${obj.write}`};
-            }
+  switch (obj.type) {
+    case "legacy": {
+      if (typeof obj.write !== "function") {
+        return { error: `Expected write to be string, got ${obj.write}` };
+      }
 
-            break;
-        }
+      break;
+    }
 
-        case 'page': {
-            const path = validateWritePath(obj.path, urlSpec.localized);
-            if (path.error) {
-                return {error: `Path validation failed: ${path.error}`};
-            }
+    case "page": {
+      const path = validateWritePath(obj.path, urlSpec.localized);
+      if (path.error) {
+        return { error: `Path validation failed: ${path.error}` };
+      }
 
-            if (typeof obj.page !== 'function') {
-                return {error: `Expected page to be function, got ${obj.content}`};
-            }
+      if (typeof obj.page !== "function") {
+        return { error: `Expected page to be function, got ${obj.content}` };
+      }
 
-            break;
-        }
+      break;
+    }
 
-        case 'data': {
-            const path = validateWritePath(obj.path, urlSpec.data);
-            if (path.error) {
-                return {error: `Path validation failed: ${path.error}`};
-            }
+    case "data": {
+      const path = validateWritePath(obj.path, urlSpec.data);
+      if (path.error) {
+        return { error: `Path validation failed: ${path.error}` };
+      }
 
-            if (typeof obj.data !== 'function') {
-                return {error: `Expected data to be function, got ${obj.data}`};
-            }
+      if (typeof obj.data !== "function") {
+        return { error: `Expected data to be function, got ${obj.data}` };
+      }
 
-            break;
-        }
+      break;
+    }
 
-        case 'redirect': {
-            const fromPath = validateWritePath(obj.fromPath, urlSpec.localized);
-            if (fromPath.error) {
-                return {error: `Path (fromPath) validation failed: ${fromPath.error}`};
-            }
+    case "redirect": {
+      const fromPath = validateWritePath(obj.fromPath, urlSpec.localized);
+      if (fromPath.error) {
+        return {
+          error: `Path (fromPath) validation failed: ${fromPath.error}`,
+        };
+      }
 
-            const toPath = validateWritePath(obj.toPath, urlSpec.localized);
-            if (toPath.error) {
-                return {error: `Path (toPath) validation failed: ${toPath.error}`};
-            }
+      const toPath = validateWritePath(obj.toPath, urlSpec.localized);
+      if (toPath.error) {
+        return { error: `Path (toPath) validation failed: ${toPath.error}` };
+      }
 
-            if (typeof obj.title !== 'function') {
-                return {error: `Expected title to be function, got ${obj.title}`};
-            }
+      if (typeof obj.title !== "function") {
+        return { error: `Expected title to be function, got ${obj.title}` };
+      }
 
-            break;
-        }
+      break;
+    }
 
-        default: {
-            return {error: `Unknown type: ${obj.type}`};
-        }
+    default: {
+      return { error: `Unknown type: ${obj.type}` };
     }
+  }
 
-    return {success: true};
+  return { success: true };
 }
 
 /*
@@ -787,12 +833,10 @@ async function writeData(subKey, directory, data) {
 // touching the original one (which had contained everything).
 const writePage = {};
 
-writePage.to = ({
-    baseDirectory,
-    pageSubKey,
-    paths
-}) => (targetFullKey, ...args) => {
-    const [ groupKey, subKey ] = targetFullKey.split('.');
+writePage.to =
+  ({ baseDirectory, pageSubKey, paths }) =>
+  (targetFullKey, ...args) => {
+    const [groupKey, subKey] = targetFullKey.split(".");
     let path = paths.subdirectoryPrefix;
 
     let from;
@@ -800,33 +844,39 @@ writePage.to = ({
 
     // When linking to *outside* the localized area of the site, we need to
     // make sure the result is correctly relative to the 8ase directory.
-    if (groupKey !== 'localized' && groupKey !== 'localizedDefaultLanguage' && baseDirectory) {
-        from = 'localizedWithBaseDirectory.' + pageSubKey;
-        to = targetFullKey;
-    } else if (groupKey === 'localizedDefaultLanguage' && baseDirectory) {
-        // Special case for specifically linking *from* a page with base
-        // directory *to* a page without! Used for the language switcher and
-        // hopefully nothing else oh god.
-        from = 'localizedWithBaseDirectory.' + pageSubKey;
-        to = 'localized.' + subKey;
-    } else if (groupKey === 'localizedDefaultLanguage') {
-        // Linking to the default, except surprise, we're already IN the default
-        // (no baseDirectory set).
-        from = 'localized.' + pageSubKey;
-        to = 'localized.' + subKey;
+    if (
+      groupKey !== "localized" &&
+      groupKey !== "localizedDefaultLanguage" &&
+      baseDirectory
+    ) {
+      from = "localizedWithBaseDirectory." + pageSubKey;
+      to = targetFullKey;
+    } else if (groupKey === "localizedDefaultLanguage" && baseDirectory) {
+      // Special case for specifically linking *from* a page with base
+      // directory *to* a page without! Used for the language switcher and
+      // hopefully nothing else oh god.
+      from = "localizedWithBaseDirectory." + pageSubKey;
+      to = "localized." + subKey;
+    } else if (groupKey === "localizedDefaultLanguage") {
+      // Linking to the default, except surprise, we're already IN the default
+      // (no baseDirectory set).
+      from = "localized." + pageSubKey;
+      to = "localized." + subKey;
     } else {
-        // If we're linking inside the localized area (or there just is no
-        // 8ase directory), the 8ase directory doesn't matter.
-        from = 'localized.' + pageSubKey;
-        to = targetFullKey;
+      // If we're linking inside the localized area (or there just is no
+      // 8ase directory), the 8ase directory doesn't matter.
+      from = "localized." + pageSubKey;
+      to = targetFullKey;
     }
 
     path += urls.from(from).to(to, ...args);
 
     return path;
-};
+  };
 
-writePage.html = (pageInfo, {
+writePage.html = (
+  pageInfo,
+  {
     defaultLanguage,
     language,
     languages,
@@ -835,486 +885,653 @@ writePage.html = (pageInfo, {
     oEmbedJSONHref,
     to,
     transformMultiline,
-    wikiData
-}) => {
-    const { wikiInfo } = wikiData;
-
-    let {
-        title = '',
-        meta = {},
-        theme = '',
-        stylesheet = '',
-
-        showWikiNameInTitle = true,
-
-        // missing properties are auto-filled, see below!
-        body = {},
-        banner = {},
-        main = {},
-        sidebarLeft = {},
-        sidebarRight = {},
-        nav = {},
-        secondaryNav = {},
-        footer = {},
-        socialEmbed = {},
-    } = pageInfo;
-
-    body.style ??= '';
-
-    theme = theme || getThemeString(wikiInfo.color);
-
-    banner ||= {};
-    banner.classes ??= [];
-    banner.src ??= '';
-    banner.position ??= '';
-    banner.dimensions ??= [0, 0];
-
-    main.classes ??= [];
-    main.content ??= '';
-
-    sidebarLeft ??= {};
-    sidebarRight ??= {};
-
-    for (const sidebar of [sidebarLeft, sidebarRight]) {
-        sidebar.classes ??= [];
-        sidebar.content ??= '';
-        sidebar.collapse ??= true;
-    }
-
-    nav.classes ??= [];
-    nav.content ??= '';
-    nav.bottomRowContent ??= '';
-    nav.links ??= [];
-    nav.linkContainerClasses ??= [];
-
-    secondaryNav ??= {};
-    secondaryNav.content ??= '';
-    secondaryNav.content ??= '';
-
-    footer.classes ??= [];
-    footer.content ??= (wikiInfo.footerContent ? transformMultiline(wikiInfo.footerContent) : '');
-
-    footer.content += '\n' + getFooterLocalizationLinks(paths.pathname, {
-        defaultLanguage, languages, paths, language, to
+    wikiData,
+  }
+) => {
+  const { wikiInfo } = wikiData;
+
+  let {
+    title = "",
+    meta = {},
+    theme = "",
+    stylesheet = "",
+
+    showWikiNameInTitle = true,
+
+    // missing properties are auto-filled, see below!
+    body = {},
+    banner = {},
+    main = {},
+    sidebarLeft = {},
+    sidebarRight = {},
+    nav = {},
+    secondaryNav = {},
+    footer = {},
+    socialEmbed = {},
+  } = pageInfo;
+
+  body.style ??= "";
+
+  theme = theme || getThemeString(wikiInfo.color);
+
+  banner ||= {};
+  banner.classes ??= [];
+  banner.src ??= "";
+  banner.position ??= "";
+  banner.dimensions ??= [0, 0];
+
+  main.classes ??= [];
+  main.content ??= "";
+
+  sidebarLeft ??= {};
+  sidebarRight ??= {};
+
+  for (const sidebar of [sidebarLeft, sidebarRight]) {
+    sidebar.classes ??= [];
+    sidebar.content ??= "";
+    sidebar.collapse ??= true;
+  }
+
+  nav.classes ??= [];
+  nav.content ??= "";
+  nav.bottomRowContent ??= "";
+  nav.links ??= [];
+  nav.linkContainerClasses ??= [];
+
+  secondaryNav ??= {};
+  secondaryNav.content ??= "";
+  secondaryNav.content ??= "";
+
+  footer.classes ??= [];
+  footer.content ??= wikiInfo.footerContent
+    ? transformMultiline(wikiInfo.footerContent)
+    : "";
+
+  footer.content +=
+    "\n" +
+    getFooterLocalizationLinks(paths.pathname, {
+      defaultLanguage,
+      languages,
+      paths,
+      language,
+      to,
     });
 
-    const canonical = (wikiInfo.canonicalBase
-        ? wikiInfo.canonicalBase + (paths.pathname === '/' ? '' : paths.pathname)
-        : '');
-
-    const localizedCanonical = (wikiInfo.canonicalBase
-        ? Object.entries(localizedPaths).map(([ code, { pathname } ]) => ({
-            lang: code,
-            href: wikiInfo.canonicalBase + (pathname === '/' ? '' : pathname)
-        }))
-        : []);
-
-    const collapseSidebars = (sidebarLeft.collapse !== false) && (sidebarRight.collapse !== false);
-
-    const mainHTML = main.content && html.tag('main', {
-        id: 'content',
-        class: main.classes
-    }, main.content);
-
-    const footerHTML = footer.content && html.tag('footer', {
-        id: 'footer',
-        class: footer.classes
-    }, footer.content);
-
-    const generateSidebarHTML = (id, {
-        content,
-        multiple,
-        classes,
-        collapse = true,
-        wide = false
-    }) => (content
-        ? html.tag('div',
-            {id, class: [
-                'sidebar-column',
-                'sidebar',
-                wide && 'wide',
-                !collapse && 'no-hide',
-                ...classes
-            ]},
-            content)
-        : multiple ? html.tag('div',
-            {id, class: [
-                'sidebar-column',
-                'sidebar-multiple',
-                wide && 'wide',
-                !collapse && 'no-hide'
-            ]},
-            multiple.map(content => html.tag('div',
-                {class: ['sidebar', ...classes]},
-                content)))
-        : '');
-
-    const sidebarLeftHTML = generateSidebarHTML('sidebar-left', sidebarLeft);
-    const sidebarRightHTML = generateSidebarHTML('sidebar-right', sidebarRight);
-
-    if (nav.simple) {
-        nav.linkContainerClasses = ['nav-links-hierarchy'];
-        nav.links = [
-            {toHome: true},
-            {toCurrentPage: true}
-        ];
-    }
-
-    const links = (nav.links || []).filter(Boolean);
-
-    const navLinkParts = [];
-    for (let i = 0; i < links.length; i++) {
-        let cur = links[i];
-        const prev = links[i - 1];
-        const next = links[i + 1];
+  const canonical = wikiInfo.canonicalBase
+    ? wikiInfo.canonicalBase + (paths.pathname === "/" ? "" : paths.pathname)
+    : "";
+
+  const localizedCanonical = wikiInfo.canonicalBase
+    ? Object.entries(localizedPaths).map(([code, { pathname }]) => ({
+        lang: code,
+        href: wikiInfo.canonicalBase + (pathname === "/" ? "" : pathname),
+      }))
+    : [];
+
+  const collapseSidebars =
+    sidebarLeft.collapse !== false && sidebarRight.collapse !== false;
+
+  const mainHTML =
+    main.content &&
+    html.tag(
+      "main",
+      {
+        id: "content",
+        class: main.classes,
+      },
+      main.content
+    );
 
-        let { title: linkTitle } = cur;
+  const footerHTML =
+    footer.content &&
+    html.tag(
+      "footer",
+      {
+        id: "footer",
+        class: footer.classes,
+      },
+      footer.content
+    );
 
-        if (cur.toHome) {
-            linkTitle ??= wikiInfo.nameShort;
-        } else if (cur.toCurrentPage) {
-            linkTitle ??= title;
-        }
+  const generateSidebarHTML = (
+    id,
+    { content, multiple, classes, collapse = true, wide = false }
+  ) =>
+    content
+      ? html.tag(
+          "div",
+          {
+            id,
+            class: [
+              "sidebar-column",
+              "sidebar",
+              wide && "wide",
+              !collapse && "no-hide",
+              ...classes,
+            ],
+          },
+          content
+        )
+      : multiple
+      ? html.tag(
+          "div",
+          {
+            id,
+            class: [
+              "sidebar-column",
+              "sidebar-multiple",
+              wide && "wide",
+              !collapse && "no-hide",
+            ],
+          },
+          multiple.map((content) =>
+            html.tag("div", { class: ["sidebar", ...classes] }, content)
+          )
+        )
+      : "";
+
+  const sidebarLeftHTML = generateSidebarHTML("sidebar-left", sidebarLeft);
+  const sidebarRightHTML = generateSidebarHTML("sidebar-right", sidebarRight);
+
+  if (nav.simple) {
+    nav.linkContainerClasses = ["nav-links-hierarchy"];
+    nav.links = [{ toHome: true }, { toCurrentPage: true }];
+  }
+
+  const links = (nav.links || []).filter(Boolean);
+
+  const navLinkParts = [];
+  for (let i = 0; i < links.length; i++) {
+    let cur = links[i];
+    const prev = links[i - 1];
+    const next = links[i + 1];
+
+    let { title: linkTitle } = cur;
+
+    if (cur.toHome) {
+      linkTitle ??= wikiInfo.nameShort;
+    } else if (cur.toCurrentPage) {
+      linkTitle ??= title;
+    }
 
-        let partContent;
+    let partContent;
 
-        if (typeof cur.html === 'string') {
-            if (!cur.html) {
-                logWarn`Empty HTML in nav link ${JSON.stringify(cur)}`;
-                console.trace();
-            }
-            partContent = cur.html;
-        } else {
-            const attributes = {
-                class: (cur.toCurrentPage || i === links.length - 1) && 'current',
-                href: (
-                    cur.toCurrentPage ? '' :
-                    cur.toHome ? to('localized.home') :
-                    cur.path ? to(...cur.path) :
-                    cur.href ? (() => {
-                        logWarn`Using legacy href format nav link in ${paths.pathname}`;
-                        return cur.href;
-                    })() :
-                    null)
-            };
-            if (attributes.href === null) {
-                throw new Error(`Expected some href specifier for link to ${linkTitle} (${JSON.stringify(cur)})`);
-            }
-            partContent = html.tag('a', attributes, linkTitle);
-        }
+    if (typeof cur.html === "string") {
+      if (!cur.html) {
+        logWarn`Empty HTML in nav link ${JSON.stringify(cur)}`;
+        console.trace();
+      }
+      partContent = cur.html;
+    } else {
+      const attributes = {
+        class: (cur.toCurrentPage || i === links.length - 1) && "current",
+        href: cur.toCurrentPage
+          ? ""
+          : cur.toHome
+          ? to("localized.home")
+          : cur.path
+          ? to(...cur.path)
+          : cur.href
+          ? (() => {
+              logWarn`Using legacy href format nav link in ${paths.pathname}`;
+              return cur.href;
+            })()
+          : null,
+      };
+      if (attributes.href === null) {
+        throw new Error(
+          `Expected some href specifier for link to ${linkTitle} (${JSON.stringify(
+            cur
+          )})`
+        );
+      }
+      partContent = html.tag("a", attributes, linkTitle);
+    }
 
-        const part = html.tag('span',
-            {class: cur.divider === false && 'no-divider'},
-            partContent);
+    const part = html.tag(
+      "span",
+      { class: cur.divider === false && "no-divider" },
+      partContent
+    );
 
-        navLinkParts.push(part);
-    }
+    navLinkParts.push(part);
+  }
 
-    const navHTML = html.tag('nav', {
-        [html.onlyIfContent]: true,
-        id: 'header',
-        class: [
-            ...nav.classes,
-            links.length && 'nav-has-main-links',
-            nav.content && 'nav-has-content',
-            nav.bottomRowContent && 'nav-has-bottom-row',
-        ],
-    }, [
-        links.length && html.tag('div',
-            {class: ['nav-main-links', ...nav.linkContainerClasses]},
-            navLinkParts),
-        nav.content && html.tag('div', {class: 'nav-content'}, nav.content),
-        nav.bottomRowContent && html.tag('div', {class: 'nav-bottom-row'}, nav.bottomRowContent),
-    ]);
-
-    const secondaryNavHTML = html.tag('nav', {
-        [html.onlyIfContent]: true,
-        id: 'secondary-nav',
-        class: secondaryNav.classes
-    }, [
-        secondaryNav.content
-    ]);
-
-    const bannerSrc = (
-        banner.src ? banner.src :
-        banner.path ? to(...banner.path) :
-        null);
-
-    const bannerHTML = banner.position && bannerSrc && html.tag('div',
-        {
-            id: 'banner',
-            class: banner.classes
-        },
-        html.tag('img', {
-            src: bannerSrc,
-            alt: banner.alt,
-            width: banner.dimensions[0] || 1100,
-            height: banner.dimensions[1] || 200
-        })
+  const navHTML = html.tag(
+    "nav",
+    {
+      [html.onlyIfContent]: true,
+      id: "header",
+      class: [
+        ...nav.classes,
+        links.length && "nav-has-main-links",
+        nav.content && "nav-has-content",
+        nav.bottomRowContent && "nav-has-bottom-row",
+      ],
+    },
+    [
+      links.length &&
+        html.tag(
+          "div",
+          { class: ["nav-main-links", ...nav.linkContainerClasses] },
+          navLinkParts
+        ),
+      nav.content && html.tag("div", { class: "nav-content" }, nav.content),
+      nav.bottomRowContent &&
+        html.tag("div", { class: "nav-bottom-row" }, nav.bottomRowContent),
+    ]
+  );
+
+  const secondaryNavHTML = html.tag(
+    "nav",
+    {
+      [html.onlyIfContent]: true,
+      id: "secondary-nav",
+      class: secondaryNav.classes,
+    },
+    [secondaryNav.content]
+  );
+
+  const bannerSrc = banner.src
+    ? banner.src
+    : banner.path
+    ? to(...banner.path)
+    : null;
+
+  const bannerHTML =
+    banner.position &&
+    bannerSrc &&
+    html.tag(
+      "div",
+      {
+        id: "banner",
+        class: banner.classes,
+      },
+      html.tag("img", {
+        src: bannerSrc,
+        alt: banner.alt,
+        width: banner.dimensions[0] || 1100,
+        height: banner.dimensions[1] || 200,
+      })
     );
 
-    const layoutHTML = [
-        navHTML,
-        banner.position === 'top' && bannerHTML,
-        secondaryNavHTML,
-        html.tag('div',
-            {class: ['layout-columns', !collapseSidebars && 'vertical-when-thin']},
-            [
-                sidebarLeftHTML,
-                mainHTML,
-                sidebarRightHTML
-            ]),
-        banner.position === 'bottom' && bannerHTML,
-        footerHTML
-    ].filter(Boolean).join('\n');
-
-    const infoCardHTML = fixWS`
+  const layoutHTML = [
+    navHTML,
+    banner.position === "top" && bannerHTML,
+    secondaryNavHTML,
+    html.tag(
+      "div",
+      { class: ["layout-columns", !collapseSidebars && "vertical-when-thin"] },
+      [sidebarLeftHTML, mainHTML, sidebarRightHTML]
+    ),
+    banner.position === "bottom" && bannerHTML,
+    footerHTML,
+  ]
+    .filter(Boolean)
+    .join("\n");
+
+  const infoCardHTML = fixWS`
         <div id="info-card-container">
             <div class="info-card-decor">
                 <div class="info-card">
                     <div class="info-card-art-container no-reveal">
                         ${img({
-                            class: 'info-card-art',
-                            src: '',
-                            link: true,
-                            square: true
+                          class: "info-card-art",
+                          src: "",
+                          link: true,
+                          square: true,
                         })}
                     </div>
                     <div class="info-card-art-container reveal">
                         ${img({
-                            class: 'info-card-art',
-                            src: '',
-                            link: true,
-                            square: true,
-                            reveal: getRevealStringFromWarnings('<span class="info-card-art-warnings"></span>', {language})
+                          class: "info-card-art",
+                          src: "",
+                          link: true,
+                          square: true,
+                          reveal: getRevealStringFromWarnings(
+                            '<span class="info-card-art-warnings"></span>',
+                            { language }
+                          ),
                         })}
                     </div>
                     <h1 class="info-card-name"><a></a></h1>
-                    <p class="info-card-album">${language.$('releaseInfo.from', {album: '<a></a>'})}</p>
-                    <p class="info-card-artists">${language.$('releaseInfo.by', {artists: '<span></span>'})}</p>
-                    <p class="info-card-cover-artists">${language.$('releaseInfo.coverArtBy', {artists: '<span></span>'})}</p>
+                    <p class="info-card-album">${language.$(
+                      "releaseInfo.from",
+                      { album: "<a></a>" }
+                    )}</p>
+                    <p class="info-card-artists">${language.$(
+                      "releaseInfo.by",
+                      { artists: "<span></span>" }
+                    )}</p>
+                    <p class="info-card-cover-artists">${language.$(
+                      "releaseInfo.coverArtBy",
+                      { artists: "<span></span>" }
+                    )}</p>
                 </div>
             </div>
         </div>
     `;
 
-    const socialEmbedHTML = [
-        socialEmbed.title && html.tag('meta', {property: 'og:title', content: socialEmbed.title}),
-        socialEmbed.description && html.tag('meta', {property: 'og:description', content: socialEmbed.description}),
-        socialEmbed.image && html.tag('meta', {property: 'og:image', content: socialEmbed.image}),
-        socialEmbed.color && html.tag('meta', {name: 'theme-color', content: socialEmbed.color}),
-        oEmbedJSONHref && html.tag('link', {type: 'application/json+oembed', href: oEmbedJSONHref}),
-    ].filter(Boolean).join('\n');
-
-    return filterEmptyLines(fixWS`
+  const socialEmbedHTML = [
+    socialEmbed.title &&
+      html.tag("meta", { property: "og:title", content: socialEmbed.title }),
+    socialEmbed.description &&
+      html.tag("meta", {
+        property: "og:description",
+        content: socialEmbed.description,
+      }),
+    socialEmbed.image &&
+      html.tag("meta", { property: "og:image", content: socialEmbed.image }),
+    socialEmbed.color &&
+      html.tag("meta", { name: "theme-color", content: socialEmbed.color }),
+    oEmbedJSONHref &&
+      html.tag("link", {
+        type: "application/json+oembed",
+        href: oEmbedJSONHref,
+      }),
+  ]
+    .filter(Boolean)
+    .join("\n");
+
+  return filterEmptyLines(fixWS`
         <!DOCTYPE html>
         <html ${html.attributes({
-            lang: language.intlCode,
-            'data-language-code': language.code,
-            'data-url-key': paths.toPath[0],
-            ...Object.fromEntries(paths.toPath.slice(1).map((v, i) => [['data-url-value' + i], v])),
-            'data-rebase-localized': to('localized.root'),
-            'data-rebase-shared': to('shared.root'),
-            'data-rebase-media': to('media.root'),
-            'data-rebase-data': to('data.root')
+          lang: language.intlCode,
+          "data-language-code": language.code,
+          "data-url-key": paths.toPath[0],
+          ...Object.fromEntries(
+            paths.toPath.slice(1).map((v, i) => [["data-url-value" + i], v])
+          ),
+          "data-rebase-localized": to("localized.root"),
+          "data-rebase-shared": to("shared.root"),
+          "data-rebase-media": to("media.root"),
+          "data-rebase-data": to("data.root"),
         })}>
             <head>
-                <title>${(showWikiNameInTitle
-                    ? language.formatString('misc.pageTitle.withWikiName', {
+                <title>${
+                  showWikiNameInTitle
+                    ? language.formatString("misc.pageTitle.withWikiName", {
                         title,
-                        wikiName: wikiInfo.nameShort
-                    })
-                    : language.formatString('misc.pageTitle', {title}))}</title>
+                        wikiName: wikiInfo.nameShort,
+                      })
+                    : language.formatString("misc.pageTitle", { title })
+                }</title>
                 <meta charset="utf-8">
                 <meta name="viewport" content="width=device-width, initial-scale=1">
-                ${Object.entries(meta).filter(([ key, value ]) => value).map(([ key, value ]) => `<meta ${key}="${html.escapeAttributeValue(value)}">`).join('\n')}
+                ${Object.entries(meta)
+                  .filter(([key, value]) => value)
+                  .map(
+                    ([key, value]) =>
+                      `<meta ${key}="${html.escapeAttributeValue(value)}">`
+                  )
+                  .join("\n")}
                 ${canonical && `<link rel="canonical" href="${canonical}">`}
-                ${localizedCanonical.map(({ lang, href }) => `<link rel="alternate" hreflang="${lang}" href="${href}">`).join('\n')}
+                ${localizedCanonical
+                  .map(
+                    ({ lang, href }) =>
+                      `<link rel="alternate" hreflang="${lang}" href="${href}">`
+                  )
+                  .join("\n")}
                 ${socialEmbedHTML}
-                <link rel="stylesheet" href="${to('shared.staticFile', `site.css?${CACHEBUST}`)}">
-                ${(theme || stylesheet) && fixWS`
+                <link rel="stylesheet" href="${to(
+                  "shared.staticFile",
+                  `site.css?${CACHEBUST}`
+                )}">
+                ${
+                  (theme || stylesheet) &&
+                  fixWS`
                     <style>
                         ${theme}
                         ${stylesheet}
                     </style>
-                `}
-                <script src="${to('shared.staticFile', `lazy-loading.js?${CACHEBUST}`)}"></script>
+                `
+                }
+                <script src="${to(
+                  "shared.staticFile",
+                  `lazy-loading.js?${CACHEBUST}`
+                )}"></script>
             </head>
-            <body ${html.attributes({style: body.style || ''})}>
+            <body ${html.attributes({ style: body.style || "" })}>
                 <div id="page-container">
-                    ${mainHTML && fixWS`
+                    ${
+                      mainHTML &&
+                      fixWS`
                         <div id="skippers">
                             ${[
-                                ['#content', language.$('misc.skippers.skipToContent')],
-                                sidebarLeftHTML && ['#sidebar-left', (sidebarRightHTML
-                                    ? language.$('misc.skippers.skipToSidebar.left')
-                                    : language.$('misc.skippers.skipToSidebar'))],
-                                sidebarRightHTML && ['#sidebar-right', (sidebarLeftHTML
-                                    ? language.$('misc.skippers.skipToSidebar.right')
-                                    : language.$('misc.skippers.skipToSidebar'))],
-                                footerHTML && ['#footer', language.$('misc.skippers.skipToFooter')]
-                            ].filter(Boolean).map(([ href, title ]) => fixWS`
+                              [
+                                "#content",
+                                language.$("misc.skippers.skipToContent"),
+                              ],
+                              sidebarLeftHTML && [
+                                "#sidebar-left",
+                                sidebarRightHTML
+                                  ? language.$(
+                                      "misc.skippers.skipToSidebar.left"
+                                    )
+                                  : language.$("misc.skippers.skipToSidebar"),
+                              ],
+                              sidebarRightHTML && [
+                                "#sidebar-right",
+                                sidebarLeftHTML
+                                  ? language.$(
+                                      "misc.skippers.skipToSidebar.right"
+                                    )
+                                  : language.$("misc.skippers.skipToSidebar"),
+                              ],
+                              footerHTML && [
+                                "#footer",
+                                language.$("misc.skippers.skipToFooter"),
+                              ],
+                            ]
+                              .filter(Boolean)
+                              .map(
+                                ([href, title]) => fixWS`
                                 <span class="skipper"><a href="${href}">${title}</a></span>
-                            `).join('\n')}
+                            `
+                              )
+                              .join("\n")}
                         </div>
-                    `}
+                    `
+                    }
                     ${layoutHTML}
                 </div>
                 ${infoCardHTML}
-                <script type="module" src="${to('shared.staticFile', `client.js?${CACHEBUST}`)}"></script>
+                <script type="module" src="${to(
+                  "shared.staticFile",
+                  `client.js?${CACHEBUST}`
+                )}"></script>
             </body>
         </html>
     `);
 };
 
-writePage.oEmbedJSON = (pageInfo, {
-    language,
-    wikiData,
-}) => {
-    const { socialEmbed } = pageInfo;
-    const { wikiInfo } = wikiData;
-    const { canonicalBase, nameShort } = wikiInfo;
-
-    if (!socialEmbed) return '';
-
-    const entries = [
-        socialEmbed.heading && ['author_name',
-            language.$('misc.socialEmbed.heading', {
-                wikiName: nameShort,
-                heading: socialEmbed.heading
-            })],
-        socialEmbed.headingLink && canonicalBase && ['author_url',
-            canonicalBase.replace(/\/$/, '') + '/' +
-            socialEmbed.headingLink.replace(/^\//, '')],
-    ].filter(Boolean);
-
-    if (!entries.length) return '';
-
-    return JSON.stringify(Object.fromEntries(entries));
+writePage.oEmbedJSON = (pageInfo, { language, wikiData }) => {
+  const { socialEmbed } = pageInfo;
+  const { wikiInfo } = wikiData;
+  const { canonicalBase, nameShort } = wikiInfo;
+
+  if (!socialEmbed) return "";
+
+  const entries = [
+    socialEmbed.heading && [
+      "author_name",
+      language.$("misc.socialEmbed.heading", {
+        wikiName: nameShort,
+        heading: socialEmbed.heading,
+      }),
+    ],
+    socialEmbed.headingLink &&
+      canonicalBase && [
+        "author_url",
+        canonicalBase.replace(/\/$/, "") +
+          "/" +
+          socialEmbed.headingLink.replace(/^\//, ""),
+      ],
+  ].filter(Boolean);
+
+  if (!entries.length) return "";
+
+  return JSON.stringify(Object.fromEntries(entries));
 };
 
-writePage.write = async ({
-    html,
-    oEmbedJSON = '',
-    paths,
-}) => {
-    await mkdir(paths.outputDirectory, {recursive: true});
-    await Promise.all([
-        writeFile(paths.outputFile, html),
-        oEmbedJSON && writeFile(paths.oEmbedJSONFile, oEmbedJSON)
-    ].filter(Boolean));
+writePage.write = async ({ html, oEmbedJSON = "", paths }) => {
+  await mkdir(paths.outputDirectory, { recursive: true });
+  await Promise.all(
+    [
+      writeFile(paths.outputFile, html),
+      oEmbedJSON && writeFile(paths.oEmbedJSONFile, oEmbedJSON),
+    ].filter(Boolean)
+  );
 };
 
 // TODO: This only supports one <>-style argument.
-writePage.paths = (baseDirectory, fullKey, directory = '', {
-    file = 'index.html'
-} = {}) => {
-    const [ groupKey, subKey ] = fullKey.split('.');
-
-    const pathname = (groupKey === 'localized' && baseDirectory
-        ? urls.from('shared.root').toDevice('localizedWithBaseDirectory.' + subKey, baseDirectory, directory)
-        : urls.from('shared.root').toDevice(fullKey, directory));
-
-    // Needed for the rare directory which itself contains a slash, e.g. for
-    // listings, with directories like 'albums/by-name'.
-    const subdirectoryPrefix = '../'.repeat(directory.split('/').length - 1);
-
-    const outputDirectory = path.join(outputPath, pathname);
-    const outputFile = path.join(outputDirectory, file);
-    const oEmbedJSONFile = path.join(outputDirectory, OEMBED_JSON_FILE);
-
-    return {
-        toPath: [fullKey, directory],
-        pathname,
-        subdirectoryPrefix,
-        outputDirectory, outputFile,
-        oEmbedJSONFile,
-    };
+writePage.paths = (
+  baseDirectory,
+  fullKey,
+  directory = "",
+  { file = "index.html" } = {}
+) => {
+  const [groupKey, subKey] = fullKey.split(".");
+
+  const pathname =
+    groupKey === "localized" && baseDirectory
+      ? urls
+          .from("shared.root")
+          .toDevice(
+            "localizedWithBaseDirectory." + subKey,
+            baseDirectory,
+            directory
+          )
+      : urls.from("shared.root").toDevice(fullKey, directory);
+
+  // Needed for the rare directory which itself contains a slash, e.g. for
+  // listings, with directories like 'albums/by-name'.
+  const subdirectoryPrefix = "../".repeat(directory.split("/").length - 1);
+
+  const outputDirectory = path.join(outputPath, pathname);
+  const outputFile = path.join(outputDirectory, file);
+  const oEmbedJSONFile = path.join(outputDirectory, OEMBED_JSON_FILE);
+
+  return {
+    toPath: [fullKey, directory],
+    pathname,
+    subdirectoryPrefix,
+    outputDirectory,
+    outputFile,
+    oEmbedJSONFile,
+  };
 };
 
 async function writeFavicon() {
+  try {
+    await stat(path.join(mediaPath, FAVICON_FILE));
+  } catch (error) {
+    return;
+  }
+
+  try {
+    await copyFile(
+      path.join(mediaPath, FAVICON_FILE),
+      path.join(outputPath, FAVICON_FILE)
+    );
+  } catch (error) {
+    logWarn`Failed to copy favicon! ${error.message}`;
+    return;
+  }
+
+  logInfo`Copied favicon to site root.`;
+}
+
+function writeSymlinks() {
+  return progressPromiseAll("Writing site symlinks.", [
+    link(path.join(__dirname, UTILITY_DIRECTORY), "shared.utilityRoot"),
+    link(path.join(__dirname, STATIC_DIRECTORY), "shared.staticRoot"),
+    link(mediaPath, "media.root"),
+  ]);
+
+  async function link(directory, urlKey) {
+    const pathname = urls.from("shared.root").toDevice(urlKey);
+    const file = path.join(outputPath, pathname);
     try {
-        await stat(path.join(mediaPath, FAVICON_FILE));
+      await unlink(file);
     } catch (error) {
-        return;
+      if (error.code !== "ENOENT") {
+        throw error;
+      }
     }
-
     try {
-        await copyFile(
-            path.join(mediaPath, FAVICON_FILE),
-            path.join(outputPath, FAVICON_FILE)
-        );
+      await symlink(path.resolve(directory), file);
     } catch (error) {
-        logWarn`Failed to copy favicon! ${error.message}`;
-        return;
+      if (error.code === "EPERM") {
+        await symlink(path.resolve(directory), file, "junction");
+      }
     }
-
-    logInfo`Copied favicon to site root.`;
+  }
 }
 
-function writeSymlinks() {
-    return progressPromiseAll('Writing site symlinks.', [
-        link(path.join(__dirname, UTILITY_DIRECTORY), 'shared.utilityRoot'),
-        link(path.join(__dirname, STATIC_DIRECTORY), 'shared.staticRoot'),
-        link(mediaPath, 'media.root')
-    ]);
-
-    async function link(directory, urlKey) {
-        const pathname = urls.from('shared.root').toDevice(urlKey);
-        const file = path.join(outputPath, pathname);
-        try {
-            await unlink(file);
-        } catch (error) {
-            if (error.code !== 'ENOENT') {
-                throw error;
-            }
-        }
-        try {
-            await symlink(path.resolve(directory), file);
-        } catch (error) {
-            if (error.code === 'EPERM') {
-                await symlink(path.resolve(directory), file, 'junction');
-            }
-        }
-    }
-}
-
-function writeSharedFilesAndPages({language, wikiData}) {
-    const { groupData, wikiInfo } = wikiData;
-
-    const redirect = async (title, from, urlKey, directory) => {
-        const target = path.relative(from, urls.from('shared.root').to(urlKey, directory));
-        const content = generateRedirectPage(title, target, {language});
-        await mkdir(path.join(outputPath, from), {recursive: true});
-        await writeFile(path.join(outputPath, from, 'index.html'), content);
-    };
-
-    return progressPromiseAll(`Writing files & pages shared across languages.`, [
-        groupData?.some(group => group.directory === 'fandom') &&
-        redirect('Fandom - Gallery', 'albums/fandom', 'localized.groupGallery', 'fandom'),
-
-        groupData?.some(group => group.directory === 'official') &&
-        redirect('Official - Gallery', 'albums/official', 'localized.groupGallery', 'official'),
+function writeSharedFilesAndPages({ language, wikiData }) {
+  const { groupData, wikiInfo } = wikiData;
 
-        wikiInfo.enableListings &&
-        redirect('Album Commentary', 'list/all-commentary', 'localized.commentaryIndex', ''),
-
-        writeFile(path.join(outputPath, 'data.json'), fixWS`
+  const redirect = async (title, from, urlKey, directory) => {
+    const target = path.relative(
+      from,
+      urls.from("shared.root").to(urlKey, directory)
+    );
+    const content = generateRedirectPage(title, target, { language });
+    await mkdir(path.join(outputPath, from), { recursive: true });
+    await writeFile(path.join(outputPath, from, "index.html"), content);
+  };
+
+  return progressPromiseAll(
+    `Writing files & pages shared across languages.`,
+    [
+      groupData?.some((group) => group.directory === "fandom") &&
+        redirect(
+          "Fandom - Gallery",
+          "albums/fandom",
+          "localized.groupGallery",
+          "fandom"
+        ),
+
+      groupData?.some((group) => group.directory === "official") &&
+        redirect(
+          "Official - Gallery",
+          "albums/official",
+          "localized.groupGallery",
+          "official"
+        ),
+
+      wikiInfo.enableListings &&
+        redirect(
+          "Album Commentary",
+          "list/all-commentary",
+          "localized.commentaryIndex",
+          ""
+        ),
+
+      writeFile(
+        path.join(outputPath, "data.json"),
+        fixWS`
             {
                 "albumData": ${stringifyThings(wikiData.albumData)},
-                ${wikiInfo.enableFlashesAndGames && `"flashData": ${stringifyThings(wikiData.flashData)},`}
+                ${
+                  wikiInfo.enableFlashesAndGames &&
+                  `"flashData": ${stringifyThings(wikiData.flashData)},`
+                }
                 "artistData": ${stringifyThings(wikiData.artistData)}
             }
-        `)
-    ].filter(Boolean));
+        `
+      ),
+    ].filter(Boolean)
+  );
 }
 
-function generateRedirectPage(title, target, {language}) {
-    return fixWS`
+function generateRedirectPage(title, target, { language }) {
+  return fixWS`
         <!DOCTYPE html>
         <html>
             <head>
-                <title>${language.$('redirectPage.title', {title})}</title>
+                <title>${language.$("redirectPage.title", { title })}</title>
                 <meta charset="utf-8">
                 <meta http-equiv="refresh" content="0;url=${target}">
                 <link rel="canonical" href="${target}">
@@ -1322,9 +1539,9 @@ function generateRedirectPage(title, target, {language}) {
             </head>
             <body>
                 <main>
-                    <h1>${language.$('redirectPage.title', {title})}</h1>
-                    <p>${language.$('redirectPage.infoLine', {
-                        target: `<a href="${target}">${target}</a>`
+                    <h1>${language.$("redirectPage.title", { title })}</h1>
+                    <p>${language.$("redirectPage.infoLine", {
+                      target: `<a href="${target}">${target}</a>`,
                     })}</p>
                 </main>
             </body>
@@ -1334,622 +1551,663 @@ function generateRedirectPage(title, target, {language}) {
 
 // RIP toAnythingMan (previously getHrefOfAnythingMan), 2020-05-25<>2021-05-14.
 // ........Yet the function 8reathes life anew as linkAnythingMan! ::::)
-function linkAnythingMan(anythingMan, {link, wikiData, ...opts}) {
-    return (
-        wikiData.albumData.includes(anythingMan) ? link.album(anythingMan, opts) :
-        wikiData.trackData.includes(anythingMan) ? link.track(anythingMan, opts) :
-        wikiData.flashData?.includes(anythingMan) ? link.flash(anythingMan, opts) :
-        'idk bud'
-    )
+function linkAnythingMan(anythingMan, { link, wikiData, ...opts }) {
+  return wikiData.albumData.includes(anythingMan)
+    ? link.album(anythingMan, opts)
+    : wikiData.trackData.includes(anythingMan)
+    ? link.track(anythingMan, opts)
+    : wikiData.flashData?.includes(anythingMan)
+    ? link.flash(anythingMan, opts)
+    : "idk bud";
 }
 
 async function processLanguageFile(file) {
-    const contents = await readFile(file, 'utf-8');
-    const json = JSON.parse(contents);
-
-    const code = json['meta.languageCode'];
-    if (!code) {
-        throw new Error(`Missing language code (file: ${file})`);
-    }
-    delete json['meta.languageCode'];
-
-    const intlCode = json['meta.languageIntlCode'] ?? null;
-    delete json['meta.languageIntlCode'];
-
-    const name = json['meta.languageName'];
-    if (!name) {
-        throw new Error(`Missing language name (${code})`);
-    }
-    delete json['meta.languageName'];
-
-    const hidden = json['meta.hidden'] ?? false;
-    delete json['meta.hidden'];
-
-    if (json['meta.baseDirectory']) {
-        logWarn`(${code}) Language JSON still has unused meta.baseDirectory`;
-        delete json['meta.baseDirectory'];
-    }
-
-    const language = new Language();
-    language.code = code;
-    language.intlCode = intlCode;
-    language.name = name;
-    language.hidden = hidden;
-    language.escapeHTML = string => he.encode(string, {useNamedReferences: true});
-    language.strings = json;
-    return language;
+  const contents = await readFile(file, "utf-8");
+  const json = JSON.parse(contents);
+
+  const code = json["meta.languageCode"];
+  if (!code) {
+    throw new Error(`Missing language code (file: ${file})`);
+  }
+  delete json["meta.languageCode"];
+
+  const intlCode = json["meta.languageIntlCode"] ?? null;
+  delete json["meta.languageIntlCode"];
+
+  const name = json["meta.languageName"];
+  if (!name) {
+    throw new Error(`Missing language name (${code})`);
+  }
+  delete json["meta.languageName"];
+
+  const hidden = json["meta.hidden"] ?? false;
+  delete json["meta.hidden"];
+
+  if (json["meta.baseDirectory"]) {
+    logWarn`(${code}) Language JSON still has unused meta.baseDirectory`;
+    delete json["meta.baseDirectory"];
+  }
+
+  const language = new Language();
+  language.code = code;
+  language.intlCode = intlCode;
+  language.name = name;
+  language.hidden = hidden;
+  language.escapeHTML = (string) =>
+    he.encode(string, { useNamedReferences: true });
+  language.strings = json;
+  return language;
 }
 
 // Wrapper function for running a function once for all languages.
-async function wrapLanguages(fn, {languages, writeOneLanguage = null}) {
-    const k = writeOneLanguage;
-    const languagesToRun = (k
-        ? {[k]: languages[k]}
-        : languages);
+async function wrapLanguages(fn, { languages, writeOneLanguage = null }) {
+  const k = writeOneLanguage;
+  const languagesToRun = k ? { [k]: languages[k] } : languages;
 
-    const entries = Object.entries(languagesToRun)
-        .filter(([ key ]) => key !== 'default');
+  const entries = Object.entries(languagesToRun).filter(
+    ([key]) => key !== "default"
+  );
 
-    for (let i = 0; i < entries.length; i++) {
-        const [ key, language ] = entries[i];
+  for (let i = 0; i < entries.length; i++) {
+    const [key, language] = entries[i];
 
-        await fn(language, i, entries);
-    }
+    await fn(language, i, entries);
+  }
 }
 
 async function main() {
-    Error.stackTraceLimit = Infinity;
+  Error.stackTraceLimit = Infinity;
 
-    const WD = wikiData;
+  const WD = wikiData;
 
-    WD.listingSpec = listingSpec;
-    WD.listingTargetSpec = listingTargetSpec;
-
-    const miscOptions = await parseOptions(process.argv.slice(2), {
-        // Data files for the site, including flash, artist, and al8um data,
-        // and like a jillion other things too. Pretty much everything which
-        // makes an individual wiki what it is goes here!
-        'data-path': {
-            type: 'value'
-        },
+  WD.listingSpec = listingSpec;
+  WD.listingTargetSpec = listingTargetSpec;
 
-        // Static media will 8e referenced in the site here! The contents are
-        // categorized; check out MEDIA_ALBUM_ART_DIRECTORY and other constants
-        // near the top of this file (upd8.js).
-        'media-path': {
-            type: 'value'
-        },
-
-        // String files! For the most part, this is used for translating the
-        // site to different languages, though you can also customize strings
-        // for your own 8uild of the site if you'd like. Files here should all
-        // match the format in strings-default.json in this repository. (If a
-        // language file is missing any strings, the site code will fall 8ack
-        // to what's specified in strings-default.json.)
-        //
-        // Unlike the other options here, this one's optional - the site will
-        // 8uild with the default (English) strings if this path is left
-        // unspecified.
-        'lang-path': {
-            type: 'value'
-        },
+  const miscOptions = await parseOptions(process.argv.slice(2), {
+    // Data files for the site, including flash, artist, and al8um data,
+    // and like a jillion other things too. Pretty much everything which
+    // makes an individual wiki what it is goes here!
+    "data-path": {
+      type: "value",
+    },
 
-        // This is the output directory. It's the one you'll upload online with
-        // rsync or whatever when you're pushing an upd8, and also the one
-        // you'd archive if you wanted to make a 8ackup of the whole dang
-        // site. Just keep in mind that the gener8ted result will contain a
-        // couple symlinked directories, so if you're uploading, you're pro8a8ly
-        // gonna want to resolve those yourself.
-        'out-path': {
-            type: 'value'
-        },
+    // Static media will 8e referenced in the site here! The contents are
+    // categorized; check out MEDIA_ALBUM_ART_DIRECTORY and other constants
+    // near the top of this file (upd8.js).
+    "media-path": {
+      type: "value",
+    },
 
-        // Thum8nail gener8tion is *usually* something you want, 8ut it can 8e
-        // kinda a pain to run every time, since it does necessit8te reading
-        // every media file at run time. Pass this to skip it.
-        'skip-thumbs': {
-            type: 'flag'
-        },
+    // String files! For the most part, this is used for translating the
+    // site to different languages, though you can also customize strings
+    // for your own 8uild of the site if you'd like. Files here should all
+    // match the format in strings-default.json in this repository. (If a
+    // language file is missing any strings, the site code will fall 8ack
+    // to what's specified in strings-default.json.)
+    //
+    // Unlike the other options here, this one's optional - the site will
+    // 8uild with the default (English) strings if this path is left
+    // unspecified.
+    "lang-path": {
+      type: "value",
+    },
 
-        // Or, if you *only* want to gener8te newly upd8ted thum8nails, you can
-        // pass this flag! It exits 8efore 8uilding the rest of the site.
-        'thumbs-only': {
-            type: 'flag'
-        },
+    // This is the output directory. It's the one you'll upload online with
+    // rsync or whatever when you're pushing an upd8, and also the one
+    // you'd archive if you wanted to make a 8ackup of the whole dang
+    // site. Just keep in mind that the gener8ted result will contain a
+    // couple symlinked directories, so if you're uploading, you're pro8a8ly
+    // gonna want to resolve those yourself.
+    "out-path": {
+      type: "value",
+    },
 
-        // Just working on data entries and not interested in actually
-        // generating site HTML yet? This flag will cut execution off right
-        // 8efore any site 8uilding actually happens.
-        'no-build': {
-            type: 'flag'
-        },
+    // Thum8nail gener8tion is *usually* something you want, 8ut it can 8e
+    // kinda a pain to run every time, since it does necessit8te reading
+    // every media file at run time. Pass this to skip it.
+    "skip-thumbs": {
+      type: "flag",
+    },
 
-        // Only want to 8uild one language during testing? This can chop down
-        // 8uild times a pretty 8ig chunk! Just pass a single language code.
-        'lang': {
-            type: 'value'
-        },
+    // Or, if you *only* want to gener8te newly upd8ted thum8nails, you can
+    // pass this flag! It exits 8efore 8uilding the rest of the site.
+    "thumbs-only": {
+      type: "flag",
+    },
 
-        // Working without a dev server and just using file:// URLs in your we8
-        // 8rowser? This will automatically append index.html to links across
-        // the site. Not recommended for production, since it isn't guaranteed
-        // 100% error-free (and index.html-style links are less pretty anyway).
-        'append-index-html': {
-            type: 'flag'
-        },
+    // Just working on data entries and not interested in actually
+    // generating site HTML yet? This flag will cut execution off right
+    // 8efore any site 8uilding actually happens.
+    "no-build": {
+      type: "flag",
+    },
 
-        // Want sweet, sweet trace8ack info in aggreg8te error messages? This
-        // will print all the juicy details (or at least the first relevant
-        // line) right to your output, 8ut also pro8a8ly give you a headache
-        // 8ecause wow that is a lot of visual noise.
-        'show-traces': {
-            type: 'flag'
-        },
+    // Only want to 8uild one language during testing? This can chop down
+    // 8uild times a pretty 8ig chunk! Just pass a single language code.
+    lang: {
+      type: "value",
+    },
 
-        'queue-size': {
-            type: 'value',
-            validate(size) {
-                if (parseInt(size) !== parseFloat(size)) return 'an integer';
-                if (parseInt(size) < 0) return 'a counting number or zero';
-                return true;
-            }
-        },
-        queue: {alias: 'queue-size'},
+    // Working without a dev server and just using file:// URLs in your we8
+    // 8rowser? This will automatically append index.html to links across
+    // the site. Not recommended for production, since it isn't guaranteed
+    // 100% error-free (and index.html-style links are less pretty anyway).
+    "append-index-html": {
+      type: "flag",
+    },
 
-        // This option is super slow and has the potential for bugs! It puts
-        // CacheableObject in a mode where every instance is a Proxy which will
-        // keep track of invalid property accesses.
-        'show-invalid-property-accesses': {
-            type: 'flag'
-        },
+    // Want sweet, sweet trace8ack info in aggreg8te error messages? This
+    // will print all the juicy details (or at least the first relevant
+    // line) right to your output, 8ut also pro8a8ly give you a headache
+    // 8ecause wow that is a lot of visual noise.
+    "show-traces": {
+      type: "flag",
+    },
 
-        [parseOptions.handleUnknown]: () => {}
-    });
+    "queue-size": {
+      type: "value",
+      validate(size) {
+        if (parseInt(size) !== parseFloat(size)) return "an integer";
+        if (parseInt(size) < 0) return "a counting number or zero";
+        return true;
+      },
+    },
+    queue: { alias: "queue-size" },
 
-    dataPath = miscOptions['data-path'] || process.env.HSMUSIC_DATA;
-    mediaPath = miscOptions['media-path'] || process.env.HSMUSIC_MEDIA;
-    langPath = miscOptions['lang-path'] || process.env.HSMUSIC_LANG; // Can 8e left unset!
-    outputPath = miscOptions['out-path'] || process.env.HSMUSIC_OUT;
+    // This option is super slow and has the potential for bugs! It puts
+    // CacheableObject in a mode where every instance is a Proxy which will
+    // keep track of invalid property accesses.
+    "show-invalid-property-accesses": {
+      type: "flag",
+    },
 
-    const writeOneLanguage = miscOptions['lang'];
+    [parseOptions.handleUnknown]: () => {},
+  });
 
-    {
-        let errored = false;
-        const error = (cond, msg) => {
-            if (cond) {
-                console.error(`\x1b[31;1m${msg}\x1b[0m`);
-                errored = true;
-            }
-        };
-        error(!dataPath,   `Expected --data-path option or HSMUSIC_DATA to be set`);
-        error(!mediaPath,  `Expected --media-path option or HSMUSIC_MEDIA to be set`);
-        error(!outputPath, `Expected --out-path option or HSMUSIC_OUT to be set`);
-        if (errored) {
-            return;
-        }
-    }
+  dataPath = miscOptions["data-path"] || process.env.HSMUSIC_DATA;
+  mediaPath = miscOptions["media-path"] || process.env.HSMUSIC_MEDIA;
+  langPath = miscOptions["lang-path"] || process.env.HSMUSIC_LANG; // Can 8e left unset!
+  outputPath = miscOptions["out-path"] || process.env.HSMUSIC_OUT;
 
-    const appendIndexHTML = miscOptions['append-index-html'] ?? false;
-    if (appendIndexHTML) {
-        logWarn`Appending index.html to link hrefs. (Note: not recommended for production release!)`;
-        unbound_link.globalOptions.appendIndexHTML = true;
-    }
+  const writeOneLanguage = miscOptions["lang"];
 
-    const skipThumbs = miscOptions['skip-thumbs'] ?? false;
-    const thumbsOnly = miscOptions['thumbs-only'] ?? false;
-    const noBuild = miscOptions['no-build'] ?? false;
-    const showAggregateTraces = miscOptions['show-traces'] ?? false;
-
-    const niceShowAggregate = (error, ...opts) => {
-        showAggregate(error, {
-            showTraces: showAggregateTraces,
-            pathToFile: f => path.relative(__dirname, f),
-            ...opts
-        });
+  {
+    let errored = false;
+    const error = (cond, msg) => {
+      if (cond) {
+        console.error(`\x1b[31;1m${msg}\x1b[0m`);
+        errored = true;
+      }
     };
-
-    if (skipThumbs && thumbsOnly) {
-        logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`;
-        return;
+    error(!dataPath, `Expected --data-path option or HSMUSIC_DATA to be set`);
+    error(
+      !mediaPath,
+      `Expected --media-path option or HSMUSIC_MEDIA to be set`
+    );
+    error(!outputPath, `Expected --out-path option or HSMUSIC_OUT to be set`);
+    if (errored) {
+      return;
     }
-
-    if (skipThumbs) {
-        logInfo`Skipping thumbnail generation.`;
-    } else {
-        logInfo`Begin thumbnail generation... -----+`;
-        const result = await genThumbs(mediaPath, {queueSize, quiet: true});
-        logInfo`Done thumbnail generation! --------+`;
-        if (!result) return;
-        if (thumbsOnly) return;
+  }
+
+  const appendIndexHTML = miscOptions["append-index-html"] ?? false;
+  if (appendIndexHTML) {
+    logWarn`Appending index.html to link hrefs. (Note: not recommended for production release!)`;
+    unbound_link.globalOptions.appendIndexHTML = true;
+  }
+
+  const skipThumbs = miscOptions["skip-thumbs"] ?? false;
+  const thumbsOnly = miscOptions["thumbs-only"] ?? false;
+  const noBuild = miscOptions["no-build"] ?? false;
+  const showAggregateTraces = miscOptions["show-traces"] ?? false;
+
+  const niceShowAggregate = (error, ...opts) => {
+    showAggregate(error, {
+      showTraces: showAggregateTraces,
+      pathToFile: (f) => path.relative(__dirname, f),
+      ...opts,
+    });
+  };
+
+  if (skipThumbs && thumbsOnly) {
+    logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`;
+    return;
+  }
+
+  if (skipThumbs) {
+    logInfo`Skipping thumbnail generation.`;
+  } else {
+    logInfo`Begin thumbnail generation... -----+`;
+    const result = await genThumbs(mediaPath, { queueSize, quiet: true });
+    logInfo`Done thumbnail generation! --------+`;
+    if (!result) return;
+    if (thumbsOnly) return;
+  }
+
+  const showInvalidPropertyAccesses =
+    miscOptions["show-invalid-property-accesses"] ?? false;
+
+  if (showInvalidPropertyAccesses) {
+    CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true;
+  }
+
+  const { aggregate: processDataAggregate, result: wikiDataResult } =
+    await loadAndProcessDataDocuments({ dataPath });
+
+  Object.assign(wikiData, wikiDataResult);
+
+  {
+    const logThings = (thingDataProp, label) =>
+      logInfo` - ${
+        wikiData[thingDataProp]?.length ?? color.red("(Missing!)")
+      } ${color.normal(color.dim(label))}`;
+    try {
+      logInfo`Loaded data and processed objects:`;
+      logThings("albumData", "albums");
+      logThings("trackData", "tracks");
+      logThings("artistData", "artists");
+      if (wikiData.flashData) {
+        logThings("flashData", "flashes");
+        logThings("flashActData", "flash acts");
+      }
+      logThings("groupData", "groups");
+      logThings("groupCategoryData", "group categories");
+      logThings("artTagData", "art tags");
+      if (wikiData.newsData) {
+        logThings("newsData", "news entries");
+      }
+      logThings("staticPageData", "static pages");
+      if (wikiData.homepageLayout) {
+        logInfo` - ${1} homepage layout (${
+          wikiData.homepageLayout.rows.length
+        } rows)`;
+      }
+      if (wikiData.wikiInfo) {
+        logInfo` - ${1} wiki config file`;
+      }
+    } catch (error) {
+      console.error(`Error showing data summary:`, error);
     }
 
-    const showInvalidPropertyAccesses = miscOptions['show-invalid-property-accesses'] ?? false;
+    let errorless = true;
+    try {
+      processDataAggregate.close();
+    } catch (error) {
+      niceShowAggregate(error);
+      logWarn`The above errors were detected while processing data files.`;
+      logWarn`If the remaining valid data is complete enough, the wiki will`;
+      logWarn`still build - but all errored data will be skipped.`;
+      logWarn`(Resolve errors for more complete output!)`;
+      errorless = false;
+    }
 
-    if (showInvalidPropertyAccesses) {
-        CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true;
+    if (errorless) {
+      logInfo`All data processed without any errors - nice!`;
+      logInfo`(This means all source files will be fully accounted for during page generation.)`;
     }
+  }
 
-    const {
-        aggregate: processDataAggregate,
-        result: wikiDataResult
-    } = await loadAndProcessDataDocuments({dataPath});
+  if (!WD.wikiInfo) {
+    logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`;
+    return;
+  }
 
-    Object.assign(wikiData, wikiDataResult);
+  let duplicateDirectoriesErrored = false;
 
-    {
-        const logThings = (thingDataProp, label) => logInfo` - ${wikiData[thingDataProp]?.length ?? color.red('(Missing!)')} ${color.normal(color.dim(label))}`;
-        try {
-            logInfo`Loaded data and processed objects:`;
-            logThings('albumData', 'albums');
-            logThings('trackData', 'tracks');
-            logThings('artistData', 'artists');
-            if (wikiData.flashData) {
-                logThings('flashData', 'flashes');
-                logThings('flashActData', 'flash acts');
-            }
-            logThings('groupData', 'groups');
-            logThings('groupCategoryData', 'group categories');
-            logThings('artTagData', 'art tags');
-            if (wikiData.newsData) {
-                logThings('newsData', 'news entries');
-            }
-            logThings('staticPageData', 'static pages');
-            if (wikiData.homepageLayout) {
-                logInfo` - ${1} homepage layout (${wikiData.homepageLayout.rows.length} rows)`;
-            }
-            if (wikiData.wikiInfo) {
-                logInfo` - ${1} wiki config file`;
-            }
-        } catch (error) {
-            console.error(`Error showing data summary:`, error);
-        }
-
-        let errorless = true;
-        try {
-            processDataAggregate.close();
-        } catch (error) {
-            niceShowAggregate(error);
-            logWarn`The above errors were detected while processing data files.`;
-            logWarn`If the remaining valid data is complete enough, the wiki will`;
-            logWarn`still build - but all errored data will be skipped.`;
-            logWarn`(Resolve errors for more complete output!)`;
-            errorless = false;
-        }
-
-        if (errorless) {
-            logInfo`All data processed without any errors - nice!`;
-            logInfo`(This means all source files will be fully accounted for during page generation.)`;
-        }
+  function filterAndShowDuplicateDirectories() {
+    const aggregate = filterDuplicateDirectories(wikiData);
+    let errorless = true;
+    try {
+      aggregate.close();
+    } catch (aggregate) {
+      niceShowAggregate(aggregate);
+      logWarn`The above duplicate directories were detected while reviewing data files.`;
+      logWarn`Each thing listed above will been totally excempt from this build of the site!`;
+      logWarn`Specify unique 'Directory' fields in data entries to resolve these.`;
+      logWarn`${`Note:`} This will probably result in reference errors below.`;
+      logWarn`${`. . .`} You should fix duplicate directories first!`;
+      logWarn`(Resolve errors for more complete output!)`;
+      duplicateDirectoriesErrored = true;
+      errorless = false;
     }
-
-    if (!WD.wikiInfo) {
-        logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`;
-        return;
+    if (errorless) {
+      logInfo`No duplicate directories found - nice!`;
     }
+  }
 
-    let duplicateDirectoriesErrored = false;
-
-    function filterAndShowDuplicateDirectories() {
-        const aggregate = filterDuplicateDirectories(wikiData);
-        let errorless = true;
-        try {
-            aggregate.close();
-        } catch (aggregate) {
-            niceShowAggregate(aggregate);
-            logWarn`The above duplicate directories were detected while reviewing data files.`;
-            logWarn`Each thing listed above will been totally excempt from this build of the site!`;
-            logWarn`Specify unique 'Directory' fields in data entries to resolve these.`;
-            logWarn`${`Note:`} This will probably result in reference errors below.`;
-            logWarn`${`. . .`} You should fix duplicate directories first!`;
-            logWarn`(Resolve errors for more complete output!)`;
-            duplicateDirectoriesErrored = true;
-            errorless = false;
-        }
-        if (errorless) {
-            logInfo`No duplicate directories found - nice!`;
-        }
+  function filterAndShowReferenceErrors() {
+    const aggregate = filterReferenceErrors(wikiData);
+    let errorless = true;
+    try {
+      aggregate.close();
+    } catch (error) {
+      niceShowAggregate(error);
+      logWarn`The above errors were detected while validating references in data files.`;
+      logWarn`If the remaining valid data is complete enough, the wiki will still build -`;
+      logWarn`but all errored references will be skipped.`;
+      if (duplicateDirectoriesErrored) {
+        logWarn`${`Note:`} Duplicate directories were found as well. Review those first,`;
+        logWarn`${`. . .`} as they may have caused some of the errors detected above.`;
+      }
+      logWarn`(Resolve errors for more complete output!)`;
+      errorless = false;
     }
-
-    function filterAndShowReferenceErrors() {
-        const aggregate = filterReferenceErrors(wikiData);
-        let errorless = true;
-        try {
-            aggregate.close();
-        } catch (error) {
-            niceShowAggregate(error);
-            logWarn`The above errors were detected while validating references in data files.`;
-            logWarn`If the remaining valid data is complete enough, the wiki will still build -`;
-            logWarn`but all errored references will be skipped.`;
-            if (duplicateDirectoriesErrored) {
-                logWarn`${`Note:`} Duplicate directories were found as well. Review those first,`;
-                logWarn`${`. . .`} as they may have caused some of the errors detected above.`;
-            }
-            logWarn`(Resolve errors for more complete output!)`;
-            errorless = false;
-        }
-        if (errorless) {
-            logInfo`All references validated without any errors - nice!`;
-            logInfo`(This means all references between things, such as leitmotif references`
-            logInfo` and artist credits, will be fully accounted for during page generation.)`;
-        }
+    if (errorless) {
+      logInfo`All references validated without any errors - nice!`;
+      logInfo`(This means all references between things, such as leitmotif references`;
+      logInfo` and artist credits, will be fully accounted for during page generation.)`;
     }
+  }
 
-    // Link data arrays so that all essential references between objects are
-    // complete, so properties (like dates!) are inherited where that's
-    // appropriate.
-    linkWikiDataArrays(wikiData);
+  // Link data arrays so that all essential references between objects are
+  // complete, so properties (like dates!) are inherited where that's
+  // appropriate.
+  linkWikiDataArrays(wikiData);
 
-    // Filter out any things with duplicate directories throughout the data,
-    // warning about them too.
-    filterAndShowDuplicateDirectories();
+  // Filter out any things with duplicate directories throughout the data,
+  // warning about them too.
+  filterAndShowDuplicateDirectories();
 
-    // Filter out any reference errors throughout the data, warning about them
-    // too.
-    filterAndShowReferenceErrors();
+  // Filter out any reference errors throughout the data, warning about them
+  // too.
+  filterAndShowReferenceErrors();
 
-    // Sort data arrays so that they're all in order! This may use properties
-    // which are only available after the initial linking.
-    sortWikiDataArrays(wikiData);
+  // Sort data arrays so that they're all in order! This may use properties
+  // which are only available after the initial linking.
+  sortWikiDataArrays(wikiData);
 
-    const internalDefaultLanguage = await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE));
+  const internalDefaultLanguage = await processLanguageFile(
+    path.join(__dirname, DEFAULT_STRINGS_FILE)
+  );
 
-    let languages;
-    if (langPath) {
-        const languageDataFiles = await findFiles(langPath, {
-            filter: f => path.extname(f) === '.json'
-        });
+  let languages;
+  if (langPath) {
+    const languageDataFiles = await findFiles(langPath, {
+      filter: (f) => path.extname(f) === ".json",
+    });
 
-        const results = await progressPromiseAll(`Reading & processing language files.`, languageDataFiles
-            .map(file => processLanguageFile(file)));
+    const results = await progressPromiseAll(
+      `Reading & processing language files.`,
+      languageDataFiles.map((file) => processLanguageFile(file))
+    );
 
-        languages = Object.fromEntries(results.map(language => [language.code, language]));
+    languages = Object.fromEntries(
+      results.map((language) => [language.code, language])
+    );
+  } else {
+    languages = {};
+  }
+
+  const customDefaultLanguage =
+    languages[WD.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code];
+  let finalDefaultLanguage;
+
+  if (customDefaultLanguage) {
+    logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`;
+    customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings;
+    finalDefaultLanguage = customDefaultLanguage;
+  } else if (WD.wikiInfo.defaultLanguage) {
+    logError`Wiki info file specified default language is ${WD.wikiInfo.defaultLanguage}, but no such language file exists!`;
+    if (langPath) {
+      logError`Check if an appropriate file exists in ${langPath}?`;
     } else {
-        languages = {};
+      logError`Be sure to specify ${"--lang"} or ${"HSMUSIC_LANG"} with the path to language files.`;
     }
-
-    const customDefaultLanguage = languages[WD.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code];
-    let finalDefaultLanguage;
-
-    if (customDefaultLanguage) {
-        logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`;
-        customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings;
-        finalDefaultLanguage = customDefaultLanguage;
-    } else if (WD.wikiInfo.defaultLanguage) {
-        logError`Wiki info file specified default language is ${WD.wikiInfo.defaultLanguage}, but no such language file exists!`;
-        if (langPath) {
-            logError`Check if an appropriate file exists in ${langPath}?`;
-        } else {
-            logError`Be sure to specify ${'--lang'} or ${'HSMUSIC_LANG'} with the path to language files.`;
-        }
-        return;
-    } else {
-        languages[internalDefaultLanguage.code] = internalDefaultLanguage;
-        finalDefaultLanguage = internalDefaultLanguage;
+    return;
+  } else {
+    languages[internalDefaultLanguage.code] = internalDefaultLanguage;
+    finalDefaultLanguage = internalDefaultLanguage;
+  }
+
+  for (const language of Object.values(languages)) {
+    if (language === finalDefaultLanguage) {
+      continue;
     }
 
-    for (const language of Object.values(languages)) {
-        if (language === finalDefaultLanguage) {
-            continue;
-        }
+    language.inheritedStrings = finalDefaultLanguage.strings;
+  }
+
+  logInfo`Loaded language strings: ${Object.keys(languages).join(", ")}`;
+
+  if (noBuild) {
+    logInfo`Not generating any site or page files this run (--no-build passed).`;
+  } else if (writeOneLanguage && !(writeOneLanguage in languages)) {
+    logError`Specified to write only ${writeOneLanguage}, but there is no strings file with this language code!`;
+    return;
+  } else if (writeOneLanguage) {
+    logInfo`Writing only language ${writeOneLanguage} this run.`;
+  } else {
+    logInfo`Writing all languages.`;
+  }
+
+  {
+    const tagRefs = new Set(
+      [...WD.trackData, ...WD.albumData].flatMap(
+        (thing) => thing.artTagsByRef ?? []
+      )
+    );
 
-        language.inheritedStrings = finalDefaultLanguage.strings;
+    for (const ref of tagRefs) {
+      if (find.artTag(ref, WD.artTagData)) {
+        tagRefs.delete(ref);
+      }
     }
 
-    logInfo`Loaded language strings: ${Object.keys(languages).join(', ')}`;
-
-    if (noBuild) {
-        logInfo`Not generating any site or page files this run (--no-build passed).`;
-    } else if (writeOneLanguage && !(writeOneLanguage in languages)) {
-        logError`Specified to write only ${writeOneLanguage}, but there is no strings file with this language code!`;
-        return;
-    } else if (writeOneLanguage) {
-        logInfo`Writing only language ${writeOneLanguage} this run.`;
-    } else {
-        logInfo`Writing all languages.`;
+    if (tagRefs.size) {
+      for (const ref of Array.from(tagRefs).sort()) {
+        console.log(`\x1b[33;1m- Missing tag: "${ref}"\x1b[0m`);
+      }
+      return;
     }
-
-    {
-        const tagRefs = new Set([...WD.trackData, ...WD.albumData].flatMap(thing => thing.artTagsByRef ?? []));
-
-        for (const ref of tagRefs) {
-            if (find.artTag(ref, WD.artTagData)) {
-                tagRefs.delete(ref);
-            }
+  }
+
+  WD.officialAlbumData = WD.albumData.filter((album) =>
+    album.groups.some((group) => group.directory === OFFICIAL_GROUP_DIRECTORY)
+  );
+  WD.fandomAlbumData = WD.albumData.filter((album) =>
+    album.groups.every((group) => group.directory !== OFFICIAL_GROUP_DIRECTORY)
+  );
+
+  const fileSizePreloader = new FileSizePreloader();
+
+  // File sizes of additional files need to be precalculated before we can
+  // actually reference 'em in site building, so get those loading right
+  // away. We actually need to keep track of two things here - the on-device
+  // file paths we're actually reading, and the corresponding on-site media
+  // paths that will be exposed in site build code. We'll build a mapping
+  // function between them so that when site code requests a site path,
+  // it'll get the size of the file at the corresponding device path.
+  const additionalFilePaths = [
+    ...WD.albumData.flatMap((album) =>
+      [
+        ...(album.additionalFiles ?? []),
+        ...album.tracks.flatMap((track) => track.additionalFiles ?? []),
+      ]
+        .flatMap((fileGroup) => fileGroup.files)
+        .map((file) => ({
+          device: path.join(
+            mediaPath,
+            urls
+              .from("media.root")
+              .toDevice("media.albumAdditionalFile", album.directory, file)
+          ),
+          media: urls
+            .from("media.root")
+            .to("media.albumAdditionalFile", album.directory, file),
+        }))
+    ),
+  ];
+
+  const getSizeOfAdditionalFile = (mediaPath) => {
+    const { device = null } =
+      additionalFilePaths.find(({ media }) => media === mediaPath) || {};
+    if (!device) return null;
+    return fileSizePreloader.getSizeOfPath(device);
+  };
+
+  logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`;
+
+  fileSizePreloader.loadPaths(
+    ...additionalFilePaths.map((path) => path.device)
+  );
+  await fileSizePreloader.waitUntilDoneLoading();
+
+  logInfo`Done preloading filesizes!`;
+
+  if (noBuild) return;
+
+  // Makes writing a little nicer on CPU theoretically, 8ut also costs in
+  // performance right now 'cuz it'll w8 for file writes to 8e completed
+  // 8efore moving on to more data processing. So, defaults to zero, which
+  // disa8les the queue feature altogether.
+  queueSize = +(miscOptions["queue-size"] ?? 0);
+
+  const buildDictionary = pageSpecs;
+
+  // NOT for ena8ling or disa8ling specific features of the site!
+  // This is only in charge of what general groups of files to 8uild.
+  // They're here to make development quicker when you're only working
+  // on some particular area(s) of the site rather than making changes
+  // across all of them.
+  const writeFlags = await parseOptions(process.argv.slice(2), {
+    all: { type: "flag" }, // Defaults to true if none 8elow specified.
+
+    // Kinda a hack t8h!
+    ...Object.fromEntries(
+      Object.keys(buildDictionary).map((key) => [key, { type: "flag" }])
+    ),
+
+    [parseOptions.handleUnknown]: () => {},
+  });
+
+  const writeAll = !Object.keys(writeFlags).length || writeFlags.all;
+
+  logInfo`Writing site pages: ${
+    writeAll ? "all" : Object.keys(writeFlags).join(", ")
+  }`;
+
+  await writeFavicon();
+  await writeSymlinks();
+  await writeSharedFilesAndPages({ language: finalDefaultLanguage, wikiData });
+
+  const buildSteps = writeAll
+    ? Object.entries(buildDictionary)
+    : Object.entries(buildDictionary).filter(([flag]) => writeFlags[flag]);
+
+  let writes;
+  {
+    let error = false;
+
+    const buildStepsWithTargets = buildSteps
+      .map(([flag, pageSpec]) => {
+        // Condition not met: skip this build step altogether.
+        if (pageSpec.condition && !pageSpec.condition({ wikiData })) {
+          return null;
         }
 
-        if (tagRefs.size) {
-            for (const ref of Array.from(tagRefs).sort()) {
-                console.log(`\x1b[33;1m- Missing tag: "${ref}"\x1b[0m`);
-            }
-            return;
+        // May still call writeTargetless if present.
+        if (!pageSpec.targets) {
+          return { flag, pageSpec, targets: [] };
         }
-    }
-
-    WD.officialAlbumData = WD.albumData.filter(album => album.groups.some(group => group.directory === OFFICIAL_GROUP_DIRECTORY));
-    WD.fandomAlbumData = WD.albumData.filter(album => album.groups.every(group => group.directory !== OFFICIAL_GROUP_DIRECTORY));
-
-    const fileSizePreloader = new FileSizePreloader();
-
-    // File sizes of additional files need to be precalculated before we can
-    // actually reference 'em in site building, so get those loading right
-    // away. We actually need to keep track of two things here - the on-device
-    // file paths we're actually reading, and the corresponding on-site media
-    // paths that will be exposed in site build code. We'll build a mapping
-    // function between them so that when site code requests a site path,
-    // it'll get the size of the file at the corresponding device path.
-    const additionalFilePaths = [
-        ...WD.albumData.flatMap(album => (
-            [
-                ...album.additionalFiles ?? [],
-                ...album.tracks.flatMap(track => track.additionalFiles ?? [])
-            ]
-            .flatMap(fileGroup => fileGroup.files)
-            .map(file => ({
-                device: (path.join(mediaPath, urls
-                    .from('media.root')
-                    .toDevice('media.albumAdditionalFile', album.directory, file))),
-                media: (urls
-                    .from('media.root')
-                    .to('media.albumAdditionalFile', album.directory, file))
-            })))),
-    ];
-
-    const getSizeOfAdditionalFile = mediaPath => {
-        const { device = null } = additionalFilePaths.find(({ media }) => media === mediaPath) || {};
-        if (!device) return null;
-        return fileSizePreloader.getSizeOfPath(device);
-    };
-
-    logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`;
-
-    fileSizePreloader.loadPaths(...additionalFilePaths.map(path => path.device));
-    await fileSizePreloader.waitUntilDoneLoading();
-
-    logInfo`Done preloading filesizes!`;
-
-    if (noBuild) return;
-
-    // Makes writing a little nicer on CPU theoretically, 8ut also costs in
-    // performance right now 'cuz it'll w8 for file writes to 8e completed
-    // 8efore moving on to more data processing. So, defaults to zero, which
-    // disa8les the queue feature altogether.
-    queueSize = +(miscOptions['queue-size'] ?? 0);
-
-    const buildDictionary = pageSpecs;
-
-    // NOT for ena8ling or disa8ling specific features of the site!
-    // This is only in charge of what general groups of files to 8uild.
-    // They're here to make development quicker when you're only working
-    // on some particular area(s) of the site rather than making changes
-    // across all of them.
-    const writeFlags = await parseOptions(process.argv.slice(2), {
-        all: {type: 'flag'}, // Defaults to true if none 8elow specified.
-
-        // Kinda a hack t8h!
-        ...Object.fromEntries(Object.keys(buildDictionary)
-            .map(key => [key, {type: 'flag'}])),
-
-        [parseOptions.handleUnknown]: () => {}
-    });
-
-    const writeAll = !Object.keys(writeFlags).length || writeFlags.all;
-
-    logInfo`Writing site pages: ${writeAll ? 'all' : Object.keys(writeFlags).join(', ')}`;
-
-    await writeFavicon();
-    await writeSymlinks();
-    await writeSharedFilesAndPages({language: finalDefaultLanguage, wikiData});
-
-    const buildSteps = (writeAll
-        ? Object.entries(buildDictionary)
-        : (Object.entries(buildDictionary)
-            .filter(([ flag ]) => writeFlags[flag])));
-
-    let writes;
-    {
-        let error = false;
 
-        const buildStepsWithTargets = buildSteps.map(([ flag, pageSpec ]) => {
-            // Condition not met: skip this build step altogether.
-            if (pageSpec.condition && !pageSpec.condition({wikiData})) {
-                return null;
-            }
-
-            // May still call writeTargetless if present.
-            if (!pageSpec.targets) {
-                return {flag, pageSpec, targets: []};
-            }
-
-            if (!pageSpec.write) {
-                logError`${flag + '.targets'} is specified, but ${flag + '.write'} is missing!`;
-                error = true;
-                return null;
-            }
-
-            const targets = pageSpec.targets({wikiData});
-            if (!Array.isArray(targets)) {
-                logError`${flag + '.targets'} was called, but it didn't return an array! (${typeof targets})`;
-                error = true;
-                return null;
-            }
-
-            return {flag, pageSpec, targets};
-        }).filter(Boolean);
+        if (!pageSpec.write) {
+          logError`${flag + ".targets"} is specified, but ${
+            flag + ".write"
+          } is missing!`;
+          error = true;
+          return null;
+        }
 
-        if (error) {
-            return;
+        const targets = pageSpec.targets({ wikiData });
+        if (!Array.isArray(targets)) {
+          logError`${
+            flag + ".targets"
+          } was called, but it didn't return an array! (${typeof targets})`;
+          error = true;
+          return null;
         }
 
-        const validateWrites = (writes, fnName) => {
-            // Do a quick valid8tion! If one of the writeThingPages functions go
-            // wrong, this will stall out early and tell us which did.
+        return { flag, pageSpec, targets };
+      })
+      .filter(Boolean);
 
-            if (!Array.isArray(writes)) {
-                logError`${fnName} didn't return an array!`;
-                error = true;
-                return false;
-            }
+    if (error) {
+      return;
+    }
 
-            if (!(
-                writes.every(obj => typeof obj === 'object') &&
-                writes.every(obj => {
-                    const result = validateWriteObject(obj);
-                    if (result.error) {
-                        logError`Validating write object failed: ${result.error}`;
-                        return false;
-                    } else {
-                        return true;
-                    }
-                })
-            )) {
-                logError`${fnName} returned invalid entries!`;
-                error = true;
-                return false;
+    const validateWrites = (writes, fnName) => {
+      // Do a quick valid8tion! If one of the writeThingPages functions go
+      // wrong, this will stall out early and tell us which did.
+
+      if (!Array.isArray(writes)) {
+        logError`${fnName} didn't return an array!`;
+        error = true;
+        return false;
+      }
+
+      if (
+        !(
+          writes.every((obj) => typeof obj === "object") &&
+          writes.every((obj) => {
+            const result = validateWriteObject(obj);
+            if (result.error) {
+              logError`Validating write object failed: ${result.error}`;
+              return false;
+            } else {
+              return true;
             }
+          })
+        )
+      ) {
+        logError`${fnName} returned invalid entries!`;
+        error = true;
+        return false;
+      }
+
+      return true;
+    };
 
-            return true;
-        };
-
-        // return;
+    // return;
 
-        writes = buildStepsWithTargets.flatMap(({ flag, pageSpec, targets }) => {
-            const writes = targets.flatMap(target =>
-                pageSpec.write(target, {wikiData})?.slice() || []);
+    writes = buildStepsWithTargets.flatMap(({ flag, pageSpec, targets }) => {
+      const writes = targets.flatMap(
+        (target) => pageSpec.write(target, { wikiData })?.slice() || []
+      );
 
-            if (!validateWrites(writes, flag + '.write')) {
-                return [];
-            }
+      if (!validateWrites(writes, flag + ".write")) {
+        return [];
+      }
 
-            if (pageSpec.writeTargetless) {
-                const writes2 = pageSpec.writeTargetless({wikiData});
+      if (pageSpec.writeTargetless) {
+        const writes2 = pageSpec.writeTargetless({ wikiData });
 
-                if (!validateWrites(writes2, flag + '.writeTargetless')) {
-                    return [];
-                }
+        if (!validateWrites(writes2, flag + ".writeTargetless")) {
+          return [];
+        }
 
-                writes.push(...writes2);
-            }
+        writes.push(...writes2);
+      }
 
-            return writes;
-        });
+      return writes;
+    });
 
-        if (error) {
-            return;
-        }
+    if (error) {
+      return;
     }
+  }
 
-    const pageWrites = writes.filter(({ type }) => type === 'page');
-    const dataWrites = writes.filter(({ type }) => type === 'data');
-    const redirectWrites = writes.filter(({ type }) => type === 'redirect');
+  const pageWrites = writes.filter(({ type }) => type === "page");
+  const dataWrites = writes.filter(({ type }) => type === "data");
+  const redirectWrites = writes.filter(({ type }) => type === "redirect");
 
-    if (writes.length) {
-        logInfo`Total of ${writes.length} writes returned. (${pageWrites.length} page, ${dataWrites.length} data [currently skipped], ${redirectWrites.length} redirect)`;
-    } else {
-        logWarn`No writes returned at all, so exiting early. This is probably a bug!`;
-        return;
-    }
+  if (writes.length) {
+    logInfo`Total of ${writes.length} writes returned. (${pageWrites.length} page, ${dataWrites.length} data [currently skipped], ${redirectWrites.length} redirect)`;
+  } else {
+    logWarn`No writes returned at all, so exiting early. This is probably a bug!`;
+    return;
+  }
 
-    /*
+  /*
     await progressPromiseAll(`Writing data files shared across languages.`, queue(
         dataWrites.map(({path, data}) => () => {
             const bound = {};
@@ -1985,272 +2243,331 @@ async function main() {
     ));
     */
 
-    const perLanguageFn = async (language, i, entries) => {
-        const baseDirectory = (language === finalDefaultLanguage ? '' : language.code);
+  const perLanguageFn = async (language, i, entries) => {
+    const baseDirectory =
+      language === finalDefaultLanguage ? "" : language.code;
 
-        console.log(`\x1b[34;1m${
-            (`[${i + 1}/${entries.length}] ${language.code} (-> /${baseDirectory}) `
-                .padEnd(60, '-'))
-        }\x1b[0m`);
+    console.log(
+      `\x1b[34;1m${`[${i + 1}/${entries.length}] ${
+        language.code
+      } (-> /${baseDirectory}) `.padEnd(60, "-")}\x1b[0m`
+    );
 
-        await progressPromiseAll(`Writing ${language.code}`, queue([
-            ...pageWrites.map(({type, ...props}) => () => {
-                const { path, page } = props;
+    await progressPromiseAll(
+      `Writing ${language.code}`,
+      queue(
+        [
+          ...pageWrites.map(({ type, ...props }) => () => {
+            const { path, page } = props;
 
-                // TODO: This only supports one <>-style argument.
-                const pageSubKey = path[0];
-                const directory = path[1];
-
-                const localizedPaths = Object.fromEntries(Object.entries(languages)
-                    .filter(([ key, language ]) => key !== 'default' && !language.hidden)
-                    .map(([ key, language ]) => [language.code, writePage.paths(
-                        (language === finalDefaultLanguage ? '' : language.code),
-                        'localized.' + pageSubKey,
-                        directory
-                    )]));
-
-                const paths = writePage.paths(
-                    baseDirectory,
-                    'localized.' + pageSubKey,
+            // TODO: This only supports one <>-style argument.
+            const pageSubKey = path[0];
+            const directory = path[1];
+
+            const localizedPaths = Object.fromEntries(
+              Object.entries(languages)
+                .filter(
+                  ([key, language]) => key !== "default" && !language.hidden
+                )
+                .map(([key, language]) => [
+                  language.code,
+                  writePage.paths(
+                    language === finalDefaultLanguage ? "" : language.code,
+                    "localized." + pageSubKey,
                     directory
-                );
-
-                const to = writePage.to({
-                    baseDirectory,
-                    pageSubKey,
-                    paths
-                });
-
-                const absoluteTo = (targetFullKey, ...args) => {
-                    const [ groupKey, subKey ] = targetFullKey.split('.');
-                    const from = urls.from('shared.root');
-                    return '/' + (groupKey === 'localized' && baseDirectory
-                        ? from.to('localizedWithBaseDirectory.' + subKey, baseDirectory, ...args)
-                        : from.to(targetFullKey, ...args));
-                };
-
-                // TODO: Is there some nicer way to define these,
-                // may8e without totally re-8inding everything for
-                // each page?
-                const bound = {};
-
-                bound.link = withEntries(unbound_link, entries => entries
-                    .map(([ key, fn ]) => [key, bindOpts(fn, {to})]));
-
-                bound.linkAnythingMan = bindOpts(linkAnythingMan, {
-                    link: bound.link,
-                    wikiData
-                });
+                  ),
+                ])
+            );
+
+            const paths = writePage.paths(
+              baseDirectory,
+              "localized." + pageSubKey,
+              directory
+            );
+
+            const to = writePage.to({
+              baseDirectory,
+              pageSubKey,
+              paths,
+            });
 
-                bound.parseAttributes = bindOpts(parseAttributes, {
-                    to
-                });
+            const absoluteTo = (targetFullKey, ...args) => {
+              const [groupKey, subKey] = targetFullKey.split(".");
+              const from = urls.from("shared.root");
+              return (
+                "/" +
+                (groupKey === "localized" && baseDirectory
+                  ? from.to(
+                      "localizedWithBaseDirectory." + subKey,
+                      baseDirectory,
+                      ...args
+                    )
+                  : from.to(targetFullKey, ...args))
+              );
+            };
 
-                bound.find = bindFind(wikiData, {mode: 'warn'});
+            // TODO: Is there some nicer way to define these,
+            // may8e without totally re-8inding everything for
+            // each page?
+            const bound = {};
 
-                bound.transformInline = bindOpts(transformInline, {
-                    find: bound.find,
-                    link: bound.link,
-                    replacerSpec,
-                    language,
-                    to,
-                    wikiData
-                });
+            bound.link = withEntries(unbound_link, (entries) =>
+              entries.map(([key, fn]) => [key, bindOpts(fn, { to })])
+            );
 
-                bound.transformMultiline = bindOpts(transformMultiline, {
-                    transformInline: bound.transformInline,
-                    parseAttributes: bound.parseAttributes
-                });
-
-                bound.transformLyrics = bindOpts(transformLyrics, {
-                    transformInline: bound.transformInline,
-                    transformMultiline: bound.transformMultiline
-                });
+            bound.linkAnythingMan = bindOpts(linkAnythingMan, {
+              link: bound.link,
+              wikiData,
+            });
 
-                bound.iconifyURL = bindOpts(iconifyURL, {
-                    language,
-                    to
-                });
+            bound.parseAttributes = bindOpts(parseAttributes, {
+              to,
+            });
 
-                bound.fancifyURL = bindOpts(fancifyURL, {
-                    language
-                });
+            bound.find = bindFind(wikiData, { mode: "warn" });
 
-                bound.fancifyFlashURL = bindOpts(fancifyFlashURL, {
-                    [bindOpts.bindIndex]: 2,
-                    language
-                });
+            bound.transformInline = bindOpts(transformInline, {
+              find: bound.find,
+              link: bound.link,
+              replacerSpec,
+              language,
+              to,
+              wikiData,
+            });
 
-                bound.getLinkThemeString = getLinkThemeString;
+            bound.transformMultiline = bindOpts(transformMultiline, {
+              transformInline: bound.transformInline,
+              parseAttributes: bound.parseAttributes,
+            });
 
-                bound.getThemeString = getThemeString;
+            bound.transformLyrics = bindOpts(transformLyrics, {
+              transformInline: bound.transformInline,
+              transformMultiline: bound.transformMultiline,
+            });
 
-                bound.getArtistString = bindOpts(getArtistString, {
-                    iconifyURL: bound.iconifyURL,
-                    link: bound.link,
-                    language
-                });
+            bound.iconifyURL = bindOpts(iconifyURL, {
+              language,
+              to,
+            });
 
-                bound.getAlbumCover = bindOpts(getAlbumCover, {
-                    to
-                });
+            bound.fancifyURL = bindOpts(fancifyURL, {
+              language,
+            });
 
-                bound.getTrackCover = bindOpts(getTrackCover, {
-                    to
-                });
+            bound.fancifyFlashURL = bindOpts(fancifyFlashURL, {
+              [bindOpts.bindIndex]: 2,
+              language,
+            });
 
-                bound.getFlashCover = bindOpts(getFlashCover, {
-                    to
-                });
+            bound.getLinkThemeString = getLinkThemeString;
 
-                bound.getArtistAvatar = bindOpts(getArtistAvatar, {
-                    to
-                });
+            bound.getThemeString = getThemeString;
 
-                bound.generateAdditionalFilesShortcut = bindOpts(generateAdditionalFilesShortcut, {
-                    language
-                });
+            bound.getArtistString = bindOpts(getArtistString, {
+              iconifyURL: bound.iconifyURL,
+              link: bound.link,
+              language,
+            });
 
-                bound.generateAdditionalFilesList = bindOpts(generateAdditionalFilesList, {
-                    language
-                });
+            bound.getAlbumCover = bindOpts(getAlbumCover, {
+              to,
+            });
 
-                bound.generateChronologyLinks = bindOpts(generateChronologyLinks, {
-                    link: bound.link,
-                    linkAnythingMan: bound.linkAnythingMan,
-                    language,
-                    wikiData
-                });
+            bound.getTrackCover = bindOpts(getTrackCover, {
+              to,
+            });
 
-                bound.generateCoverLink = bindOpts(generateCoverLink, {
-                    [bindOpts.bindIndex]: 0,
-                    img,
-                    link: bound.link,
-                    language,
-                    to,
-                    wikiData
-                });
+            bound.getFlashCover = bindOpts(getFlashCover, {
+              to,
+            });
 
-                bound.generateInfoGalleryLinks = bindOpts(generateInfoGalleryLinks, {
-                    [bindOpts.bindIndex]: 2,
-                    link: bound.link,
-                    language
-                });
+            bound.getArtistAvatar = bindOpts(getArtistAvatar, {
+              to,
+            });
 
-                bound.generatePreviousNextLinks = bindOpts(generatePreviousNextLinks, {
-                    link: bound.link,
-                    language
-                });
+            bound.generateAdditionalFilesShortcut = bindOpts(
+              generateAdditionalFilesShortcut,
+              {
+                language,
+              }
+            );
+
+            bound.generateAdditionalFilesList = bindOpts(
+              generateAdditionalFilesList,
+              {
+                language,
+              }
+            );
+
+            bound.generateChronologyLinks = bindOpts(generateChronologyLinks, {
+              link: bound.link,
+              linkAnythingMan: bound.linkAnythingMan,
+              language,
+              wikiData,
+            });
 
-                bound.generateTrackListDividedByGroups = bindOpts(generateTrackListDividedByGroups, {
-                    language,
-                    wikiData,
-                });
+            bound.generateCoverLink = bindOpts(generateCoverLink, {
+              [bindOpts.bindIndex]: 0,
+              img,
+              link: bound.link,
+              language,
+              to,
+              wikiData,
+            });
 
-                bound.getGridHTML = bindOpts(getGridHTML, {
-                    [bindOpts.bindIndex]: 0,
-                    img,
-                    language
-                });
+            bound.generateInfoGalleryLinks = bindOpts(
+              generateInfoGalleryLinks,
+              {
+                [bindOpts.bindIndex]: 2,
+                link: bound.link,
+                language,
+              }
+            );
+
+            bound.generatePreviousNextLinks = bindOpts(
+              generatePreviousNextLinks,
+              {
+                link: bound.link,
+                language,
+              }
+            );
+
+            bound.generateTrackListDividedByGroups = bindOpts(
+              generateTrackListDividedByGroups,
+              {
+                language,
+                wikiData,
+              }
+            );
+
+            bound.getGridHTML = bindOpts(getGridHTML, {
+              [bindOpts.bindIndex]: 0,
+              img,
+              language,
+            });
 
-                bound.getAlbumGridHTML = bindOpts(getAlbumGridHTML, {
-                    [bindOpts.bindIndex]: 0,
-                    getAlbumCover: bound.getAlbumCover,
-                    getGridHTML: bound.getGridHTML,
-                    link: bound.link,
-                    language
-                });
+            bound.getAlbumGridHTML = bindOpts(getAlbumGridHTML, {
+              [bindOpts.bindIndex]: 0,
+              getAlbumCover: bound.getAlbumCover,
+              getGridHTML: bound.getGridHTML,
+              link: bound.link,
+              language,
+            });
 
-                bound.getFlashGridHTML = bindOpts(getFlashGridHTML, {
-                    [bindOpts.bindIndex]: 0,
-                    getFlashCover: bound.getFlashCover,
-                    getGridHTML: bound.getGridHTML,
-                    link: bound.link
-                });
+            bound.getFlashGridHTML = bindOpts(getFlashGridHTML, {
+              [bindOpts.bindIndex]: 0,
+              getFlashCover: bound.getFlashCover,
+              getGridHTML: bound.getGridHTML,
+              link: bound.link,
+            });
 
-                bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, {
-                    language
-                });
+            bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, {
+              language,
+            });
 
-                bound.getRevealStringFromWarnings = bindOpts(getRevealStringFromWarnings, {
-                    language
-                });
+            bound.getRevealStringFromWarnings = bindOpts(
+              getRevealStringFromWarnings,
+              {
+                language,
+              }
+            );
 
-                bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, {
-                    to
-                });
+            bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, {
+              to,
+            });
 
-                const pageInfo = page({
-                    ...bound,
+            const pageInfo = page({
+              ...bound,
 
-                    language,
+              language,
 
-                    absoluteTo,
-                    relativeTo: to,
-                    to,
-                    urls,
+              absoluteTo,
+              relativeTo: to,
+              to,
+              urls,
 
-                    getSizeOfAdditionalFile,
-                });
+              getSizeOfAdditionalFile,
+            });
 
-                const oEmbedJSON = writePage.oEmbedJSON(pageInfo, {
-                    language,
-                    wikiData,
-                });
+            const oEmbedJSON = writePage.oEmbedJSON(pageInfo, {
+              language,
+              wikiData,
+            });
 
-                const oEmbedJSONHref = (oEmbedJSON && wikiData.wikiInfo.canonicalBase) && (
-                    wikiData.wikiInfo.canonicalBase + urls.from('shared.root').to('shared.path', paths.pathname + OEMBED_JSON_FILE));
-
-                const html = writePage.html(pageInfo, {
-                    defaultLanguage: finalDefaultLanguage,
-                    language,
-                    languages,
-                    localizedPaths,
-                    oEmbedJSONHref,
-                    paths,
-                    to,
-                    transformMultiline: bound.transformMultiline,
-                    wikiData
-                });
+            const oEmbedJSONHref =
+              oEmbedJSON &&
+              wikiData.wikiInfo.canonicalBase &&
+              wikiData.wikiInfo.canonicalBase +
+                urls
+                  .from("shared.root")
+                  .to("shared.path", paths.pathname + OEMBED_JSON_FILE);
+
+            const html = writePage.html(pageInfo, {
+              defaultLanguage: finalDefaultLanguage,
+              language,
+              languages,
+              localizedPaths,
+              oEmbedJSONHref,
+              paths,
+              to,
+              transformMultiline: bound.transformMultiline,
+              wikiData,
+            });
 
-                return writePage.write({
-                    html,
-                    oEmbedJSON,
-                    paths,
-                });
-            }),
-            ...redirectWrites.map(({fromPath, toPath, title: titleFn}) => () => {
+            return writePage.write({
+              html,
+              oEmbedJSON,
+              paths,
+            });
+          }),
+          ...redirectWrites.map(
+            ({ fromPath, toPath, title: titleFn }) =>
+              () => {
                 const title = titleFn({
-                    language
+                  language,
                 });
 
                 // TODO: This only supports one <>-style argument.
-                const fromPaths = writePage.paths(baseDirectory, 'localized.' + fromPath[0], fromPath[1]);
-                const to = writePage.to({baseDirectory, pageSubKey: fromPath[0], paths: fromPaths});
-
-                const target = to('localized.' + toPath[0], ...toPath.slice(1));
-                const html = generateRedirectPage(title, target, {language});
-                return writePage.write({html, paths: fromPaths});
-            })
-        ], queueSize));
-    };
+                const fromPaths = writePage.paths(
+                  baseDirectory,
+                  "localized." + fromPath[0],
+                  fromPath[1]
+                );
+                const to = writePage.to({
+                  baseDirectory,
+                  pageSubKey: fromPath[0],
+                  paths: fromPaths,
+                });
 
-    await wrapLanguages(perLanguageFn, {
-        languages,
-        writeOneLanguage,
-    });
+                const target = to("localized." + toPath[0], ...toPath.slice(1));
+                const html = generateRedirectPage(title, target, { language });
+                return writePage.write({ html, paths: fromPaths });
+              }
+          ),
+        ],
+        queueSize
+      )
+    );
+  };
+
+  await wrapLanguages(perLanguageFn, {
+    languages,
+    writeOneLanguage,
+  });
 
-    // The single most important step.
-    logInfo`Written!`;
+  // The single most important step.
+  logInfo`Written!`;
 }
 
-main().catch(error => {
+main()
+  .catch((error) => {
     if (error instanceof AggregateError) {
-        showAggregate(error);
+      showAggregate(error);
     } else {
-        console.error(error);
+      console.error(error);
     }
-}).then(() => {
+  })
+  .then(() => {
     decorateTime.displayTime();
     CacheableObject.showInvalidAccesses();
-});
+  });