« 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.js3624
1 files changed, 1702 insertions, 1922 deletions
diff --git a/src/upd8.js b/src/upd8.js
index ba59068..6bd52da 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -31,1719 +31,1640 @@
 // 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 {execSync} from 'node:child_process';
+import {readdir, readFile} from 'node:fs/promises';
+import * as path from 'node:path';
+import {fileURLToPath} from 'node: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';
-// Wait nevermind, I forgot a8out why-do-kids-love-the-taste-of-cinnamon-toast-
-// crunch. THAT is my 8est li8rary.
+import wrap from 'word-wrap';
 
-// It stands for "HTML Entities", apparently. Cursed.
-import he from 'he';
+// Due to import time shenanigans, these imports have to come in the specified
+// order. This obviously needs fixing up.
 
+/* precede #find */
 import {
-    copyFile,
-    mkdir,
-    readFile,
-    stat,
-    symlink,
-    writeFile,
-    unlink,
-} from 'fs/promises';
+  filterReferenceErrors,
+  reportDuplicateDirectories,
+  reportContentTextErrors,
+} from '#data-checks';
+
+import {bindFind, getAllFindSpecs} from '#find';
+
+// End of import time shenanigans (hopefully)
+
+import {showAggregate} from '#aggregate';
+import CacheableObject from '#cacheable-object';
+import {displayCompositeCacheAnalysis} from '#composite';
+import {processLanguageFile, watchLanguageFile, internalDefaultStringsFile}
+  from '#language';
+import {isMain, traverse} from '#node-utils';
+import {sortByName} from '#sort';
+import {empty, withEntries} from '#sugar';
+import {generateURLs, urlSpec} from '#urls';
+import {linkWikiDataArrays, loadAndProcessDataDocuments, sortWikiDataArrays}
+  from '#yaml';
 
-import { inspect as nodeInspect } from 'util';
+import {
+  colors,
+  decorateTime,
+  fileIssue,
+  logWarn,
+  logInfo,
+  logError,
+  parseOptions,
+  progressCallAll,
+} from '#cli';
+
+import genThumbs, {
+  CACHE_FILE as thumbsCacheFile,
+  defaultMagickThreads,
+  determineMediaCachePath,
+  isThumb,
+  migrateThumbsIntoDedicatedCacheDirectory,
+  verifyImagePaths,
+} from '#thumbs';
 
-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 FileSizePreloader from './file-size-preloader.js';
+import {listingSpec, listingTargetSpec} from './listing-spec.js';
+import * as buildModes from './write/build-modes/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';
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
-import CacheableObject from './data/cacheable-object.js';
+const CACHEBUST = 23;
 
-import { serializeThings } from './data/serialize.js';
+let COMMIT;
+try {
+  COMMIT = execSync('git log --format="%h %B" -n 1 HEAD', {cwd: __dirname}).toString().trim();
+} catch (error) {
+  COMMIT = '(failed to detect)';
+}
 
-import {
-    Language,
-} from './data/things.js';
+const BUILD_TIME = new Date();
 
-import {
-    filterDuplicateDirectories,
-    filterReferenceErrors,
-    linkWikiDataArrays,
-    loadAndProcessDataDocuments,
-    sortWikiDataArrays,
-    WIKI_INFO_FILE,
-} from './data/yaml.js';
+const STATUS_NOT_STARTED       = `not started`;
+const STATUS_NOT_APPLICABLE    = `not applicable`;
+const STATUS_STARTED_NOT_DONE  = `started but not yet done`;
+const STATUS_DONE_CLEAN        = `done without warnings`;
+const STATUS_FATAL_ERROR       = `fatal error`;
+const STATUS_HAS_WARNINGS      = `has warnings`;
 
-import {
-    fancifyFlashURL,
-    fancifyURL,
-    generateAdditionalFilesShortcut,
-    generateAdditionalFilesList,
-    generateChronologyLinks,
-    generateCoverLink,
-    generateInfoGalleryLinks,
-    generatePreviousNextLinks,
-    generateTrackListDividedByGroups,
-    getAlbumGridHTML,
-    getAlbumStylesheet,
-    getArtistString,
-    getFlashGridHTML,
-    getFooterLocalizationLinks,
-    getGridHTML,
-    getRevealStringFromTags,
-    getRevealStringFromWarnings,
-    getThemeString,
-    iconifyURL
-} from './misc-templates.js';
+const defaultStepStatus = {status: STATUS_NOT_STARTED, annotation: null};
 
-import {
-    color,
-    decorateTime,
-    logWarn,
-    logInfo,
-    logError,
-    parseOptions,
-    progressPromiseAll,
-    ENABLE_COLOR
-} from './util/cli.js';
+// Defined globally for quick access outside the main() function's contents.
+// This will be initialized and mutated over the course of main().
+let stepStatusSummary;
+let showStepStatusSummary = false;
 
-import {
-    validateReplacerSpec,
-    transformInline
-} from './util/replacer.js';
+async function main() {
+  Error.stackTraceLimit = Infinity;
 
-import {
-    chunkByConditions,
-    chunkByProperties,
-    getAlbumCover,
-    getAlbumListTag,
-    getAllTracks,
-    getArtistAvatar,
-    getArtistCommentary,
-    getArtistNumContributions,
-    getFlashCover,
-    getKebabCase,
-    getTotalDuration,
-    getTrackCover,
-    sortByArtDate,
-    sortByDate,
-    sortByName
-} from './util/wiki-data.js';
+  stepStatusSummary = {
+    determineMediaCachePath:
+      {...defaultStepStatus, name: `determine media cache path`},
 
-import {
-    serializeContribs,
-    serializeCover,
-    serializeGroupsForAlbum,
-    serializeGroupsForTrack,
-    serializeImagePaths,
-    serializeLink
-} from './util/serialize.js';
+    migrateThumbnails:
+      {...defaultStepStatus, name: `migrate thumbnails`},
 
-import {
-    bindOpts,
-    decorateErrorWithIndex,
-    filterAggregateAsync,
-    filterEmptyLines,
-    mapAggregate,
-    mapAggregateAsync,
-    openAggregate,
-    queue,
-    showAggregate,
-    splitArray,
-    unique,
-    withAggregate,
-    withEntries
-} from './util/sugar.js';
+    loadThumbnailCache:
+      {...defaultStepStatus, name: `load thumbnail cache file`},
 
-import {
-    generateURLs,
-    thumb
-} from './util/urls.js';
+    generateThumbnails:
+      {...defaultStepStatus, name: `generate thumbnails`},
 
-// Pensive emoji!
-import {
-    FANDOM_GROUP_DIRECTORY,
-    OFFICIAL_GROUP_DIRECTORY
-} from './util/magic-constants.js';
+    loadDataFiles:
+      {...defaultStepStatus, name: `load and process data files`},
 
-import FileSizePreloader from './file-size-preloader.js';
+    linkWikiDataArrays:
+      {...defaultStepStatus, name: `link wiki data arrays`},
 
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
+    precacheCommonData:
+      {...defaultStepStatus, name: `precache common data`},
 
-const CACHEBUST = 8;
+    reportDuplicateDirectories:
+      {...defaultStepStatus, name: `report duplicate directories`},
 
-const DEFAULT_STRINGS_FILE = 'strings-default.json';
+    filterReferenceErrors:
+      {...defaultStepStatus, name: `filter reference errors`},
 
-// 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
-// file~~ is now a variety of useful utilities!
-//
-// 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';
+    reportContentTextErrors:
+      {...defaultStepStatus, name: `report content text errors`},
 
-// 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';
+    sortWikiDataArrays:
+      {...defaultStepStatus, name: `sort wiki data arrays`},
 
-// This exists adjacent to index.html for any page with oEmbed metadata.
-const OEMBED_JSON_FILE = 'oembed.json';
+    precacheAllData:
+      {...defaultStepStatus, name: `precache nearly all data`},
 
-// Automatically copied (if present) from media directory to site root.
-const FAVICON_FILE = 'favicon.ico';
+    // TODO: This should be split into load/watch steps.
+    loadInternalDefaultLanguage:
+      {...defaultStepStatus, name: `load internal default language`},
 
-function inspect(value) {
-    return nodeInspect(value, {colors: ENABLE_COLOR});
-}
+    loadLanguageFiles:
+      {...defaultStepStatus, name: `statically load custom language files`},
 
-// Shared varia8les! These are more efficient to access than a shared varia8le
-// (or at least I h8pe so), and are easier to pass across functions than a
-// 8unch of specific arguments.
-//
-// Upd8: Okay yeah these aren't actually any different. Still cleaner than
-// passing around a data object containing all this, though.
-let dataPath;
-let mediaPath;
-let langPath;
-let outputPath;
+    watchLanguageFiles:
+      {...defaultStepStatus, name: `watch custom language files`},
 
-// Glo8al data o8ject shared 8etween 8uild functions and all that. This keeps
-// everything encapsul8ted in one place, so it's easy to pass and share across
-// modules!
-let wikiData = {};
+    initializeDefaultLanguage:
+      {...defaultStepStatus, name: `initialize default language`},
 
-let queueSize;
+    verifyImagePaths:
+      {...defaultStepStatus, name: `verify missing/misplaced image paths`},
 
-const urls = generateURLs(urlSpec);
+    preloadFileSizes:
+      {...defaultStepStatus, name: `preload file sizes`},
 
-function splitLines(text) {
-    return text.split(/\r\n|\r|\n/);
-}
+    performBuild:
+      {...defaultStepStatus, name: `perform selected build mode`},
+  };
 
-const replacerSpec = {
-    'album': {
-        find: 'album',
-        link: 'album'
+  const defaultQueueSize = 500;
+
+  const buildModeFlagOptions = (
+    withEntries(buildModes, entries =>
+      entries.map(([key, mode]) => [key, {
+        help: mode.description,
+        type: 'flag',
+      }])));
+
+  const selectedBuildModeFlags = Object.keys(
+    await parseOptions(process.argv.slice(2), {
+      [parseOptions.handleUnknown]: () => {},
+      ...buildModeFlagOptions,
+    }));
+
+  let selectedBuildModeFlag;
+  let usingDefaultBuildMode;
+
+  if (empty(selectedBuildModeFlags)) {
+    selectedBuildModeFlag = 'static-build';
+    usingDefaultBuildMode = true;
+  } else if (selectedBuildModeFlags.length > 1) {
+    logError`Building multiple modes (${selectedBuildModeFlags.join(', ')}) at once not supported.`;
+    logError`Please specify a maximum of one build mode.`;
+    return false;
+  } else {
+    selectedBuildModeFlag = selectedBuildModeFlags[0];
+    usingDefaultBuildMode = false;
+  }
+
+  const selectedBuildMode = buildModes[selectedBuildModeFlag];
+
+  // This is about to get a whole lot more stuff put in it.
+  const wikiData = {
+    listingSpec,
+    listingTargetSpec,
+  };
+
+  const buildOptions = selectedBuildMode.getCLIOptions();
+
+  const commonOptions = {
+    'help': {
+      help: `Display usage info and basic information for the \`hsmusic\` command`,
+      type: 'flag',
     },
-    'album-commentary': {
-        find: 'album',
-        link: 'albumCommentary'
+
+    // 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': {
+      help: `Specify path to data directory, including YAML files that cover all info about wiki content, layout, and structure\n\nAlways required for wiki building, but may be provided via the HSMUSIC_DATA environment variable instead`,
+      type: 'value',
     },
-    'artist': {
-        find: 'artist',
-        link: 'artist'
+
+    // 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': {
+      help: `Specify path to media directory, including album artwork and additional files, as well as custom site layout media and other media files for reference or linking in wiki content\n\nAlways required for wiki building, but may be provided via the HSMUSIC_MEDIA environment variable instead`,
+      type: 'value',
     },
-    'artist-gallery': {
-        find: 'artist',
-        link: 'artistGallery'
+
+    'media-cache-path': {
+      help: `Specify path to media cache directory, including automatically generated thumbnails\n\nThis usually doesn't need to be provided, and will be inferred by adding "-cache" to the end of the media directory`,
+      type: 'value',
     },
-    'commentary-index': {
-        find: null,
-        link: 'commentaryIndex'
+
+    // 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': {
+      help: `Specify path to language directory, including JSON files that mapping internal string keys to localized language content, and various language metadata\n\nOptional for wiki building, unless the wiki's default language is not English; may be provided via the HSMUSIC_LANG environment variable instead`,
+      type: 'value',
     },
-    'date': {
-        find: null,
-        value: ref => new Date(ref),
-        html: (date, {language}) => `<time datetime="${date.toString()}">${language.formatDate(date)}</time>`
+
+    'skip-reference-validation': {
+      help: `Skips checking and reporting reference errors, which speeds up the build but may silently allow erroneous data to pass through`,
+      type: 'flag',
     },
-    '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;
-            }
-        }
+
+    // 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': {
+      help: `Skip processing and generating thumbnails in media directory (speeds up subsequent builds, but remove this option [or use --thumbs-only] and re-run once when you add or modify media files to ensure thumbnails stay up-to-date!)`,
+      type: 'flag',
     },
-    'group': {
-        find: 'group',
-        link: 'groupInfo'
+
+    // 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': {
+      help: `Skip everything besides processing media directory and generating up-to-date thumbnails (useful when using --skip-thumbs for most runs)`,
+      type: 'flag',
     },
-    'group-gallery': {
-        find: 'group',
-        link: 'groupGallery'
+
+    'migrate-thumbs': {
+      help: `Transfer automatically generated thumbnail files out of an existing media directory and into the easier-to-manage media-cache directory`,
+      type: 'flag',
     },
-    'home': {
-        find: null,
-        link: 'home'
+
+    'skip-file-sizes': {
+      help: `Skips preloading file sizes for images and additional files, which will be left blank in the build`,
+      type: 'flag',
     },
-    'listing-index': {
-        find: null,
-        link: 'listingIndex'
+
+    'skip-media-validation': {
+      help: `Skips checking and reporting missing and misplaced media files, which isn't necessary if you aren't adding or removing data or updating directories`,
+      type: 'flag',
     },
-    'listing': {
-        find: 'listing',
-        link: 'listing'
+
+    // 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': {
+      help: `Don't run a build of the site at all; only process data/media and report any errors detected`,
+      type: 'flag',
     },
-    'media': {
-        find: null,
-        link: 'media'
+
+    'no-input': {
+      help: `Don't wait on input from stdin - assume the device is headless`,
+      type: 'flag',
     },
-    'news-index': {
-        find: null,
-        link: 'newsIndex'
+
+    'no-language-reloading': {
+      help: `Don't reload language files while the build is running\n\nApplied by default for --static-build`,
+      type: 'flag',
     },
-    'news-entry': {
-        find: 'newsEntry',
-        link: 'newsEntry'
+
+    'no-language-reload': {alias: 'no-language-reloading'},
+
+    // 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': {
+      help: `Show JavaScript source code paths for reported errors in "aggregate" error displays\n\n(Debugging use only, but please enable this if you're reporting bugs for our issue tracker!)`,
+      type: 'flag',
     },
-    'root': {
-        find: null,
-        link: 'root'
+
+    'show-step-summary': {
+      help: `Show a summary of all the top-level build steps once hsmusic exits. This is mostly useful for progammer debugging!`,
+      type: 'flag',
     },
-    'site': {
-        find: null,
-        link: 'site'
+
+    'queue-size': {
+      help: `Process more or fewer disk files at once to optimize performance or avoid I/O errors, unlimited if set to 0 (between 500 and 700 is usually a safe range for building HSMusic on Windows machines)\nDefaults to ${defaultQueueSize}`,
+      type: 'value',
+      validate(size) {
+        if (parseInt(size) !== parseFloat(size)) return 'an integer';
+        if (parseInt(size) < 0) return 'a counting number or zero';
+        return true;
+      },
     },
-    'static': {
-        find: 'staticPage',
-        link: 'staticPage'
+    queue: {alias: 'queue-size'},
+
+    'magick-threads': {
+      help: `Process more or fewer thumbnail files at once with ImageMagick when generating thumbnails. (Each ImageMagick thread may also make use of multi-core processing at its own utility.)`,
+      type: 'value',
+      validate(threads) {
+        if (parseInt(threads) !== parseFloat(threads)) return 'an integer';
+        if (parseInt(threads) < 0) return 'a counting number or zero';
+        return true;
+      }
     },
-    'string': {
-        find: null,
-        value: ref => ref,
-        html: (ref, {language, args}) => language.$(ref, args)
+    magick: {alias: 'magick-threads'},
+
+    // 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': {
+      help: `Report accesses at runtime to nonexistant properties on wiki data objects, at a dramatic performance cost\n(Internal/development use only)`,
+      type: 'flag',
     },
-    'tag': {
-        find: 'artTag',
-        link: 'tag'
+
+    'precache-mode': {
+      help:
+        `Change the way certain runtime-computed values are preemptively evaluated and cached\n\n` +
+        `common: Preemptively compute certain properties which are needed for basic data loading and site generation\n\n` +
+        `all: Compute every visible data property, optimizing rate of content generation, but causing a long stall before the build actually starts\n\n` +
+        `none: Don't preemptively compute any values - strictly the most efficient, but may result in unpredictably "lopsided" performance for individual steps of loading data and building the site\n\n` +
+        `Defaults to 'common'`,
+      type: 'value',
+      validate(value) {
+        if (['common', 'all', 'none'].includes(value)) return true;
+        return 'common, all, or none';
+      },
     },
-    'track': {
-        find: 'track',
-        link: 'track'
-    }
-};
+  };
 
-if (!validateReplacerSpec(replacerSpec, {find, link: unbound_link})) {
-    process.exit();
-}
+  const cliOptions = await parseOptions(process.argv.slice(2), {
+    // We don't want to error when we receive these options, so specify them
+    // here, even though we won't be doing anything with them later.
+    // (This is a bit of a hack.)
+    ...buildModeFlagOptions,
 
-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;
+    ...commonOptions,
+    ...buildOptions,
+  });
+
+  if (cliOptions['help']) {
+    const indentWrap = (spaces, str) => wrap(str, {width: 60 - spaces, indent: ' '.repeat(spaces)});
+
+    const showOptions = (msg, options) => {
+      console.log(colors.bright(msg));
+
+      const entries = Object.entries(options);
+      const sortedOptions = sortByName(entries
+        .map(([name, descriptor]) => ({name, descriptor})));
+
+      if (!sortedOptions.length) {
+        console.log(`(No options available)`)
+      }
+
+      let justInsertedPaddingLine = false;
+
+      for (const {name, descriptor} of sortedOptions) {
+        if (descriptor.alias) {
+          continue;
         }
-    };
 
-    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;
+        const aliases = entries
+          .filter(([_name, {alias}]) => alias === name)
+          .map(([name]) => name);
+
+        let wrappedHelp, wrappedHelpLines = 0;
+        if (descriptor.help) {
+          wrappedHelp = indentWrap(4, descriptor.help);
+          wrappedHelpLines = wrappedHelp.split('\n').length;
+        }
+
+        if (wrappedHelpLines > 0 && !justInsertedPaddingLine) {
+          console.log('');
         }
-    }
-    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 = [];
+        console.log(colors.bright(` --` + name) +
+          (aliases.length
+            ? ` (or: ${aliases.map(alias => colors.bright(`--` + alias)).join(', ')})`
+            : '') +
+          (descriptor.help
+            ? ''
+            : colors.dim('  (no help provided)')));
 
-    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 (wrappedHelp) {
+          console.log(wrappedHelp);
         }
-    }
 
-    if (lineSoFar) {
-        outLines.push(lineSoFar);
+        if (wrappedHelpLines > 1) {
+          console.log('');
+          justInsertedPaddingLine = true;
+        } else {
+          justInsertedPaddingLine = false;
+        }
+      }
+
+      if (!justInsertedPaddingLine) {
+        console.log(``);
+      }
+    };
+
+    console.log(
+      colors.bright(`hsmusic (aka. Homestuck Music Wiki)\n`) +
+      `static wiki software cataloguing collaborative creation\n`);
+
+    console.log(indentWrap(0,
+      `The \`hsmusic\` command provides basic control over all parts of generating user-visible HTML pages and website content/structure from provided data, media, and language directories.\n` +
+      `\n` +
+      `CLI options are divided into three groups:\n`));
+    console.log(` 1) ` + indentWrap(4,
+      `Common options: These are shared by all build modes and always have the same essential behavior`).trim());
+    console.log(` 2) ` + indentWrap(4,
+      `Build mode selection: One build mode may be selected (or else the default, --static-build, is used), and it decides which entire set of behavior to use for providing site content to the user`).trim());
+    console.log(` 3) ` + indentWrap(4,
+      `Build options: Each build mode has a set of unique options which customize behavior for that build mode`).trim());
+    console.log(``);
+
+    showOptions(`Common options`, commonOptions);
+    showOptions(`Build mode selection`, buildModeFlagOptions);
+
+    if (buildOptions) {
+      showOptions(`Build options for --${selectedBuildModeFlag} (${
+        usingDefaultBuildMode ? 'default' : 'selected'
+      })`, buildOptions);
     }
 
-    return outLines;
-}
+    return true;
+  }
 
-function transformMultiline(text, {
-    parseAttributes,
-    transformInline
-}) {
-    // Heck yes, HTML magics.
+  const dataPath = cliOptions['data-path'] || process.env.HSMUSIC_DATA;
+  const mediaPath = cliOptions['media-path'] || process.env.HSMUSIC_MEDIA;
+  const langPath = cliOptions['lang-path'] || process.env.HSMUSIC_LANG; // Can 8e left unset!
 
-    text = transformInline(text.trim());
+  const thumbsOnly = cliOptions['thumbs-only'] ?? false;
+  const noInput = cliOptions['no-input'] ?? false;
 
-    const outLines = [];
+  showStepStatusSummary = cliOptions['show-step-summary'] ?? false;
 
-    const indentString = ' '.repeat(4);
+  const showAggregateTraces = cliOptions['show-traces'] ?? false;
 
-    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>';
+  const precacheMode = cliOptions['precache-mode'] ?? 'common';
+  const showInvalidPropertyAccesses = cliOptions['show-invalid-property-accesses'] ?? false;
+
+  // Makes writing nicer on the CPU and file I/O parts of the OS, with a
+  // marginal performance deficit while waiting for file writes to finish
+  // before proceeding to more page processing.
+  const queueSize = +(cliOptions['queue-size'] ?? defaultQueueSize);
+
+  const magickThreads = +(cliOptions['magick-threads'] ?? defaultMagickThreads);
+
+  if (!dataPath) {
+    logError`${`Expected --data-path option or HSMUSIC_DATA to be set`}`;
+  }
+
+  if (!mediaPath) {
+    logError`${`Expected --media-path option or HSMUSIC_MEDIA to be set`}`;
+  }
+
+  if (!dataPath || !mediaPath) {
+    return false;
+  }
+
+  if (cliOptions['no-build']) {
+    logInfo`Won't generate any site or page files this run (--no-build passed).`;
+
+    Object.assign(stepStatusSummary.performBuild, {
+      status: STATUS_NOT_APPLICABLE,
+      annotation: `--no-build provided`,
+    });
+  } else {
+    if (usingDefaultBuildMode) {
+      logInfo`No build mode specified, will use default: ${selectedBuildModeFlag}`;
+    } else {
+      logInfo`Will use specified build mode: ${selectedBuildModeFlag}`;
+    }
+  }
+
+  // Finish setting up defaults by combining information from all options.
+
+  const _fallbackStep = (stepKey, {
+    default: defaultValue,
+
+    cli: {
+      flag: cliFlag = null,
+      negate: cliFlagNegates = false,
+      warn: cliFlagWarning = null,
+    } = {},
+
+    buildConfig: buildConfigKey,
+  }) => {
+    const {[buildConfigKey]: buildConfig} = selectedBuildMode.config;
+    const {[stepKey]: step} = stepStatusSummary;
+
+    if (cliFlag && cliOptions[cliFlag]) {
+      const cliPart = `--` + cliFlag;
+      const modePart = `--` + selectedBuildModeFlag;
+      if (buildConfig?.applicable === false) {
+        if (cliFlagNegates) {
+          logWarn`${cliPart} provided, but ${modePart} already skips this step`;
+          logWarn`Redundant option ${cliPart}`;
         } 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>');
+          logWarn`${cliPart} provided, but this step isn't applicable for ${modePart}`;
+          logWarn`Ignoring option ${cliPart}`;
         }
-        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 if (buildConfig?.required === true) {
+        if (cliFlagNegates) {
+          logWarn`${cliPart} provided, but ${modePart} requires this step`;
+          logWarn`Ignoring option ${cliPart}`;
         } else {
-            // closing the final list level! no need for indent here
-            outLines.push('</ul>');
+          logWarn`${cliPart} provided, but ${modePart} already requires this step`;
+          logWarn`Redundant option ${cliPart}`;
         }
-    };
-
-    // 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>');
-            }
-
-            // 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('\\>', '>');
-            }
+      } else {
+        if (cliFlagNegates) {
+          step.status = STATUS_NOT_APPLICABLE;
+          step.annotation = `--${cliFlag} provided`;
         }
-
-        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 = '';
-            }
+        if (cliFlagWarning) {
+          for (const line of cliFlagWarning.split('\n')) {
+            logWarn(line);
+          }
         }
+      }
+    }
 
-        let pushString = indentString.repeat(indentThisLine);
-        if (lineTag) {
-            pushString += `<${lineTag}>${lineContent}</${lineTag}>`;
-        } else {
-            pushString += lineContent;
-        }
-        outLines.push(pushString);
+    if (buildConfig?.applicable === false) {
+      step.status = STATUS_NOT_APPLICABLE;
+      step.annotation = `N/A for --${selectedBuildModeFlag}`;
+      return;
+    }
+
+    if (buildConfig?.default === 'skip') {
+      step.status = STATUS_NOT_APPLICABLE;
+      step.annotation = `default for --${selectedBuildModeFlag}`;
+      return;
     }
 
-    // after processing all lines...
+    switch (defaultValue) {
+      case 'skip':
+        step.status = STATUS_NOT_APPLICABLE;
+        if (cliFlag && !cliFlagNegates) {
+          step.annotation = `--${cliFlag} not provided`;
+        }
+        break;
 
-    // if still in a list, close all levels
-    while (levelIndents.length) closeLevel();
+      case 'perform':
+        break;
 
-    // if still in a blockquote, close its tag
-    if (inBlockquote) {
-        inBlockquote = false;
-        outLines.push('</blockquote>');
+      default:
+        throw new Error(`Invalid default step status ${defaultValue}`);
     }
+  };
 
-    return outLines.join('\n');
-}
+  {
+    let errored = false;
 
-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);
-    }
+    const fallbackStep = (stepKey, options) => {
+      try {
+        _fallbackStep(stepKey, options);
+      } catch (error) {
+        logError`Error determining fallback for step ${stepKey}`;
+        showAggregate(error);
+        errored = true;
+      }
+    };
 
-    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 = '';
-        }
-    }
-    if (buildLine.length) {
-        addLine();
-    }
-    return outLines.join('\n');
-}
+    fallbackStep('filterReferenceErrors', {
+      default: 'perform',
+      buildConfig: null,
+      cli: {
+        flag: 'skip-reference-validation',
+        negate: true,
+        warn:
+          `Skipping reference validation. If any reference errors are present\n` +
+          `in data, they will be silently passed along to the build.`,
+      }
+    });
 
-function stringifyThings(thingData) {
-    return JSON.stringify(serializeThings(thingData));
-}
+    fallbackStep('generateThumbnails', {
+      default: 'perform',
+      buildConfig: 'thumbs',
+      cli: {
+        flag: 'skip-thumbs',
+        negate: true,
+      },
+    });
 
-function img({
-    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
+    fallbackStep('migrateThumbnails', {
+      default: 'skip',
+      buildConfig: null,
+      cli: {
+        flag: 'migrate-thumbs',
+      },
     });
 
-    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`
-            <noscript>${nonlazyHTML}</noscript>
-            ${lazyHTML}
-        `;
-    } else {
-        return nonlazyHTML;
-    }
+    fallbackStep('preloadFileSizes', {
+      default: 'perform',
+      buildConfig: 'fileSizes',
+      cli: {
+        flag: 'skip-file-sizes',
+        negate: true,
+      },
+    });
 
-    function wrap(input, hide = false) {
-        let wrapped = input;
+    fallbackStep('verifyImagePaths', {
+      default: 'perform',
+      buildConfig: 'mediaValidation',
+      cli: {
+        flag: 'skip-media-validation',
+        negate: true,
+        warning:
+          `Skipping media validation. If any media files are missing or misplaced,\n` +
+          `those errors will be silently passed along to the build.`,
+      },
+    });
 
-        wrapped = `<div class="image-inner-area">${wrapped}</div>`;
-        wrapped = `<div class="image-container">${wrapped}</div>`;
+    fallbackStep('watchLanguageFiles', {
+      default: 'perform',
+      buildConfig: 'languageReloading',
+      cli: {
+        flag: 'no-language-reloading',
+        negate: true,
+      },
+    });
 
-        if (reveal) {
-            wrapped = fixWS`
-                <div class="reveal">
-                    ${wrapped}
-                    <span class="reveal-text">${reveal}</span>
-                </div>
-            `;
-        }
+    if (errored) {
+      return false;
+    }
+  }
 
-        if (willSquare) {
-            wrapped = html.tag('div', {class: 'square-content'}, wrapped);
-            wrapped = html.tag('div', {class: ['square', hide && !willLink && 'js-hide']}, wrapped);
-        }
+  if (stepStatusSummary.generateThumbnails.status === STATUS_NOT_STARTED) {
+    Object.assign(stepStatusSummary.loadThumbnailCache, {
+      status: STATUS_NOT_APPLICABLE,
+      annotation: `using cache from thumbnail generation`,
+    });
+  }
 
-        if (willLink) {
-            wrapped = html.tag('a', {
-                id,
-                class: ['box', hide && 'js-hide'],
-                href: typeof link === 'string' ? link : originalSrc
-            }, wrapped);
-        }
+  if (stepStatusSummary.watchLanguageFiles.status === STATUS_NOT_STARTED) {
+    Object.assign(stepStatusSummary.loadLanguageFiles, {
+      status: STATUS_NOT_APPLICABLE,
+      annotation: `watching for changes instead`,
+    });
+  }
+
+  switch (precacheMode) {
+    case 'common':
+      Object.assign(stepStatusSummary.precacheAllData, {
+        status: STATUS_NOT_APPLICABLE,
+        annotation: `--precache-mode is common, not all`,
+      });
+
+      break;
+
+    case 'all':
+      Object.assign(stepStatusSummary.precacheCommonData, {
+        status: STATUS_NOT_APPLICABLE,
+        annotation: `--precache-mode is all, not common`,
+      });
+
+      break;
+
+    case 'none':
+      Object.assign(stepStatusSummary.precacheCommonData, {
+        status: STATUS_NOT_APPLICABLE,
+        annotation: `--precache-mode is none`,
+      });
+
+      Object.assign(stepStatusSummary.precacheAllData, {
+        status: STATUS_NOT_APPLICABLE,
+        annotation: `--precache-mode is none`,
+      });
+
+      break;
+  }
+
+  if (!langPath) {
+    Object.assign(stepStatusSummary.loadLanguageFiles, {
+      status: STATUS_NOT_APPLICABLE,
+      annotation: `neither --lang-path nor HSMUSIC_LANG provided`,
+    });
 
-        return wrapped;
-    }
-}
+    Object.assign(stepStatusSummary.watchLanguageFiles, {
+      status: STATUS_NOT_APPLICABLE,
+      annotation: `neither --lang-path nor HSMUSIC_LANG provided`,
+    });
+  }
+
+  if (stepStatusSummary.generateThumbnails.status === STATUS_NOT_APPLICABLE && thumbsOnly) {
+    logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`;
+    return false;
+  }
+
+  Object.assign(stepStatusSummary.determineMediaCachePath, {
+    status: STATUS_STARTED_NOT_DONE,
+    timeStart: Date.now(),
+  });
+
+  const {mediaCachePath, annotation: mediaCachePathAnnotation} =
+    await determineMediaCachePath({
+      mediaPath,
+      providedMediaCachePath:
+        cliOptions['media-cache-path'] || process.env.HSMUSIC_MEDIA_CACHE,
+      disallowDoubling:
+        stepStatusSummary.migrateThumbnails.status === STATUS_NOT_STARTED,
+    });
 
-function validateWritePath(path, urlGroup) {
-    if (!Array.isArray(path)) {
-        return {error: `Expected array, got ${path}`};
+  if (!mediaCachePath) {
+    logError`Couldn't determine a media cache path. (${mediaCachePathAnnotation})`;
+
+    switch (mediaCachePathAnnotation) {
+      case 'inferred path does not have cache':
+        logError`If you're certain this is the right path, you can provide it via`;
+        logError`${'--media-cache-path'} or ${'HSMUSIC_MEDIA_CACHE'}, and it should work.`;
+        break;
+
+      case 'inferred path not readable':
+        logError`The folder couldn't be read, which usually indicates`;
+        logError`a permissions error. Try to resolve this, or provide`;
+        logError`a new path with ${'--media-cache-path'} or ${'HSMUSIC_MEDIA_CACHE'}.`;
+        break;
+
+      case 'media path not provided': /* unreachable */
+        logError`It seems a ${'--media-path'} (or ${'HSMUSIC_MEDIA'}) wasn't provided.`;
+        logError`Make sure one of these is actually pointing to a path that exists.`;
+        break;
     }
 
-    const { paths } = urlGroup;
+    Object.assign(stepStatusSummary.determineMediaCachePath, {
+      status: STATUS_FATAL_ERROR,
+      annotation: mediaCachePathAnnotation,
+      timeEnd: Date.now(),
+    });
 
-    const definedKeys = Object.keys(paths);
-    const specifiedKey = path[0];
+    return false;
+  }
 
-    if (!definedKeys.includes(specifiedKey)) {
-        return {error: `Specified key ${specifiedKey} isn't defined`};
-    }
+  logInfo`Using media cache at: ${mediaCachePath} (${mediaCachePathAnnotation})`;
 
-    const expectedArgs = paths[specifiedKey].match(/<>/g)?.length ?? 0;
-    const specifiedArgs = path.length - 1;
+  Object.assign(stepStatusSummary.determineMediaCachePath, {
+    status: STATUS_DONE_CLEAN,
+    annotation: mediaCachePathAnnotation,
+    timeEnd: Date.now(),
+  });
 
-    if (specifiedArgs !== expectedArgs) {
-        return {error: `Expected ${expectedArgs} arguments, got ${specifiedArgs}`};
-    }
+  if (stepStatusSummary.migrateThumbnails.status === STATUS_NOT_STARTED) {
+    Object.assign(stepStatusSummary.migrateThumbnails, {
+      status: STATUS_STARTED_NOT_DONE,
+      timeStart: Date.now(),
+    });
 
-    return {success: true};
-}
+    const result = await migrateThumbsIntoDedicatedCacheDirectory({
+      mediaPath,
+      mediaCachePath,
+      queueSize,
+    });
 
-function validateWriteObject(obj) {
-    if (typeof obj !== 'object') {
-        return {error: `Expected object, got ${typeof obj}`};
-    }
+    if (result.succses) {
+      Object.assign(stepStatusSummary.migrateThumbnails, {
+        status: STATUS_FATAL_ERROR,
+        annotation: `view log for details`,
+        timeEnd: Date.now(),
+      });
 
-    if (typeof obj.type !== 'string') {
-        return {error: `Expected type to be string, got ${obj.type}`};
+      return false;
     }
 
-    switch (obj.type) {
-        case 'legacy': {
-            if (typeof obj.write !== 'function') {
-                return {error: `Expected write to be string, got ${obj.write}`};
-            }
+    logInfo`Good to go! Run hsmusic again without ${'--migrate-thumbs'} to start`;
+    logInfo`using the migrated media cache.`;
 
-            break;
-        }
+    Object.assign(stepStatusSummary.migrateThumbnails, {
+      status: STATUS_DONE_CLEAN,
+      timeEnd: Date.now(),
+    });
 
-        case 'page': {
-            const path = validateWritePath(obj.path, urlSpec.localized);
-            if (path.error) {
-                return {error: `Path validation failed: ${path.error}`};
-            }
+    return true;
+  }
 
-            if (typeof obj.page !== 'function') {
-                return {error: `Expected page to be function, got ${obj.content}`};
-            }
+  const niceShowAggregate = (error, ...opts) => {
+    showAggregate(error, {
+      showTraces: showAggregateTraces,
+      pathToFileURL: (f) => path.relative(__dirname, fileURLToPath(f)),
+      ...opts,
+    });
+  };
 
-            break;
-        }
+  if (
+    stepStatusSummary.loadThumbnailCache.status === STATUS_NOT_STARTED &&
+    stepStatusSummary.generateThumbnails.status === STATUS_NOT_STARTED
+  ) {
+    throw new Error(`Unable to continue with both loadThumbnailCache and generateThumbnails`);
+  }
 
-        case 'data': {
-            const path = validateWritePath(obj.path, urlSpec.data);
-            if (path.error) {
-                return {error: `Path validation failed: ${path.error}`};
-            }
+  let thumbsCache;
 
-            if (typeof obj.data !== 'function') {
-                return {error: `Expected data to be function, got ${obj.data}`};
-            }
+  if (stepStatusSummary.loadThumbnailCache.status === STATUS_NOT_STARTED) {
+    Object.assign(stepStatusSummary.loadThumbnailCache, {
+      status: STATUS_STARTED_NOT_DONE,
+      timeStart: Date.now(),
+    });
 
-            break;
-        }
+    const thumbsCachePath = path.join(mediaCachePath, thumbsCacheFile);
 
-        case 'redirect': {
-            const fromPath = validateWritePath(obj.fromPath, urlSpec.localized);
-            if (fromPath.error) {
-                return {error: `Path (fromPath) validation failed: ${fromPath.error}`};
-            }
+    try {
+      thumbsCache = JSON.parse(await readFile(thumbsCachePath));
+    } catch (error) {
+      if (error.code === 'ENOENT') {
+        logError`The thumbnail cache doesn't exist, and it's necessary to build`
+        logError`the website. Please run once without ${'--skip-thumbs'} - after`
+        logError`that you'll be good to go and don't need to process thumbnails`
+        logError`again!`;
+
+        Object.assign(stepStatusSummary.loadThumbnailCache, {
+          status: STATUS_FATAL_ERROR,
+          annotation: `cache does not exist`,
+          timeEnd: Date.now(),
+        });
 
-            const toPath = validateWritePath(obj.toPath, urlSpec.localized);
-            if (toPath.error) {
-                return {error: `Path (toPath) validation failed: ${toPath.error}`};
-            }
+        return false;
+      } else {
+        logError`Malformed or unreadable thumbnail cache file: ${error}`;
+        logError`Path: ${thumbsCachePath}`;
+        logError`The thumbbnail cache is necessary to build the site, so you'll`;
+        logError`have to investigate this to get the build working. Try running`;
+        logError`again without ${'--skip-thumbs'}. If you can't get it working,`;
+        logError`you're welcome to message in the HSMusic Discord and we'll try`;
+        logError`to help you out with troubleshooting!`;
+        logError`${'https://hsmusic.wiki/discord/'}`;
+
+        Object.assign(stepStatusSummary.loadThumbnailCache, {
+          status: STATUS_FATAL_ERROR,
+          annotation: `cache malformed or unreadable`,
+          timeEnd: Date.now(),
+        });
 
-            if (typeof obj.title !== 'function') {
-                return {error: `Expected title to be function, got ${obj.title}`};
-            }
+        return false;
+      }
+    }
 
-            break;
-        }
+    logInfo`Thumbnail cache file successfully read.`;
 
-        default: {
-            return {error: `Unknown type: ${obj.type}`};
-        }
-    }
+    Object.assign(stepStatusSummary.loadThumbnailCache, {
+      status: STATUS_DONE_CLEAN,
+      timeEnd: Date.now(),
+    });
 
-    return {success: true};
-}
+    logInfo`Skipping thumbnail generation.`;
+  } else if (stepStatusSummary.generateThumbnails.status === STATUS_NOT_STARTED) {
+    Object.assign(stepStatusSummary.generateThumbnails, {
+      status: STATUS_STARTED_NOT_DONE,
+      timeStart: Date.now(),
+    });
 
-/*
-async function writeData(subKey, directory, data) {
-    const paths = writePage.paths('', 'data.' + subKey, directory, {file: 'data.json'});
-    await writePage.write(JSON.stringify(data), {paths});
-}
-*/
-
-// This used to 8e a function! It's long 8een divided into multiple helper
-// functions, and nowadays we just directly access those, rather than ever
-// touching the original one (which had contained everything).
-const writePage = {};
-
-writePage.to = ({
-    baseDirectory,
-    pageSubKey,
-    paths
-}) => (targetFullKey, ...args) => {
-    const [ groupKey, subKey ] = targetFullKey.split('.');
-    let path = paths.subdirectoryPrefix;
-
-    let from;
-    let 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;
-    } 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;
-    }
+    logInfo`Begin thumbnail generation... -----+`;
 
-    path += urls.from(from).to(to, ...args);
-
-    return path;
-};
-
-writePage.html = (pageInfo, {
-    defaultLanguage,
-    language,
-    languages,
-    localizedPaths,
-    paths,
-    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 = {},
-        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;
-    }
+    const result = await genThumbs({
+      mediaPath,
+      mediaCachePath,
 
-    nav.classes ??= [];
-    nav.content ??= '';
-    nav.links ??= [];
+      queueSize,
+      magickThreads,
+      quiet: !thumbsOnly,
+    });
+
+    logInfo`Done thumbnail generation! --------+`;
 
-    footer.classes ??= [];
-    footer.content ??= (wikiInfo.footerContent ? transformMultiline(wikiInfo.footerContent) : '');
+    if (!result.success) {
+      Object.assign(stepStatusSummary.generateThumbnails, {
+        status: STATUS_FATAL_ERROR,
+        annotation: `view log for details`,
+        timeEnd: Date.now(),
+      });
 
-    footer.content += '\n' + getFooterLocalizationLinks(paths.pathname, {
-        defaultLanguage, languages, paths, language, to
+      return false;
+    }
+
+    Object.assign(stepStatusSummary.generateThumbnails, {
+      status: STATUS_DONE_CLEAN,
+      timeEnd: Date.now(),
     });
 
-    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.links = [
-            {toHome: true},
-            {toCurrentPage: true}
-        ];
+    if (thumbsOnly) {
+      return true;
     }
 
-    const links = (nav.links || []).filter(Boolean);
+    thumbsCache = result.cache;
+  } else {
+    thumbsCache = {};
+  }
 
-    const navLinkParts = [];
-    for (let i = 0; i < links.length; i++) {
-        let cur = links[i];
-        const prev = links[i - 1];
-        const next = links[i + 1];
+  if (showInvalidPropertyAccesses) {
+    CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true;
+  }
 
-        let { title: linkTitle } = cur;
+  Object.assign(stepStatusSummary.loadDataFiles, {
+    status: STATUS_STARTED_NOT_DONE,
+    timeStart: Date.now(),
+  });
 
-        if (cur.toHome) {
-            linkTitle ??= wikiInfo.nameShort;
-        } else if (cur.toCurrentPage) {
-            linkTitle ??= title;
-        }
+  let processDataAggregate, wikiDataResult;
 
-        let part = prev && (cur.divider ?? true) ? '/ ' : '';
+  try {
+    ({aggregate: processDataAggregate, result: wikiDataResult} =
+        await loadAndProcessDataDocuments({dataPath}));
+  } catch (error) {
+    console.error(error);
 
-        if (typeof cur.html === 'string') {
-            if (!cur.html) {
-                logWarn`Empty HTML in nav link ${JSON.stringify(cur)}`;
-            }
-            part += `<span>${cur.html}</span>`;
-        } 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)})`);
-            }
-            part += html.tag('a', attributes, linkTitle);
-        }
-        navLinkParts.push(part);
-    }
+    logError`There was a JavaScript error loading data files.`;
+    fileIssue();
 
-    const navHTML = html.tag('nav', {
-        [html.onlyIfContent]: true,
-        id: 'header',
-        class: nav.classes
-    }, [
-        links.length && html.tag('h2', {class: 'highlight-last-link'}, navLinkParts),
-        nav.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,
-        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
-                        })}
-                    </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})
-                        })}
-                    </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>
-                </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`
-        <!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')
-        })}>
-            <head>
-                <title>${(showWikiNameInTitle
-                    ? language.formatString('misc.pageTitle.withWikiName', {
-                        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')}
-                ${canonical && `<link rel="canonical" href="${canonical}">`}
-                ${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`
-                    <style>
-                        ${theme}
-                        ${stylesheet}
-                    </style>
-                `}
-                <script src="${to('shared.staticFile', `lazy-loading.js?${CACHEBUST}`)}"></script>
-            </head>
-            <body ${html.attributes({style: body.style || ''})}>
-                <div id="page-container">
-                    ${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`
-                                <span class="skipper"><a href="${href}">${title}</a></span>
-                            `).join('\n')}
-                        </div>
-                    `}
-                    ${layoutHTML}
-                </div>
-                ${infoCardHTML}
-                <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.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,
-    };
-};
+    Object.assign(stepStatusSummary.loadDataFiles, {
+      status: STATUS_FATAL_ERROR,
+      annotation: `javascript error - view log for details`,
+      timeEnd: Date.now(),
+    });
+
+    return false;
+  }
+
+  Object.assign(wikiData, wikiDataResult);
+
+  {
+    const logThings = (prop, label) => {
+      const array =
+        (Array.isArray(prop)
+          ? prop
+          : wikiData[prop]);
+
+      logInfo` - ${array?.length ?? colors.red('(Missing!)')} ${colors.normal(colors.dim(label))}`;
+    }
 
-async function writeFavicon() {
     try {
-        await stat(path.join(mediaPath, FAVICON_FILE));
+      logInfo`Loaded data and processed objects:`;
+      logThings('albumData', 'albums');
+      logThings('trackData', 'tracks');
+      logThings(wikiData.artistData.filter(artist => !artist.isAlias), 'artists');
+      if (wikiData.flashData) {
+        logThings('flashData', 'flashes');
+        logThings('flashActData', 'flash acts');
+        logThings('flashSideData', 'flash sides');
+      }
+      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) {
-        return;
+      console.error(`Error showing data summary:`, error);
     }
 
+    let errorless = true;
     try {
-        await copyFile(
-            path.join(mediaPath, FAVICON_FILE),
-            path.join(outputPath, FAVICON_FILE)
-        );
+      processDataAggregate.close();
     } catch (error) {
-        logWarn`Failed to copy favicon! ${error.message}`;
-        return;
+      niceShowAggregate(error);
+      logWarn`The above errors were detected while processing data files.`;
+      errorless = false;
     }
 
-    logInfo`Copied favicon to site root.`;
-}
+    if (!wikiData.wikiInfo) {
+      logError`Can't proceed without wiki info file successfully loading`;
 
-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');
-            }
-        }
+      Object.assign(stepStatusSummary.loadDataFiles, {
+        status: STATUS_FATAL_ERROR,
+        annotation: `wiki info object not available`,
+        timeEnd: Date.now(),
+      });
+
+      return false;
     }
-}
 
-function writeSharedFilesAndPages({language, wikiData}) {
-    const { groupData, wikiInfo } = wikiData;
+    if (errorless) {
+      logInfo`All data files processed without any errors - nice!`;
 
-    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);
+      Object.assign(stepStatusSummary.loadDataFiles, {
+        status: STATUS_DONE_CLEAN,
+        timeEnd: Date.now(),
+      });
+    } else {
+      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!)`;
+
+      Object.assign(stepStatusSummary.loadDataFiles, {
+        status: STATUS_HAS_WARNINGS,
+        annotation: `view log for details`,
+        timeEnd: Date.now(),
+      });
+    }
+  }
+
+  // Link data arrays so that all essential references between objects are
+  // complete, so properties (like dates!) are inherited where that's
+  // appropriate.
+
+  Object.assign(stepStatusSummary.linkWikiDataArrays, {
+    status: STATUS_STARTED_NOT_DONE,
+    timeStart: Date.now(),
+  });
+
+  linkWikiDataArrays(wikiData);
+
+  Object.assign(stepStatusSummary.linkWikiDataArrays, {
+    status: STATUS_DONE_CLEAN,
+    timeEnd: Date.now(),
+  });
+
+  if (precacheMode === 'common') {
+    Object.assign(stepStatusSummary.precacheCommonData, {
+      status: STATUS_STARTED_NOT_DONE,
+      timeStart: Date.now(),
+    });
+
+    const commonDataMap = {
+      albumData: new Set([
+        // Needed for sorting
+        'date', 'tracks',
+        // Needed for computing page paths
+        'aliasedArtist', 'commentary', 'coverArtistContribs',
+      ]),
+
+      artTagData: new Set([
+        // Needed for computing page paths
+        'isContentWarning',
+      ]),
+
+      flashData: new Set([
+        // Needed for sorting
+        'act', 'date',
+      ]),
+
+      flashActData: new Set([
+        // Needed for sorting
+        'flashes',
+      ]),
+
+      groupData: new Set([
+        // Needed for computing page paths
+        'albums',
+      ]),
+
+      listingSpec: new Set([
+        // Needed for computing page paths
+        'contentFunction', 'featureFlag',
+      ]),
+
+      trackData: new Set([
+        // Needed for sorting
+        'album', 'date',
+        // Needed for computing page paths
+        'commentary', 'coverArtistContribs',
+      ]),
     };
 
-    return progressPromiseAll(`Writing files & pages shared across languages.`, [
-        groupData?.some(group => group.directory === 'fandom') &&
-        redirect('Fandom - Gallery', 'albums/fandom', 'localized.groupGallery', 'fandom'),
+    for (const [wikiDataKey, properties] of Object.entries(commonDataMap)) {
+      const thingData = wikiData[wikiDataKey];
+      const allProperties = new Set(['name', 'directory', ...properties]);
+      for (const thing of thingData) {
+        for (const property of allProperties) {
+          void thing[property];
+        }
+      }
+    }
 
-        groupData?.some(group => group.directory === 'official') &&
-        redirect('Official - Gallery', 'albums/official', 'localized.groupGallery', 'official'),
+    Object.assign(stepStatusSummary.precacheCommonData, {
+      status: STATUS_DONE_CLEAN,
+      timeEnd: Date.now(),
+    });
+  }
 
-        wikiInfo.enableListings &&
-        redirect('Album Commentary', 'list/all-commentary', 'localized.commentaryIndex', ''),
+  // Filter out any things with duplicate directories throughout the data,
+  // warning about them too.
 
-        writeFile(path.join(outputPath, 'data.json'), fixWS`
-            {
-                "albumData": ${stringifyThings(wikiData.albumData)},
-                ${wikiInfo.enableFlashesAndGames && `"flashData": ${stringifyThings(wikiData.flashData)},`}
-                "artistData": ${stringifyThings(wikiData.artistData)}
-            }
-        `)
-    ].filter(Boolean));
-}
+  Object.assign(stepStatusSummary.reportDuplicateDirectories, {
+    status: STATUS_STARTED_NOT_DONE,
+    timeStart: Date.now(),
+  });
 
-function generateRedirectPage(title, target, {language}) {
-    return fixWS`
-        <!DOCTYPE html>
-        <html>
-            <head>
-                <title>${language.$('redirectPage.title', {title})}</title>
-                <meta charset="utf-8">
-                <meta http-equiv="refresh" content="0;url=${target}">
-                <link rel="canonical" href="${target}">
-                <link rel="stylesheet" href="static/site-basic.css">
-            </head>
-            <body>
-                <main>
-                    <h1>${language.$('redirectPage.title', {title})}</h1>
-                    <p>${language.$('redirectPage.infoLine', {
-                        target: `<a href="${target}">${target}</a>`
-                    })}</p>
-                </main>
-            </body>
-        </html>
-    `;
-}
+  try {
+    reportDuplicateDirectories(wikiData, {getAllFindSpecs});
+    logInfo`No duplicate directories found - nice!`;
 
-// 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'
-    )
-}
+    Object.assign(stepStatusSummary.reportDuplicateDirectories, {
+      status: STATUS_DONE_CLEAN,
+      timeEnd: Date.now(),
+    });
+  } catch (aggregate) {
+    niceShowAggregate(aggregate);
+
+    logWarn`The above duplicate directories were detected while reviewing data files.`;
+    logWarn`Since it's impossible to automatically determine which one's directory is`;
+    logWarn`correct, the build can't continue. Specify unique 'Directory' fields in`;
+    logWarn`some or all of these data entries to resolve the errors.`;
+
+    Object.assign(stepStatusSummary.reportDuplicateDirectories, {
+      status: STATUS_FATAL_ERROR,
+      annotation: `duplicate directories found`,
+      timeEnd: Date.now(),
+    });
 
-async function processLanguageFile(file) {
-    const contents = await readFile(file, 'utf-8');
-    const json = JSON.parse(contents);
+    return false;
+  }
 
-    const code = json['meta.languageCode'];
-    if (!code) {
-        throw new Error(`Missing language code (file: ${file})`);
-    }
-    delete json['meta.languageCode'];
+  // Filter out any reference errors throughout the data, warning about them
+  // too.
 
-    const intlCode = json['meta.languageIntlCode'] ?? null;
-    delete json['meta.languageIntlCode'];
+  if (stepStatusSummary.filterReferenceErrors.status === STATUS_NOT_STARTED) {
+    Object.assign(stepStatusSummary.filterReferenceErrors, {
+      status: STATUS_STARTED_NOT_DONE,
+      timeStart: Date.now(),
+    });
 
-    const name = json['meta.languageName'];
-    if (!name) {
-        throw new Error(`Missing language name (${code})`);
-    }
-    delete json['meta.languageName'];
+    const filterReferenceErrorsAggregate =
+      filterReferenceErrors(wikiData, {bindFind});
+
+    try {
+      filterReferenceErrorsAggregate.close();
+
+      logInfo`All references validated without any errors - nice!`;
 
-    const hidden = json['meta.hidden'] ?? false;
-    delete json['meta.hidden'];
+      Object.assign(stepStatusSummary.filterReferenceErrors, {
+        status: STATUS_DONE_CLEAN,
+        timeEnd: Date.now(),
+      });
+    } catch (error) {
+      niceShowAggregate(error);
 
-    if (json['meta.baseDirectory']) {
-        logWarn`(${code}) Language JSON still has unused meta.baseDirectory`;
-        delete json['meta.baseDirectory'];
+      logWarn`The above errors were detected while validating references in data files.`;
+      logWarn`The wiki will still build, but these connections between data objects`;
+      logWarn`will be completely skipped. Resolve the errors for more complete output.`;
+
+      Object.assign(stepStatusSummary.filterReferenceErrors, {
+        status: STATUS_HAS_WARNINGS,
+        annotation: `view log for details`,
+        timeEnd: Date.now(),
+      });
     }
+  }
 
-    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;
-}
+  if (stepStatusSummary.reportContentTextErrors.status === STATUS_NOT_STARTED) {
+    Object.assign(stepStatusSummary.reportContentTextErrors, {
+      status: STATUS_STARTED_NOT_DONE,
+      timeStart: Date.now(),
+    });
 
-// 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);
+    try {
+      reportContentTextErrors(wikiData, {bindFind});
+      logInfo`All content text validated without any errors - nice!`;
 
-    const entries = Object.entries(languagesToRun)
-        .filter(([ key ]) => key !== 'default');
+      Object.assign(stepStatusSummary.reportContentTextErrors, {
+        status: STATUS_DONE_CLEAN,
+        timeEnd: Date.now(),
+      });
+    } catch (error) {
+      niceShowAggregate(error);
 
-    for (let i = 0; i < entries.length; i++) {
-        const [ key, language ] = entries[i];
+      logWarn`The above errors were detected while processing content text in data files.`;
+      logWarn`The wiki will still build, but placeholders will be displayed in these spots.`;
+      logWarn`Resolve the errors for more complete output.`;
 
-        await fn(language, i, entries);
+      Object.assign(stepStatusSummary.reportContentTextErrors, {
+        status: STATUS_HAS_WARNINGS,
+        annotation: `view log for details`,
+        timeEnd: Date.now(),
+      });
     }
-}
+  }
 
-async function main() {
-    Error.stackTraceLimit = Infinity;
-
-    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'
-        },
-
-        // 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'
-        },
-
-        // 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'
-        },
-
-        // 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'
-        },
-
-        // 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'
-        },
-
-        // 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'
-        },
-
-        // 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'
-        },
-
-        // 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'
-        },
-
-        // 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'
-        },
-
-        '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'},
-
-        // 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'
-        },
-
-        [parseOptions.handleUnknown]: () => {}
+  // Sort data arrays so that they're all in order! This may use properties
+  // which are only available after the initial linking.
+
+  Object.assign(stepStatusSummary.sortWikiDataArrays, {
+    status: STATUS_STARTED_NOT_DONE,
+    timeStart: Date.now(),
+  });
+
+  sortWikiDataArrays(wikiData);
+
+  Object.assign(stepStatusSummary.sortWikiDataArrays, {
+    status: STATUS_DONE_CLEAN,
+    timeEnd: Date.now(),
+  });
+
+  if (precacheMode === 'all') {
+    Object.assign(stepStatusSummary.precacheAllData, {
+      status: STATUS_STARTED_NOT_DONE,
+      timeStart: Date.now(),
     });
 
-    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;
+    // TODO: Aggregate errors here, instead of just throwing.
+    progressCallAll('Caching all data values', Object.entries(wikiData)
+      .filter(([key]) =>
+        key !== 'listingSpec' &&
+        key !== 'listingTargetSpec')
+      .map(([key, value]) =>
+        key === 'wikiInfo' ? [key, [value]] :
+        key === 'homepageLayout' ? [key, [value]] :
+        [key, value])
+      .flatMap(([_key, things]) => things)
+      .map(thing => () => CacheableObject.cacheAllExposedProperties(thing)));
+
+    Object.assign(stepStatusSummary.precacheAllData, {
+      status: STATUS_DONE_CLEAN,
+      timeEnd: Date.now(),
+    });
+  }
 
-    const writeOneLanguage = miscOptions['lang'];
+  if (stepStatusSummary.performBuild.status === STATUS_NOT_APPLICABLE) {
+    displayCompositeCacheAnalysis();
 
-    {
-        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;
-        }
+    if (precacheMode === 'all') {
+      return true;
     }
+  }
 
-    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 languageReloading =
+    stepStatusSummary.watchLanguageFiles.status === STATUS_NOT_STARTED;
 
-    const skipThumbs = miscOptions['skip-thumbs'] ?? false;
-    const thumbsOnly = miscOptions['thumbs-only'] ?? false;
-    const noBuild = miscOptions['no-build'] ?? false;
-    const showAggregateTraces = miscOptions['show-traces'] ?? false;
+  Object.assign(stepStatusSummary.loadInternalDefaultLanguage, {
+    status: STATUS_STARTED_NOT_DONE,
+    timeStart: Date.now(),
+  });
 
-    const niceShowAggregate = (error, ...opts) => {
-        showAggregate(error, {
-            showTraces: showAggregateTraces,
-            pathToFile: f => path.relative(__dirname, f),
-            ...opts
-        });
-    };
+  let internalDefaultLanguage;
+  let internalDefaultLanguageWatcher;
 
-    if (skipThumbs && thumbsOnly) {
-        logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`;
-        return;
-    }
+  let errorLoadingInternalDefaultLanguage = false;
 
-    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;
-    }
+  if (languageReloading) {
+    internalDefaultLanguageWatcher = watchLanguageFile(internalDefaultStringsFile);
 
-    const showInvalidPropertyAccesses = miscOptions['show-invalid-property-accesses'] ?? false;
+    try {
+      await new Promise((resolve, reject) => {
+        const watcher = internalDefaultLanguageWatcher;
 
-    if (showInvalidPropertyAccesses) {
-        CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true;
-    }
+        const onReady = () => {
+          watcher.removeListener('ready', onReady);
+          watcher.removeListener('error', onError);
+          resolve();
+        };
 
-    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 onError = error => {
+          watcher.removeListener('ready', onReady);
+          watcher.removeListener('error', onError);
+          watcher.close();
+          reject(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;
-        }
+        watcher.on('ready', onReady);
+        watcher.on('error', onError);
+      });
 
-        if (errorless) {
-            logInfo`All data processed without any errors - nice!`;
-            logInfo`(This means all source files will be fully accounted for during page generation.)`;
-        }
+      internalDefaultLanguage = internalDefaultLanguageWatcher.language;
+    } catch (_error) {
+      // No need to display the error here - it's already printed by
+      // watchLanguageFile.
+      errorLoadingInternalDefaultLanguage = true;
     }
+  } else {
+    internalDefaultLanguageWatcher = null;
 
-    if (!WD.wikiInfo) {
-        logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`;
-        return;
+    try {
+      internalDefaultLanguage = await processLanguageFile(internalDefaultStringsFile);
+    } catch (error) {
+      niceShowAggregate(error);
+      errorLoadingInternalDefaultLanguage = true;
     }
+  }
 
-    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!`;
-        }
-    }
+  if (errorLoadingInternalDefaultLanguage) {
+    logError`There was an error reading the internal language file.`;
+    fileIssue();
 
-    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.)`;
-        }
+    Object.assign(stepStatusSummary.loadInternalDefaultLanguage, {
+      status: STATUS_FATAL_ERROR,
+      annotation: `see log for details`,
+      timeEnd: Date.now(),
+    });
+
+    return false;
+  }
+
+  if (languageReloading) {
+    // Bypass node.js special-case handling for uncaught error events
+    internalDefaultLanguageWatcher.on('error', () => {});
+  }
+
+  Object.assign(stepStatusSummary.loadInternalDefaultLanguage, {
+    status: STATUS_DONE_CLEAN,
+    timeEnd: Date.now(),
+  });
+
+  let customLanguageWatchers;
+  let languages;
+
+  if (langPath) {
+    if (languageReloading) {
+      Object.assign(stepStatusSummary.watchLanguageFiles, {
+        status: STATUS_STARTED_NOT_DONE,
+        timeStart: Date.now(),
+      });
+    } else {
+      Object.assign(stepStatusSummary.loadLanguageFiles, {
+        status: STATUS_STARTED_NOT_DONE,
+        timeStart: Date.now(),
+      });
     }
 
-    // Link data arrays so that all essential references between objects are
-    // complete, so properties (like dates!) are inherited where that's
-    // appropriate.
-    linkWikiDataArrays(wikiData);
+    const languageDataFiles =
+      (await readdir(langPath))
+        .filter(name => ['.json', '.yaml'].includes(path.extname(name)))
+        .map(name => path.join(langPath, name));
 
-    // Filter out any things with duplicate directories throughout the data,
-    // warning about them too.
-    filterAndShowDuplicateDirectories();
+    let errorLoadingCustomLanguages = false;
 
-    // Filter out any reference errors throughout the data, warning about them
-    // too.
-    filterAndShowReferenceErrors();
+    if (languageReloading) watchCustomLanguages: {
+      Object.assign(stepStatusSummary.watchLanguageFiles, {
+        status: STATUS_STARTED_NOT_DONE,
+        timeStart: Date.now(),
+      });
 
-    // Sort data arrays so that they're all in order! This may use properties
-    // which are only available after the initial linking.
-    sortWikiDataArrays(wikiData);
+      customLanguageWatchers =
+        languageDataFiles.map(file => {
+          const watcher = watchLanguageFile(file);
 
-    const internalDefaultLanguage = await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE));
+          // Bypass node.js special-case handling for uncaught error events
+          watcher.on('error', () => {});
 
-    let languages;
-    if (langPath) {
-        const languageDataFiles = await findFiles(langPath, {
-            filter: f => path.extname(f) === '.json'
+          return watcher;
         });
 
-        const results = await progressPromiseAll(`Reading & processing language files.`, languageDataFiles
-            .map(file => processLanguageFile(file)));
+      const waitingOnWatchers = new Set(customLanguageWatchers);
+
+      const initialResults =
+        await Promise.allSettled(
+          customLanguageWatchers
+            .map(watcher => new Promise((resolve, reject) => {
+              const onReady = () => {
+                watcher.removeListener('ready', onReady);
+                watcher.removeListener('error', onError);
+                waitingOnWatchers.delete(watcher);
+                resolve();
+              };
+
+              const onError = error => {
+                watcher.removeListener('ready', onReady);
+                watcher.removeListener('error', onError);
+                reject(error);
+              };
+
+              watcher.on('ready', onReady);
+              watcher.on('error', onError);
+            })));
+
+      if (initialResults.some(({status}) => status === 'rejected')) {
+        logWarn`There were errors loading custom languages from the language path`;
+        logWarn`provided: ${langPath}`;
+
+        if (noInput) {
+          internalDefaultLanguageWatcher.close();
+
+          for (const watcher of Object.values(customLanguageWatchers)) {
+            watcher.close();
+          }
+
+          Object.assign(stepStatusSummary.watchLanguageFiles, {
+            status: STATUS_FATAL_ERROR,
+            annotation: `see log for details`,
+            timeEnd: Date.now(),
+          });
+
+          errorLoadingCustomLanguages = true;
+          break watchCustomLanguages;
+        }
+
+        logWarn`The build should start automatically if you investigate these.`;
+        logWarn`Or, exit by pressing ^C here (control+C) and run again without`;
+        logWarn`providing ${'--lang-path'} (or ${'HSMUSIC_LANG'}) to build without custom`;
+        logWarn`languages.`;
+
+        await new Promise(resolve => {
+          for (const watcher of waitingOnWatchers) {
+            watcher.once('ready', () => {
+              waitingOnWatchers.remove(watcher);
+              if (empty(waitingOnWatchers)) {
+                resolve();
+              }
+            });
+          }
+        });
+      }
+
+      languages =
+        Object.fromEntries(
+          customLanguageWatchers
+            .map(({language}) => [language.code, language]));
 
-        languages = Object.fromEntries(results.map(language => [language.code, language]));
+      Object.assign(stepStatusSummary.watchLanguageFiles, {
+        status: STATUS_DONE_CLEAN,
+        timeEnd: Date.now(),
+      });
     } else {
-        languages = {};
-    }
+      languages = {};
+
+      const results =
+        await Promise.allSettled(
+          languageDataFiles
+            .map(file => processLanguageFile(file)));
 
-    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}?`;
+      for (const {status, value: language, reason: error} of results) {
+        if (status === 'rejected') {
+          errorLoadingCustomLanguages = true;
+          niceShowAggregate(error);
         } else {
-            logError`Be sure to specify ${'--lang'} or ${'HSMUSIC_LANG'} with the path to language files.`;
+          languages[language.code] = language;
         }
-        return;
-    } else {
-        languages[internalDefaultLanguage.code] = internalDefaultLanguage;
-        finalDefaultLanguage = internalDefaultLanguage;
+      }
+
+      if (errorLoadingCustomLanguages) {
+        Object.assign(stepStatusSummary.loadLanguageFiles, {
+          status: STATUS_FATAL_ERROR,
+          annotation: `see log for details`,
+          timeEnd: Date.now(),
+        });
+      } else {
+        Object.assign(stepStatusSummary.loadLanguageFiles, {
+          status: STATUS_DONE_CLEAN,
+          timeEnd: Date.now(),
+        });
+      }
     }
 
-    for (const language of Object.values(languages)) {
-        if (language === finalDefaultLanguage) {
-            continue;
-        }
+    if (errorLoadingCustomLanguages) {
+      logError`Failed to load language files. Please investigate these, or don't provide`;
+      logError`--lang-path (or HSMUSIC_LANG) and build again.`;
+      return false;
+    }
+  } else {
+    languages = {};
+  }
+
+  Object.assign(stepStatusSummary.initializeDefaultLanguage, {
+    status: STATUS_STARTED_NOT_DONE,
+    timeStart: Date.now(),
+  });
+
+  let finalDefaultLanguage;
+  let finalDefaultLanguageWatcher;
+  let finalDefaultLanguageAnnotation;
+
+  if (wikiData.wikiInfo.defaultLanguage) {
+    const customDefaultLanguage = languages[wikiData.wikiInfo.defaultLanguage];
+
+    if (!customDefaultLanguage) {
+      logError`Wiki info file specified default language is ${wikiData.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-path'} or ${'HSMUSIC_LANG'} with the path to language files.`;
+      }
+
+      Object.assign(stepStatusSummary.initializeDefaultLanguage, {
+        status: STATUS_FATAL_ERROR,
+        annotation: `wiki specifies default language whose file is not available`,
+        timeEnd: Date.now(),
+      });
+
+      return false;
+    }
+
+    logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`;
+
+    finalDefaultLanguage = customDefaultLanguage;
+    finalDefaultLanguageAnnotation = `using wiki-specified custom default language`;
 
-        language.inheritedStrings = finalDefaultLanguage.strings;
+    if (languageReloading) {
+      finalDefaultLanguageWatcher =
+        customLanguageWatchers
+          .find(({language}) => language === customDefaultLanguage);
     }
+  } else if (languages[internalDefaultLanguage.code]) {
+    const customDefaultLanguage = languages[internalDefaultLanguage.code];
 
-    logInfo`Loaded language strings: ${Object.keys(languages).join(', ')}`;
+    finalDefaultLanguage = customDefaultLanguage;
+    finalDefaultLanguageAnnotation = `using inferred custom default language`;
 
-    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 (languageReloading) {
+      finalDefaultLanguageWatcher =
+        customLanguageWatchers
+          .find(({language}) => language === customDefaultLanguage);
     }
+  } else {
+    languages[internalDefaultLanguage.code] = internalDefaultLanguage;
 
-    {
-        const tagRefs = new Set([...WD.trackData, ...WD.albumData].flatMap(thing => thing.artTagsByRef ?? []));
+    finalDefaultLanguage = internalDefaultLanguage;
+    finalDefaultLanguageAnnotation = `no custom default language specified`;
 
-        for (const ref of tagRefs) {
-            if (find.artTag(ref, WD.artTagData)) {
-                tagRefs.delete(ref);
-            }
-        }
+    if (languageReloading) {
+      finalDefaultLanguageWatcher = internalDefaultLanguageWatcher;
+    }
+  }
+
+  const closeLanguageWatchers = () => {
+    if (languageReloading) {
+      for (const watcher of [
+        internalDefaultLanguageWatcher,
+        ...customLanguageWatchers,
+      ]) {
+        watcher.close();
+      }
+    }
+  };
+
+  const inheritStringsFromInternalLanguage = () => {
+    // The custom default language, if set, will be the new one providing fallback
+    // strings for other languages. But on its own, it still might not be a complete
+    // list of strings - so it falls back to the internal default language, which
+    // won't otherwise be presented in the build.
+    if (finalDefaultLanguage === internalDefaultLanguage) return;
+    const {strings: inheritedStrings} = internalDefaultLanguage;
+    Object.assign(finalDefaultLanguage, {inheritedStrings});
+  };
+
+  const inheritStringsFromDefaultLanguage = () => {
+    const {strings: inheritedStrings} = finalDefaultLanguage;
+    for (const language of Object.values(languages)) {
+      if (language === finalDefaultLanguage) continue;
+      Object.assign(language, {inheritedStrings});
+    }
+  };
 
-        if (tagRefs.size) {
-            for (const ref of Array.from(tagRefs).sort()) {
-                console.log(`\x1b[33;1m- Missing tag: "${ref}"\x1b[0m`);
-            }
-            return;
-        }
+  if (finalDefaultLanguage !== internalDefaultLanguage) {
+    inheritStringsFromInternalLanguage();
+  }
+
+  inheritStringsFromDefaultLanguage();
+
+  if (languageReloading) {
+    if (finalDefaultLanguage !== internalDefaultLanguage) {
+      internalDefaultLanguageWatcher.on('update', () => {
+        inheritStringsFromInternalLanguage();
+        inheritStringsFromDefaultLanguage();
+      });
     }
 
-    WD.justEverythingMan = sortByDate([...WD.albumData, ...WD.trackData, ...(WD.flashData || [])]);
-    WD.justEverythingSortedByArtDateMan = sortByArtDate(WD.justEverythingMan.slice());
-    // console.log(JSON.stringify(justEverythingSortedByArtDateMan.map(toAnythingMan), null, 2));
+    finalDefaultLanguageWatcher.on('update', () => {
+      inheritStringsFromDefaultLanguage();
+    });
+  }
+
+  logInfo`Loaded language strings: ${Object.keys(languages).join(', ')}`;
+
+  Object.assign(stepStatusSummary.initializeDefaultLanguage, {
+    status: STATUS_DONE_CLEAN,
+    annotation: finalDefaultLanguageAnnotation,
+    timeEnd: Date.now(),
+  });
 
-    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 urls = generateURLs(urlSpec);
+
+  let missingImagePaths;
+
+  if (stepStatusSummary.verifyImagePaths.status === STATUS_NOT_APPLICABLE) {
+    missingImagePaths = [];
+  } else if (stepStatusSummary.verifyImagePaths.status === STATUS_NOT_STARTED) {
+    Object.assign(stepStatusSummary.verifyImagePaths, {
+      status: STATUS_STARTED_NOT_DONE,
+      timeStart: Date.now(),
+    });
+
+    const results =
+      await verifyImagePaths(mediaPath, {urls, wikiData});
+
+    missingImagePaths = results.missing;
+    const misplacedImagePaths = results.misplaced;
+
+    if (empty(missingImagePaths) && empty(misplacedImagePaths)) {
+      Object.assign(stepStatusSummary.verifyImagePaths, {
+        status: STATUS_DONE_CLEAN,
+        timeEnd: Date.now(),
+      });
+    } else if (empty(missingImagePaths)) {
+      Object.assign(stepStatusSummary.verifyImagePaths, {
+        status: STATUS_HAS_WARNINGS,
+        annotation: `misplaced images detected`,
+        timeEnd: Date.now(),
+      });
+    } else if (empty(misplacedImagePaths)) {
+      Object.assign(stepStatusSummary.verifyImagePaths, {
+        status: STATUS_HAS_WARNINGS,
+        annotation: `missing images detected`,
+        timeEnd: Date.now(),
+      });
+    } else {
+      Object.assign(stepStatusSummary.verifyImagePaths, {
+        status: STATUS_HAS_WARNINGS,
+        annotation: `missing and misplaced images detected`,
+        timeEnd: Date.now(),
+      });
+    }
+  }
+
+  let getSizeOfAdditionalFile;
+  let getSizeOfImagePath;
+
+  if (stepStatusSummary.preloadFileSizes.status === STATUS_NOT_APPLICABLE) {
+    getSizeOfAdditionalFile = () => null;
+    getSizeOfImagePath = () => null;
+  } else if (stepStatusSummary.preloadFileSizes.status === STATUS_NOT_STARTED) {
+    Object.assign(stepStatusSummary.preloadFileSizes, {
+      status: STATUS_STARTED_NOT_DONE,
+      timeStart: Date.now(),
+    });
 
     const fileSizePreloader = new FileSizePreloader();
 
@@ -1755,479 +1676,338 @@ async function main() {
     // 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))
-            })))),
+      ...wikiData.albumData.flatMap((album) =>
+        [
+          ...(album.additionalFiles ?? []),
+          ...album.tracks.flatMap((track) => [
+            ...(track.additionalFiles ?? []),
+            ...(track.sheetMusicFiles ?? []),
+            ...(track.midiProjectFiles ?? []),
+          ]),
+        ]
+          .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);
+    // Same dealio for images. Since just about any image can be embedded and
+    // we can't super easily know which ones are referenced at runtime, just
+    // cheat and get file sizes for all images under media. (This includes
+    // additional files which are images.)
+    const imageFilePaths =
+      await traverse(mediaPath, {
+        pathStyle: 'device',
+        filterDir: dir => dir !== '.git',
+        filterFile: file =>
+          ['.png', '.gif', '.jpg'].includes(path.extname(file)) &&
+          !isThumb(file),
+      }).then(files => files
+          .map(file => ({
+            device: file,
+            media:
+              urls
+                .from('media.root')
+                .to('media.path', path.relative(mediaPath, file).split(path.sep).join('/')),
+          })));
+
+    const getSizeOfMediaFileHelper = paths => (mediaPath) => {
+      const pair = paths.find(({media}) => media === mediaPath);
+      if (!pair) return null;
+      return fileSizePreloader.getSizeOfPath(pair.device);
     };
 
+    getSizeOfAdditionalFile = getSizeOfMediaFileHelper(additionalFilePaths);
+    getSizeOfImagePath = getSizeOfMediaFileHelper(imageFilePaths);
+
     logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`;
 
-    fileSizePreloader.loadPaths(...additionalFilePaths.map(path => path.device));
+    fileSizePreloader.loadPaths(...additionalFilePaths.map((path) => path.device));
+    await fileSizePreloader.waitUntilDoneLoading();
+
+    logInfo`Preloading filesizes for ${imageFilePaths.length} full-resolution images...`;
+
+    fileSizePreloader.loadPaths(...imageFilePaths.map((path) => path.device));
     await fileSizePreloader.waitUntilDoneLoading();
 
-    logInfo`Done preloading filesizes!`;
+    if (fileSizePreloader.hasErrored) {
+      logWarn`Some media files couldn't be read for preloading filesizes.`;
+      logWarn`This means the wiki won't display file sizes for these files.`;
+      logWarn`Investigate missing or unreadable files to get that fixed!`;
 
-    if (noBuild) return;
+      Object.assign(stepStatusSummary.preloadFileSizes, {
+        status: STATUS_HAS_WARNINGS,
+        annotation: `see log for details`,
+        timeEnd: Date.now(),
+      });
+    } else {
+      logInfo`Done preloading filesizes without any errors - nice!`;
 
-    // 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);
+      Object.assign(stepStatusSummary.preloadFileSizes, {
+        status: STATUS_DONE_CLEAN,
+        timeEnd: Date.now(),
+      });
+    }
+  }
+
+  if (stepStatusSummary.performBuild.status === STATUS_NOT_APPLICABLE) {
+    return true;
+  }
+
+  const developersComment =
+    `<!--\n` + [
+      wikiData.wikiInfo.canonicalBase
+        ? `hsmusic.wiki - ${wikiData.wikiInfo.name}, ${wikiData.wikiInfo.canonicalBase}`
+        : `hsmusic.wiki - ${wikiData.wikiInfo.name}`,
+      'Code copyright 2019-2023 Quasar Nebula et al (MIT License)',
+      ...wikiData.wikiInfo.canonicalBase === 'https://hsmusic.wiki/' ? [
+        'Data avidly compiled and localization brought to you',
+        'by our awesome team and community of wiki contributors',
+        '***',
+        'Want to contribute? Join our Discord or leave feedback!',
+        '- https://hsmusic.wiki/discord/',
+        '- https://hsmusic.wiki/feedback/',
+        '- https://github.com/hsmusic/',
+      ] : [
+        'https://github.com/hsmusic/',
+      ],
+      '***',
+      BUILD_TIME &&
+        `Site built: ${BUILD_TIME.toLocaleString('en-US', {
+          dateStyle: 'long',
+          timeStyle: 'long',
+        })}`,
+      COMMIT &&
+        `Latest code commit: ${COMMIT}`,
+    ]
+      .filter(Boolean)
+      .map(line => `    ` + line)
+      .join('\n') + `\n-->`;
+
+  Object.assign(stepStatusSummary.performBuild, {
+    status: STATUS_STARTED_NOT_DONE,
+    timeStart: Date.now(),
+  });
+
+  let buildModeResult;
+
+  try {
+    buildModeResult = await selectedBuildMode.go({
+      cliOptions,
+      dataPath,
+      mediaPath,
+      mediaCachePath,
+      queueSize,
+      srcRootPath: __dirname,
+
+      defaultLanguage: finalDefaultLanguage,
+      languages,
+      missingImagePaths,
+      thumbsCache,
+      urls,
+      urlSpec,
+      wikiData,
+
+      cachebust: '?' + CACHEBUST,
+      closeLanguageWatchers,
+      developersComment,
+      getSizeOfAdditionalFile,
+      getSizeOfImagePath,
+      niceShowAggregate,
+    });
+  } catch (error) {
+    console.error(error);
 
-    const buildDictionary = pageSpecs;
+    logError`There was a JavaScript error performing the build.`;
+    fileIssue();
 
-    // 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.
+    Object.assign(stepStatusSummary.performBuild, {
+      status: STATUS_FATAL_ERROR,
+      message: `javascript error - view log for details`,
+      timeEnd: Date.now(),
+    });
 
-        // Kinda a hack t8h!
-        ...Object.fromEntries(Object.keys(buildDictionary)
-            .map(key => [key, {type: 'flag'}])),
+    return false;
+  }
 
-        [parseOptions.handleUnknown]: () => {}
+  if (buildModeResult !== true) {
+    Object.assign(stepStatusSummary.performBuild, {
+      status: STATUS_HAS_WARNINGS,
+      annotation: `may not have completed - view log for details`,
+      timeEnd: Date.now(),
     });
 
-    const writeAll = !Object.keys(writeFlags).length || writeFlags.all;
+    return false;
+  }
 
-    logInfo`Writing site pages: ${writeAll ? 'all' : Object.keys(writeFlags).join(', ')}`;
+  Object.assign(stepStatusSummary.performBuild, {
+    status: STATUS_DONE_CLEAN,
+    timeEnd: Date.now(),
+  });
 
-    await writeFavicon();
-    await writeSymlinks();
-    await writeSharedFilesAndPages({language: finalDefaultLanguage, wikiData});
+  return true;
+}
 
-    const buildSteps = (writeAll
-        ? Object.entries(buildDictionary)
-        : (Object.entries(buildDictionary)
-            .filter(([ flag ]) => writeFlags[flag])));
+// TODO: isMain detection isn't consistent across platforms here
+/* eslint-disable-next-line no-constant-condition */
+if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmusic') {
+  (async () => {
+    let result;
 
-    let writes;
-    {
-        let error = false;
+    const totalTimeStart = Date.now();
 
-        const buildStepsWithTargets = buildSteps.map(([ flag, pageSpec ]) => {
-            // Condition not met: skip this build step altogether.
-            if (pageSpec.condition && !pageSpec.condition({wikiData})) {
-                return null;
-            }
+    try {
+      result = await main();
+    } catch (error) {
+      if (error instanceof AggregateError) {
+        showAggregate(error);
+      } else if (error.cause) {
+        console.error(error);
+        showAggregate(error);
+      } else {
+        console.error(error);
+      }
+    }
 
-            // May still call writeTargetless if present.
-            if (!pageSpec.targets) {
-                return {flag, pageSpec, targets: []};
-            }
+    const totalTimeEnd = Date.now();
 
-            if (!pageSpec.write) {
-                logError`${flag + '.targets'} is specified, but ${flag + '.write'} is missing!`;
-                error = true;
-                return null;
-            }
+    const formatDuration = timeDelta => {
+      const seconds = timeDelta / 1000;
 
-            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;
-            }
+      if (seconds > 90) {
+        const modSeconds = Math.floor(seconds % 60);
+        const minutes = Math.floor(seconds - seconds % 60) / 60;
+        return `${minutes}m${modSeconds}s`;
+      }
 
-            return {flag, pageSpec, targets};
-        }).filter(Boolean);
+      if (seconds < 0.1) {
+        return 'instant';
+      }
 
-        if (error) {
-            return;
-        }
+      const precision = (seconds > 1 ? 3 : 2);
+      return `${seconds.toPrecision(precision)}s`;
+    };
 
-        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;
-        };
+    if (showStepStatusSummary) {
+      const totalDuration = formatDuration(totalTimeEnd - totalTimeStart);
 
-        // return;
+      console.error(colors.bright(`Step summary:`));
 
-        writes = buildStepsWithTargets.flatMap(({ flag, pageSpec, targets }) => {
-            const writes = targets.flatMap(target =>
-                pageSpec.write(target, {wikiData})?.slice() || []);
+      const longestNameLength =
+        Math.max(...
+          Object.values(stepStatusSummary)
+            .map(({name}) => name.length));
 
-            if (!validateWrites(writes, flag + '.write')) {
-                return [];
-            }
+      const stepsNotClean =
+        Object.values(stepStatusSummary)
+          .map(({status}) =>
+            status === STATUS_HAS_WARNINGS ||
+            status === STATUS_FATAL_ERROR ||
+            status === STATUS_STARTED_NOT_DONE);
 
-            if (pageSpec.writeTargetless) {
-                const writes2 = pageSpec.writeTargetless({wikiData});
+      const anyStepsNotClean =
+        stepsNotClean.includes(true);
 
-                if (!validateWrites(writes2, flag + '.writeTargetless')) {
-                    return [];
-                }
+      const stepDetails = Object.values(stepStatusSummary);
 
-                writes.push(...writes2);
-            }
+      const stepDurations =
+        stepDetails.map(({status, timeStart, timeEnd}) => {
+          if (
+            status === STATUS_NOT_APPLICABLE ||
+            status === STATUS_NOT_STARTED ||
+            status === STATUS_STARTED_NOT_DONE
+          ) {
+            return '-';
+          }
 
-            return writes;
-        });
+          if (typeof timeStart !== 'number' || typeof timeEnd !== 'number') {
+            return 'unknown';
+          }
 
-        if (error) {
-            return;
-        }
-    }
+          return formatDuration(timeEnd - timeStart);
+        });
 
-    const pageWrites = writes.filter(({ type }) => type === 'page');
-    const dataWrites = writes.filter(({ type }) => type === 'data');
-    const redirectWrites = writes.filter(({ type }) => type === 'redirect');
+      const longestDurationLength =
+        Math.max(...stepDurations.map(duration => duration.length));
 
-    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;
-    }
+      for (let index = 0; index < stepDetails.length; index++) {
+        const {name, status, annotation} = stepDetails[index];
+        const duration = stepDurations[index];
 
-    /*
-    await progressPromiseAll(`Writing data files shared across languages.`, queue(
-        dataWrites.map(({path, data}) => () => {
-            const bound = {};
+        let message =
+          (stepsNotClean[index]
+            ? `!! `
+            : ` - `);
 
-            bound.serializeLink = bindOpts(serializeLink, {});
+        message += `(${duration})`.padStart(longestDurationLength + 2, ' ');
+        message += ` `;
+        message += `${name}: `.padEnd(longestNameLength + 4, '.');
+        message += ` `;
+        message += status;
 
-            bound.serializeContribs = bindOpts(serializeContribs, {});
+        if (annotation) {
+          message += ` (${annotation})`;
+        }
 
-            bound.serializeImagePaths = bindOpts(serializeImagePaths, {
-                thumb
-            });
+        switch (status) {
+          case STATUS_DONE_CLEAN:
+            console.error(colors.green(message));
+            break;
 
-            bound.serializeCover = bindOpts(serializeCover, {
-                [bindOpts.bindIndex]: 2,
-                serializeImagePaths: bound.serializeImagePaths,
-                urls
-            });
+          case STATUS_NOT_STARTED:
+          case STATUS_NOT_APPLICABLE:
+            console.error(colors.dim(message));
+            break;
 
-            bound.serializeGroupsForAlbum = bindOpts(serializeGroupsForAlbum, {
-                serializeLink
-            });
+          case STATUS_HAS_WARNINGS:
+          case STATUS_STARTED_NOT_DONE:
+            console.error(colors.yellow(message));
+            break;
 
-            bound.serializeGroupsForTrack = bindOpts(serializeGroupsForTrack, {
-                serializeLink
-            });
+          case STATUS_FATAL_ERROR:
+            console.error(colors.red(message));
+            break;
 
-            // TODO: This only supports one <>-style argument.
-            return writeData(path[0], path[1], data({
-                ...bound
-            }));
-        }),
-        queueSize
-    ));
-    */
-
-    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`);
-
-        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,
-                    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
-                });
-
-                bound.parseAttributes = bindOpts(parseAttributes, {
-                    to
-                });
-
-                bound.find = bindFind(wikiData, {mode: 'warn'});
-
-                bound.transformInline = bindOpts(transformInline, {
-                    find: bound.find,
-                    link: bound.link,
-                    replacerSpec,
-                    language,
-                    to,
-                    wikiData
-                });
-
-                bound.transformMultiline = bindOpts(transformMultiline, {
-                    transformInline: bound.transformInline,
-                    parseAttributes: bound.parseAttributes
-                });
-
-                bound.transformLyrics = bindOpts(transformLyrics, {
-                    transformInline: bound.transformInline,
-                    transformMultiline: bound.transformMultiline
-                });
-
-                bound.iconifyURL = bindOpts(iconifyURL, {
-                    language,
-                    to
-                });
-
-                bound.fancifyURL = bindOpts(fancifyURL, {
-                    language
-                });
-
-                bound.fancifyFlashURL = bindOpts(fancifyFlashURL, {
-                    [bindOpts.bindIndex]: 2,
-                    language
-                });
-
-                bound.getLinkThemeString = getLinkThemeString;
-
-                bound.getThemeString = getThemeString;
-
-                bound.getArtistString = bindOpts(getArtistString, {
-                    iconifyURL: bound.iconifyURL,
-                    link: bound.link,
-                    language
-                });
-
-                bound.getAlbumCover = bindOpts(getAlbumCover, {
-                    to
-                });
-
-                bound.getTrackCover = bindOpts(getTrackCover, {
-                    to
-                });
-
-                bound.getFlashCover = bindOpts(getFlashCover, {
-                    to
-                });
-
-                bound.getArtistAvatar = bindOpts(getArtistAvatar, {
-                    to
-                });
-
-                bound.generateAdditionalFilesShortcut = bindOpts(generateAdditionalFilesShortcut, {
-                    language
-                });
-
-                bound.generateAdditionalFilesList = bindOpts(generateAdditionalFilesList, {
-                    language
-                });
-
-                bound.generateChronologyLinks = bindOpts(generateChronologyLinks, {
-                    link: bound.link,
-                    linkAnythingMan: bound.linkAnythingMan,
-                    language,
-                    wikiData
-                });
-
-                bound.generateCoverLink = bindOpts(generateCoverLink, {
-                    [bindOpts.bindIndex]: 0,
-                    img,
-                    link: bound.link,
-                    language,
-                    to,
-                    wikiData
-                });
-
-                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.getFlashGridHTML = bindOpts(getFlashGridHTML, {
-                    [bindOpts.bindIndex]: 0,
-                    getFlashCover: bound.getFlashCover,
-                    getGridHTML: bound.getGridHTML,
-                    link: bound.link
-                });
-
-                bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, {
-                    language
-                });
-
-                bound.getRevealStringFromWarnings = bindOpts(getRevealStringFromWarnings, {
-                    language
-                });
-
-                bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, {
-                    to
-                });
-
-                const pageInfo = page({
-                    ...bound,
-
-                    language,
-
-                    absoluteTo,
-                    relativeTo: to,
-                    to,
-                    urls,
-
-                    getSizeOfAdditionalFile,
-                });
-
-                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
-                });
-
-                return writePage.write({
-                    html,
-                    oEmbedJSON,
-                    paths,
-                });
-            }),
-            ...redirectWrites.map(({fromPath, toPath, title: titleFn}) => () => {
-                const title = titleFn({
-                    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));
-    };
+          default:
+            console.error(message);
+            break;
+        }
+      }
 
-    await wrapLanguages(perLanguageFn, {
-        languages,
-        writeOneLanguage,
-    });
+      console.error(colors.bright(`Done in ${totalDuration}.`));
 
-    // The single most important step.
-    logInfo`Written!`;
-}
+      if (result === true) {
+        if (anyStepsNotClean) {
+          console.error(colors.bright(`Final output is true, but some steps aren't clean.`));
+          process.exit(1);
+          return;
+        } else {
+          console.error(colors.bright(`Final output is true and all steps are clean.`));
+        }
+      } else if (result === false) {
+        console.error(colors.bright(`Final output is false.`));
+      } else {
+        console.error(colors.bright(`Final output is not true (${result}).`));
+      }
+    }
 
-main().catch(error => {
-    if (error instanceof AggregateError) {
-        showAggregate(error);
-    } else {
-        console.error(error);
+    if (result !== true) {
+      process.exit(1);
+      return;
     }
-}).then(() => {
+
     decorateTime.displayTime();
     CacheableObject.showInvalidAccesses();
-});
+
+    process.exit(0);
+  })();
+}