« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/listing-spec.js12
-rw-r--r--src/misc-templates.js148
-rwxr-xr-xsrc/upd8.js2238
-rw-r--r--src/util/magic-constants.js1
-rw-r--r--src/util/transform-content.js453
-rw-r--r--src/util/urls.js118
-rw-r--r--src/write/bind-utilities.js272
-rw-r--r--src/write/build-modes/index.js2
-rw-r--r--src/write/build-modes/live-dev-server.js373
-rw-r--r--src/write/build-modes/static-build.js527
-rw-r--r--src/write/page-template.js631
-rw-r--r--src/write/validate-writes.js134
12 files changed, 2745 insertions, 2164 deletions
diff --git a/src/listing-spec.js b/src/listing-spec.js
index 26910c0..ef51fe9 100644
--- a/src/listing-spec.js
+++ b/src/listing-spec.js
@@ -1,3 +1,5 @@
+import {OFFICIAL_GROUP_DIRECTORY} from './util/magic-constants.js';
+
 import {
   empty,
   accumulateSum,
@@ -879,16 +881,20 @@ const listingSpec = [
     directory: 'random',
     stringsKey: 'other.randomPages',
 
-    data: ({wikiData: {fandomAlbumData, officialAlbumData}}) => [
+    data: ({wikiData: {albumData}}) => [
       {
-        albums: officialAlbumData,
         name: 'Official',
         randomCode: 'official',
+        albums: albumData
+          .filter((album) => album.groups
+            .some((group) => group.directory === OFFICIAL_GROUP_DIRECTORY)),
       },
       {
-        albums: fandomAlbumData,
         name: 'Fandom',
         randomCode: 'fandom',
+        albums: albumData
+          .filter((album) => album.groups
+            .every((group) => group.directory !== OFFICIAL_GROUP_DIRECTORY)),
       },
     ],
 
diff --git a/src/misc-templates.js b/src/misc-templates.js
index a530a3c..7cfdc86 100644
--- a/src/misc-templates.js
+++ b/src/misc-templates.js
@@ -10,6 +10,8 @@ import {
   unique,
 } from './util/sugar.js';
 
+import {thumb} from './util/urls.js';
+
 import {
   getTotalDuration,
   sortAlbumsTracksChronologically,
@@ -642,6 +644,106 @@ function unbound_getFlashGridHTML({
   });
 }
 
+// Images
+
+function unbound_img({
+  html,
+
+  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 = {
+    id: link ? '' : id,
+    class: className,
+    alt,
+    width,
+    height,
+  };
+
+  const noSrcHTML =
+    !src &&
+      wrap(
+        html.tag('div',
+          {class: 'image-text-area'},
+          noSrcText));
+
+  const nonlazyHTML =
+    src &&
+      wrap(
+        html.tag('img', {
+          ...imgAttributes,
+          src: thumbSrc,
+        }));
+
+  const lazyHTML =
+    src &&
+    lazy &&
+      wrap(
+        html.tag('img',
+          {
+            ...imgAttributes,
+            class: [className, 'lazy'],
+            'data-original': thumbSrc,
+          }),
+        true);
+
+  if (!src) {
+    return noSrcHTML;
+  } else if (lazy) {
+    return html.tag('noscript', nonlazyHTML) + '\n' + lazyHTML;
+  } else {
+    return nonlazyHTML;
+  }
+
+  function wrap(input, hide = false) {
+    let wrapped = input;
+
+    wrapped = html.tag('div', {class: 'image-container'}, wrapped);
+
+    if (reveal) {
+      wrapped = html.tag('div', {class: 'reveal'}, [
+        wrapped,
+        html.tag('span', {class: 'reveal-text'}, reveal),
+      ]);
+    }
+
+    if (willSquare) {
+      wrapped = html.tag('div', {class: 'square-content'}, wrapped);
+      wrapped = html.tag('div',
+        {class: ['square', hide && !willLink && 'js-hide']},
+        wrapped);
+    }
+
+    if (willLink) {
+      wrapped = html.tag('a',
+        {
+          id,
+          class: ['box', hide && 'js-hide'],
+          href: typeof link === 'string' ? link : originalSrc,
+        },
+        wrapped);
+    }
+
+    return wrapped;
+  }
+}
+
 // Carousel reels
 
 // Layout constants:
@@ -866,48 +968,38 @@ function unbound_generateStickyHeadingContainer({
 
 // Footer stuff
 
-function unbound_getFooterLocalizationLinks(pathname, {
+function unbound_getFooterLocalizationLinks({
   html,
-  language,
-  to,
-  paths,
-
   defaultLanguage,
+  language,
   languages,
+  pagePath,
+  to,
 }) {
-  const {urlPath} = paths;
-  const keySuffix = urlPath[0].replace(/^localized\./, '.');
-  const toArgs = urlPath.slice(1);
-
   const links = Object.entries(languages)
     .filter(([code, language]) => code !== 'default' && !language.hidden)
     .map(([code, language]) => language)
     .sort(({name: a}, {name: b}) => (a < b ? -1 : a > b ? 1 : 0))
     .map((language) =>
-      html.tag(
-        'span',
-        html.tag(
-          'a',
+      html.tag('span',
+        html.tag('a',
           {
             href:
               language === defaultLanguage
-                ? to('localizedDefaultLanguage' + keySuffix, ...toArgs)
+                ? to(
+                    'localizedDefaultLanguage.' + pagePath[0],
+                    ...pagePath.slice(1))
                 : to(
-                    'localizedWithBaseDirectory' + keySuffix,
+                    'localizedWithBaseDirectory.' + pagePath[0],
                     language.code,
-                    ...toArgs
-                  ),
+                    ...pagePath.slice(1)),
           },
-          language.name
-        )
-      )
-    );
-
-  return html.tag(
-    'div',
-    {class: 'footer-localization-links'},
-    language.$('misc.uiLanguage', {languages: links.join('\n')})
-  );
+          language.name)));
+
+  return html.tag('div', {class: 'footer-localization-links'},
+    language.$('misc.uiLanguage', {
+      languages: links.join('\n'),
+    }));
 }
 
 // Exports
@@ -940,6 +1032,8 @@ export {
 
   unbound_getCarouselHTML as getCarouselHTML,
 
+  unbound_img as img,
+
   unbound_generateInfoGalleryLinks as generateInfoGalleryLinks,
   unbound_generateNavigationLinks as generateNavigationLinks,
 
diff --git a/src/upd8.js b/src/upd8.js
index a793feb..0d9ec71 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -31,38 +31,18 @@
 // 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 {execSync} from 'child_process';
 import * as path from 'path';
 import {fileURLToPath} from 'url';
 
-import chroma from 'chroma-js';
-
-import {
-  copyFile,
-  mkdir,
-  stat,
-  symlink,
-  writeFile,
-  unlink,
-} from 'fs/promises';
-
-import { execSync } from 'child_process';
-
 import genThumbs from './gen-thumbs.js';
 import {listingSpec, listingTargetSpec} from './listing-spec.js';
 import urlSpec from './url-spec.js';
-import * as pageSpecs from './page/index.js';
 
-import find, {bindFind} from './util/find.js';
-import * as html from './util/html.js';
-import {getColors} from './util/colors.js';
-import {findFiles} from './util/io.js';
-import {isMain} from './util/node-utils.js';
+import {processLanguageFile} from './data/language.js';
 
 import CacheableObject from './data/things/cacheable-object.js';
 
-import {processLanguageFile} from './data/language.js';
-import {serializeThings} from './data/serialize.js';
-
 import {
   filterDuplicateDirectories,
   filterReferenceErrors,
@@ -72,33 +52,17 @@ import {
   WIKI_INFO_FILE,
 } from './data/yaml.js';
 
-import {
-  fancifyFlashURL,
-  fancifyURL,
-  generateAdditionalFilesShortcut,
-  generateAdditionalFilesList,
-  generateChronologyLinks,
-  generateCoverLink,
-  generateInfoGalleryLinks,
-  generateNavigationLinks,
-  generateStickyHeadingContainer,
-  generateTrackListDividedByGroups,
-  getAlbumGridHTML,
-  getAlbumStylesheet,
-  getArtistString,
-  getFlashGridHTML,
-  getFooterLocalizationLinks,
-  getGridHTML,
-  getCarouselHTML,
-  getRevealStringFromTags,
-  getRevealStringFromWarnings,
-  getThemeString as unbound_getThemeString,
-  iconifyURL,
-} from './misc-templates.js';
-
-import unbound_link, {
-  getLinkThemeString as unbound_getLinkThemeString,
-} from './util/link.js';
+import find from './util/find.js';
+import {findFiles} from './util/io.js';
+import link from './util/link.js';
+import {isMain} from './util/node-utils.js';
+import {validateReplacerSpec} from './util/replacer.js';
+import {empty, showAggregate} from './util/sugar.js';
+import {replacerSpec} from './util/transform-content.js';
+import {generateURLs} from './util/urls.js';
+
+import {generateDevelopersCommentHTML} from './write/page-template.js';
+import * as buildModes from './write/build-modes/index.js';
 
 import {
   color,
@@ -111,15 +75,6 @@ import {
   progressPromiseAll,
 } from './util/cli.js';
 
-import {validateReplacerSpec, transformInline} from './util/replacer.js';
-
-import {
-  getAlbumCover,
-  getArtistAvatar,
-  getFlashCover,
-  getTrackCover,
-} from './util/wiki-data.js';
-
 /*
 import {
   serializeContribs,
@@ -131,18 +86,6 @@ import {
 } from './util/serialize.js';
 */
 
-import {
-  bindOpts,
-  queue,
-  showAggregate,
-  withEntries,
-} from './util/sugar.js';
-
-import {generateURLs, thumb} from './util/urls.js';
-
-// Pensive emoji!
-import { OFFICIAL_GROUP_DIRECTORY } from './util/magic-constants.js';
-
 import FileSizePreloader from './file-size-preloader.js';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -160,1503 +103,46 @@ const BUILD_TIME = new Date();
 
 const DEFAULT_STRINGS_FILE = 'strings-default.json';
 
-// Code that's common 8etween the 8uild code (i.e. upd8.js) and gener8ted
-// site code should 8e put here. Which, uh, ~~only really means this one
-// 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';
-
-// 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';
-
-// This exists adjacent to index.html for any page with oEmbed metadata.
-const OEMBED_JSON_FILE = 'oembed.json';
-
-// Automatically copied (if present) from media directory to site root.
-const FAVICON_FILE = 'favicon.ico';
-
-// 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;
-
-// 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 = {};
-
-let queueSize;
-
-const urls = generateURLs(urlSpec);
-
-function splitLines(text) {
-  return text.split(/\r\n|\r|\n/);
-}
-
-const replacerSpec = {
-  album: {
-    find: 'album',
-    link: 'album',
-  },
-  'album-commentary': {
-    find: 'album',
-    link: 'albumCommentary',
-  },
-  'album-gallery': {
-    find: 'album',
-    link: 'albumGallery',
-  },
-  artist: {
-    find: 'artist',
-    link: 'artist',
-  },
-  'artist-gallery': {
-    find: 'artist',
-    link: 'artistGallery',
-  },
-  'commentary-index': {
-    find: null,
-    link: 'commentaryIndex',
-  },
-  date: {
-    find: null,
-    value: (ref) => new Date(ref),
-    html: (date, {language}) =>
-      html.tag('time',
-        {datetime: date.toString()},
-        language.formatDate(date)),
-  },
-  'flash-index': {
-    find: null,
-    link: 'flashIndex',
-  },
-  flash: {
-    find: 'flash',
-    link: 'flash',
-    transformName(name, node, input) {
-      const nextCharacter = input[node.iEnd];
-      const lastCharacter = name[name.length - 1];
-      if (![' ', '\n', '<'].includes(nextCharacter) && lastCharacter === '.') {
-        return name.slice(0, -1);
-      } else {
-        return name;
-      }
-    },
-  },
-  group: {
-    find: 'group',
-    link: 'groupInfo',
-  },
-  'group-gallery': {
-    find: 'group',
-    link: 'groupGallery',
-  },
-  home: {
-    find: null,
-    link: 'home',
-  },
-  'listing-index': {
-    find: null,
-    link: 'listingIndex',
-  },
-  listing: {
-    find: 'listing',
-    link: 'listing',
-  },
-  media: {
-    find: null,
-    link: 'media',
-  },
-  'news-index': {
-    find: null,
-    link: 'newsIndex',
-  },
-  'news-entry': {
-    find: 'newsEntry',
-    link: 'newsEntry',
-  },
-  root: {
-    find: null,
-    link: 'root',
-  },
-  site: {
-    find: null,
-    link: 'site',
-  },
-  static: {
-    find: 'staticPage',
-    link: 'staticPage',
-  },
-  string: {
-    find: null,
-    value: (ref) => ref,
-    html: (ref, {language, args}) => language.$(ref, args),
-  },
-  tag: {
-    find: 'artTag',
-    link: 'tag',
-  },
-  track: {
-    find: 'track',
-    link: 'track',
-  },
-};
-
-if (!validateReplacerSpec(replacerSpec, {find, link: unbound_link})) {
+if (!validateReplacerSpec(replacerSpec, {find, link})) {
   process.exit();
 }
 
-function parseAttributes(string, {to}) {
-  const attributes = Object.create(null);
-  const skipWhitespace = (i) => {
-    const ws = /\s/;
-    if (ws.test(string[i])) {
-      const match = string.slice(i).match(/[^\s]/);
-      if (match) {
-        return i + match.index;
-      } else {
-        return string.length;
-      }
-    } else {
-      return i;
-    }
-  };
-
-  for (let i = 0; i < string.length; ) {
-    i = skipWhitespace(i);
-    const aStart = i;
-    const aEnd = i + string.slice(i).match(/[\s=]|$/).index;
-    const attribute = string.slice(aStart, aEnd);
-    i = skipWhitespace(aEnd);
-    if (string[i] === '=') {
-      i = skipWhitespace(i + 1);
-      let end, endOffset;
-      if (string[i] === '"' || string[i] === "'") {
-        end = string[i];
-        endOffset = 1;
-        i++;
-      } else {
-        end = '\\s';
-        endOffset = 0;
-      }
-      const vStart = i;
-      const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index;
-      const value = string.slice(vStart, vEnd);
-      i = vEnd + endOffset;
-      if (attribute === 'src' && value.startsWith('media/')) {
-        attributes[attribute] = to('media.path', value.slice('media/'.length));
-      } else {
-        attributes[attribute] = value;
-      }
-    } else {
-      attributes[attribute] = attribute;
-    }
-  }
-  return Object.fromEntries(
-    Object.entries(attributes).map(([key, val]) => [
-      key,
-      val === 'true'
-        ? true
-        : val === 'false'
-        ? false
-        : val === key
-        ? true
-        : val,
-    ])
-  );
-}
-
-function joinLineBreaks(sourceLines) {
-  const outLines = [];
-
-  let lineSoFar = '';
-  for (let i = 0; i < sourceLines.length; i++) {
-    const line = sourceLines[i];
-    lineSoFar += line;
-    if (!line.endsWith('<br>')) {
-      outLines.push(lineSoFar);
-      lineSoFar = '';
-    }
-  }
-
-  if (lineSoFar) {
-    outLines.push(lineSoFar);
-  }
-
-  return outLines;
-}
-
-function transformMultiline(text, {
-  parseAttributes,
-  transformInline,
-  thumb = null,
-}) {
-  // Heck yes, HTML magics.
-
-  text = transformInline(text.trim());
-
-  const outLines = [];
-
-  const indentString = ' '.repeat(4);
-
-  let levelIndents = [];
-  const openLevel = (indent) => {
-    // opening a sublist is a pain: to be semantically *and* visually
-    // correct, we have to append the <ul> at the end of the existing
-    // previous <li>
-    const previousLine = outLines[outLines.length - 1];
-    if (previousLine?.endsWith('</li>')) {
-      // we will re-close the <li> later
-      outLines[outLines.length - 1] = previousLine.slice(0, -5) + ' <ul>';
-    } else {
-      // if the previous line isn't a list item, this is the opening of
-      // the first list level, so no need for indent
-      outLines.push('<ul>');
-    }
-    levelIndents.push(indent);
-  };
-  const closeLevel = () => {
-    levelIndents.pop();
-    if (levelIndents.length) {
-      // closing a sublist, so close the list item containing it too
-      outLines.push(indentString.repeat(levelIndents.length) + '</ul></li>');
-    } else {
-      // closing the final list level! no need for indent here
-      outLines.push('</ul>');
-    }
-  };
-
-  // 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,
-        ...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('\\>', '>');
-      }
-    }
-
-    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 = '';
-      }
-
-      // for sticky headings!
-      if (elementMatch && elementMatch[1] === 'h2') {
-        lineContent = lineContent.replace(/<h2(.*?)>/g, (match, attributes) => {
-          const parsedAttributes = parseAttributes(attributes, {to});
-          return `<h2 ${html.attributes({
-            ...parsedAttributes,
-            class: [...parsedAttributes.class?.split(' ') ?? [], 'content-heading'],
-          })}>`;
-        });
-      }
-    }
-
-    let pushString = indentString.repeat(indentThisLine);
-    if (lineTag) {
-      pushString += `<${lineTag}>${lineContent}</${lineTag}>`;
-    } else {
-      pushString += lineContent;
-    }
-    outLines.push(pushString);
-  }
-
-  // after processing all lines...
-
-  // if still in a list, close all levels
-  while (levelIndents.length) closeLevel();
-
-  // if still in a blockquote, close its tag
-  if (inBlockquote) {
-    inBlockquote = false;
-    outLines.push('</blockquote>');
-  }
-
-  return outLines.join('\n');
-}
-
-function transformLyrics(text, {transformInline, transformMultiline}) {
-  // Different from transformMultiline 'cuz it joins multiple lines together
-  // with line 8reaks (<br>); transformMultiline treats each line as its own
-  // complete paragraph (or list, etc).
-
-  // If it looks like old data, then like, oh god.
-  // Use the normal transformMultiline tool.
-  if (text.includes('<br')) {
-    return transformMultiline(text);
-  }
-
-  text = transformInline(text.trim());
-
-  let buildLine = '';
-  const addLine = () => outLines.push(`<p>${buildLine}</p>`);
-  const outLines = [];
-  for (const line of text.split('\n')) {
-    if (line.length) {
-      if (buildLine.length) {
-        buildLine += '<br>';
-      }
-      buildLine += line;
-    } else if (buildLine.length) {
-      addLine();
-      buildLine = '';
-    }
-  }
-  if (buildLine.length) {
-    addLine();
-  }
-  return outLines.join('\n');
-}
-
-function stringifyThings(thingData) {
-  return JSON.stringify(serializeThings(thingData));
-}
-
-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 = {
-    id: link ? '' : id,
-    class: className,
-    alt,
-    width,
-    height,
-  };
-
-  const noSrcHTML =
-    !src &&
-      wrap(
-        html.tag('div',
-          {class: 'image-text-area'},
-          noSrcText));
-
-  const nonlazyHTML =
-    src &&
-      wrap(
-        html.tag('img', {
-          ...imgAttributes,
-          src: thumbSrc,
-        }));
-
-  const lazyHTML =
-    src &&
-    lazy &&
-      wrap(
-        html.tag('img',
-          {
-            ...imgAttributes,
-            class: [className, 'lazy'],
-            'data-original': thumbSrc,
-          }),
-        true);
-
-  if (!src) {
-    return noSrcHTML;
-  } else if (lazy) {
-    return html.tag('noscript', nonlazyHTML) + '\n' + lazyHTML;
-  } else {
-    return nonlazyHTML;
-  }
-
-  function wrap(input, hide = false) {
-    let wrapped = input;
-
-    wrapped = html.tag('div', {class: 'image-container'}, wrapped);
-
-    if (reveal) {
-      wrapped = html.tag('div', {class: 'reveal'}, [
-        wrapped,
-        html.tag('span', {class: 'reveal-text'}, reveal),
-      ]);
-    }
-
-    if (willSquare) {
-      wrapped = html.tag('div', {class: 'square-content'}, wrapped);
-      wrapped = html.tag('div',
-        {class: ['square', hide && !willLink && 'js-hide']},
-        wrapped);
-    }
-
-    if (willLink) {
-      wrapped = html.tag('a',
-        {
-          id,
-          class: ['box', hide && 'js-hide'],
-          href: typeof link === 'string' ? link : originalSrc,
-        },
-        wrapped);
-    }
-
-    return wrapped;
-  }
-}
-
-function validateWritePath(path, urlGroup) {
-  if (!Array.isArray(path)) {
-    return {error: `Expected array, got ${path}`};
-  }
-
-  const {paths} = urlGroup;
-
-  const definedKeys = Object.keys(paths);
-  const specifiedKey = path[0];
-
-  if (!definedKeys.includes(specifiedKey)) {
-    return {error: `Specified key ${specifiedKey} isn't defined`};
-  }
-
-  const expectedArgs = paths[specifiedKey].match(/<>/g)?.length ?? 0;
-  const specifiedArgs = path.length - 1;
-
-  if (specifiedArgs !== expectedArgs) {
-    return {
-      error: `Expected ${expectedArgs} arguments, got ${specifiedArgs}`,
-    };
-  }
-
-  return {success: true};
-}
-
-function validateWriteObject(obj) {
-  if (typeof obj !== 'object') {
-    return {error: `Expected object, got ${typeof obj}`};
-  }
-
-  if (typeof obj.type !== 'string') {
-    return {error: `Expected type to be string, got ${obj.type}`};
-  }
-
-  switch (obj.type) {
-    case 'legacy': {
-      if (typeof obj.write !== 'function') {
-        return {error: `Expected write to be string, got ${obj.write}`};
-      }
-
-      break;
-    }
-
-    case 'page': {
-      const path = validateWritePath(obj.path, urlSpec.localized);
-      if (path.error) {
-        return {error: `Path validation failed: ${path.error}`};
-      }
-
-      if (typeof obj.page !== 'function') {
-        return {error: `Expected page to be function, got ${obj.content}`};
-      }
-
-      break;
-    }
-
-    case 'data': {
-      const path = validateWritePath(obj.path, urlSpec.data);
-      if (path.error) {
-        return {error: `Path validation failed: ${path.error}`};
-      }
-
-      if (typeof obj.data !== 'function') {
-        return {error: `Expected data to be function, got ${obj.data}`};
-      }
-
-      break;
-    }
-
-    case 'redirect': {
-      const fromPath = validateWritePath(obj.fromPath, urlSpec.localized);
-      if (fromPath.error) {
-        return {
-          error: `Path (fromPath) validation failed: ${fromPath.error}`,
-        };
-      }
-
-      const toPath = validateWritePath(obj.toPath, urlSpec.localized);
-      if (toPath.error) {
-        return {error: `Path (toPath) validation failed: ${toPath.error}`};
-      }
-
-      if (typeof obj.title !== 'function') {
-        return {error: `Expected title to be function, got ${obj.title}`};
-      }
-
-      break;
-    }
-
-    default: {
-      return {error: `Unknown type: ${obj.type}`};
-    }
-  }
-
-  return {success: true};
-}
-
-export function getURLsFrom({
-  baseDirectory,
-  pageSubKey,
-  paths,
-}) {
-  return (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;
-    }
-
-    path += urls.from(from).to(to, ...args);
-
-    return path;
-  };
-}
-
-export function generateDocumentHTML(pageInfo, {
-  defaultLanguage,
-  getThemeString,
-  language,
-  languages,
-  localizedPaths,
-  paths,
-  oEmbedJSONHref,
-  to,
-  transformMultiline,
-  wikiData,
-}) {
-  const {wikiInfo} = wikiData;
-
-  let {
-    title = '',
-    meta = {},
-    theme = '',
-    stylesheet = '',
-
-    showWikiNameInTitle = true,
-    themeColor = '',
-
-    // missing properties are auto-filled, see below!
-    body = {},
-    banner = {},
-    main = {},
-    sidebarLeft = {},
-    sidebarRight = {},
-    nav = {},
-    secondaryNav = {},
-    footer = {},
-    socialEmbed = {},
-  } = pageInfo;
-
-  body.style ??= '';
-
-  theme = theme || getThemeString(wikiInfo.color);
-
-  banner ||= {};
-  banner.classes ??= [];
-  banner.src ??= '';
-  banner.position ??= '';
-  banner.dimensions ??= [0, 0];
-
-  main.classes ??= [];
-  main.content ??= '';
-
-  sidebarLeft ??= {};
-  sidebarRight ??= {};
-
-  for (const sidebar of [sidebarLeft, sidebarRight]) {
-    sidebar.classes ??= [];
-    sidebar.content ??= '';
-    sidebar.collapse ??= true;
-  }
-
-  nav.classes ??= [];
-  nav.content ??= '';
-  nav.bottomRowContent ??= '';
-  nav.links ??= [];
-  nav.linkContainerClasses ??= [];
-
-  secondaryNav ??= {};
-  secondaryNav.content ??= '';
-  secondaryNav.content ??= '';
-
-  footer.classes ??= [];
-  footer.content ??= wikiInfo.footerContent
-    ? transformMultiline(wikiInfo.footerContent)
-    : '';
-
-  const colors = themeColor
-    ? getColors(themeColor, {chroma})
-    : null;
-
-  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 =
-    html.tag('footer',
-      {
-        [html.onlyIfContent]: true,
-        id: 'footer',
-        class: footer.classes,
-      },
-      [
-        html.tag('div',
-          {
-            [html.onlyIfContent]: true,
-            class: 'footer-content',
-          },
-          footer.content),
-
-        getFooterLocalizationLinks(paths.pathname, {
-          defaultLanguage,
-          html,
-          language,
-          languages,
-          paths,
-          to,
-        }),
-      ]);
-
-  const generateSidebarHTML = (id, {
-    content,
-    multiple,
-    classes,
-    collapse = true,
-    wide = false,
-
-    // 'last' - last or only sidebar box is sticky
-    // 'column' - entire column, incl. multiple boxes from top, is sticky
-    // 'none' - sidebar not sticky at all, stays at top of page
-    stickyMode = 'last',
-  }) =>
-    content
-      ? html.tag('div',
-          {
-            id,
-            class: [
-              'sidebar-column',
-              'sidebar',
-              wide && 'wide',
-              !collapse && 'no-hide',
-              stickyMode !== 'none' && 'sticky-' + stickyMode,
-              ...classes,
-            ],
-          },
-          content)
-      : multiple
-      ? html.tag('div',
-          {
-            id,
-            class: [
-              'sidebar-column',
-              'sidebar-multiple',
-              wide && 'wide',
-              !collapse && 'no-hide',
-              stickyMode !== 'none' && 'sticky-' + stickyMode,
-            ],
-          },
-          multiple
-            .map((infoOrContent) =>
-              (typeof infoOrContent === 'object' && !Array.isArray(infoOrContent))
-                ? infoOrContent
-                : {content: infoOrContent})
-            .filter(({content}) => content)
-            .map(({
-              content,
-              classes: classes2 = [],
-            }) =>
-              html.tag('div',
-                {
-                  class: ['sidebar', ...classes, ...classes2],
-                },
-                html.fragment(content))))
-      : '';
-
-  const sidebarLeftHTML = generateSidebarHTML('sidebar-left', sidebarLeft);
-  const sidebarRightHTML = generateSidebarHTML('sidebar-right', sidebarRight);
-
-  if (nav.simple) {
-    nav.linkContainerClasses = ['nav-links-hierarchy'];
-    nav.links = [{toHome: true}, {toCurrentPage: true}];
-  }
-
-  const links = (nav.links || []).filter(Boolean);
-
-  const navLinkParts = [];
-  for (let i = 0; i < links.length; i++) {
-    let cur = links[i];
-
-    let {title: linkTitle} = cur;
-
-    if (cur.toHome) {
-      linkTitle ??= wikiInfo.nameShort;
-    } else if (cur.toCurrentPage) {
-      linkTitle ??= title;
-    }
-
-    let partContent;
-
-    if (typeof cur.html === 'string') {
-      partContent = cur.html;
-    } else {
-      const attributes = {
-        class: (cur.toCurrentPage || i === links.length - 1) && 'current',
-        href: cur.toCurrentPage
-          ? ''
-          : cur.toHome
-          ? to('localized.home')
-          : cur.path
-          ? to(...cur.path)
-          : cur.href
-          ? (() => {
-              logWarn`Using legacy href format nav link in ${paths.pathname}`;
-              return cur.href;
-            })()
-          : null,
-      };
-      if (attributes.href === null) {
-        throw new Error(
-          `Expected some href specifier for link to ${linkTitle} (${JSON.stringify(
-            cur
-          )})`
-        );
-      }
-      partContent = html.tag('a', attributes, linkTitle);
-    }
-
-    if (!partContent) continue;
-
-    const part = html.tag('span',
-      {class: cur.divider === false && 'no-divider'},
-      partContent);
-
-    navLinkParts.push(part);
-  }
-
-  const navHTML = html.tag('nav',
-    {
-      [html.onlyIfContent]: true,
-      id: 'header',
-      class: [
-        ...nav.classes,
-        links.length && 'nav-has-main-links',
-        nav.content && 'nav-has-content',
-        nav.bottomRowContent && 'nav-has-bottom-row',
-      ],
-    },
-    [
-      links.length &&
-        html.tag(
-          'div',
-          {class: ['nav-main-links', ...nav.linkContainerClasses]},
-          navLinkParts
-        ),
-      nav.bottomRowContent &&
-        html.tag('div', {class: 'nav-bottom-row'}, nav.bottomRowContent),
-      nav.content && html.tag('div', {class: 'nav-content'}, nav.content),
-    ]);
-
-  const secondaryNavHTML = html.tag('nav',
-    {
-      [html.onlyIfContent]: true,
-      id: 'secondary-nav',
-      class: secondaryNav.classes,
-    },
-    secondaryNav.content);
-
-  const bannerSrc = banner.src
-    ? banner.src
-    : banner.path
-    ? to(...banner.path)
-    : null;
-
-  const bannerHTML =
-    banner.position &&
-    bannerSrc &&
-    html.tag('div',
-      {
-        id: 'banner',
-        class: banner.classes,
-      },
-      html.tag('img', {
-        src: bannerSrc,
-        alt: banner.alt,
-        width: banner.dimensions[0] || 1100,
-        height: banner.dimensions[1] || 200,
-      }));
-
-  const layoutHTML = [
-    navHTML,
-    banner.position === 'top' && bannerHTML,
-    secondaryNavHTML,
-    html.tag('div',
-      {
-        class: [
-          'layout-columns',
-          !collapseSidebars && 'vertical-when-thin',
-          (sidebarLeftHTML || sidebarRightHTML) && 'has-one-sidebar',
-          (sidebarLeftHTML && sidebarRightHTML) && 'has-two-sidebars',
-          !(sidebarLeftHTML || sidebarRightHTML) && 'has-zero-sidebars',
-          sidebarLeftHTML && 'has-sidebar-left',
-          sidebarRightHTML && 'has-sidebar-right',
-        ],
-      },
-      [
-        sidebarLeftHTML,
-        mainHTML,
-        sidebarRightHTML,
-      ]),
-    banner.position === 'bottom' && bannerHTML,
-    footerHTML,
-  ].filter(Boolean).join('\n');
-
-  const infoCardHTML = html.tag('div', {id: 'info-card-container'},
-    html.tag('div', {id: 'info-card-decor'},
-      html.tag('div', {id: 'info-card'}, [
-        html.tag('div', {class: ['info-card-art-container', 'no-reveal']},
-          img({
-            class: 'info-card-art',
-            src: '',
-            link: true,
-            square: true,
-          })),
-        html.tag('div', {class: ['info-card-art-container', 'reveal']},
-          img({
-            class: 'info-card-art',
-            src: '',
-            link: true,
-            square: true,
-            reveal: getRevealStringFromWarnings(
-              html.tag('span', {class: 'info-card-art-warnings'}),
-              {html, language}),
-          })),
-        html.tag('h1', {class: 'info-card-name'},
-          html.tag('a')),
-        html.tag('p', {class: 'info-card-album'},
-          language.$('releaseInfo.from', {
-            album: html.tag('a'),
-          })),
-        html.tag('p', {class: 'info-card-artists'},
-          language.$('releaseInfo.by', {
-            artists: html.tag('span'),
-          })),
-        html.tag('p', {class: 'info-card-cover-artists'},
-          language.$('releaseInfo.coverArtBy', {
-            artists: html.tag('span'),
-          })),
-      ])));
-
-  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}),
-
-    ...html.fragment(
-      colors && [
-        html.tag('meta', {
-          name: 'theme-color',
-          content: colors.dark,
-          media: '(prefers-color-scheme: dark)',
-        }),
-
-        html.tag('meta', {
-          name: 'theme-color',
-          content: colors.light,
-          media: '(prefers-color-scheme: light)',
-        }),
-
-        html.tag('meta', {
-          name: 'theme-color',
-          content: colors.primary,
-        }),
-      ]),
-
-    oEmbedJSONHref &&
-      html.tag('link', {
-        type: 'application/json+oembed',
-        href: oEmbedJSONHref,
-      }),
-  ].filter(Boolean).join('\n');
-
-  return `<!DOCTYPE html>\n` + html.tag('html',
-    {
-      lang: language.intlCode,
-      'data-language-code': language.code,
-      'data-url-key': paths.urlPath[0],
-      ...Object.fromEntries(
-        paths.urlPath.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'),
-    },
-    [
-      `<!--\n` + [
-        wikiInfo.canonicalBase
-          ? `hsmusic.wiki - ${wikiInfo.name}, ${wikiInfo.canonicalBase}`
-          : `hsmusic.wiki - ${wikiInfo.name}`,
-        'Code copyright 2019-2022 Quasar Nebula et al (MIT License)',
-        ...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/',
-        ],
-        '***',
-        `Site built: ${BUILD_TIME.toLocaleString('en-US', {
-          dateStyle: 'long',
-          timeStyle: 'long',
-        })}`,
-        `Latest code commit: ${COMMIT}`,
-      ]
-        .filter(Boolean)
-        .map(line => `    ` + line)
-        .join('\n') + `\n-->`,
-
-      html.tag('head', [
-        html.tag('title',
-          showWikiNameInTitle
-            ? language.formatString('misc.pageTitle.withWikiName', {
-                title,
-                wikiName: wikiInfo.nameShort,
-              })
-            : language.formatString('misc.pageTitle', {title})),
-
-        html.tag('meta', {charset: 'utf-8'}),
-        html.tag('meta', {
-          name: 'viewport',
-          content: 'width=device-width, initial-scale=1',
-        }),
-
-        ...(
-          Object.entries(meta)
-            .filter(([key, value]) => value)
-            .map(([key, value]) => html.tag('meta', {[key]: value}))),
-
-        canonical &&
-          html.tag('link', {
-            rel: 'canonical',
-            href: canonical,
-          }),
-
-        ...(
-          localizedCanonical
-            .map(({lang, href}) => html.tag('link', {
-              rel: 'alternate',
-              hreflang: lang,
-              href,
-            }))),
-
-        socialEmbedHTML,
-
-        html.tag('link', {
-          rel: 'stylesheet',
-          href: to('shared.staticFile', `site2.css?${CACHEBUST}`),
-        }),
-
-        html.tag('style',
-          {[html.onlyIfContent]: true},
-          [
-            theme,
-            stylesheet,
-          ]),
-
-        html.tag('script', {
-          src: to('shared.staticFile', `lazy-loading.js?${CACHEBUST}`),
-        }),
-      ]),
-
-      html.tag('body',
-        {style: body.style || ''},
-        [
-          html.tag('div', {id: 'page-container'}, [
-            mainHTML &&
-              html.tag('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]) =>
-                    html.tag('span', {class: 'skipper'},
-                      html.tag('a', {href}, title)))),
-            layoutHTML,
-          ]),
-
-          infoCardHTML,
-
-          html.tag('script', {
-            type: 'module',
-            src: to('shared.staticFile', `client.js?${CACHEBUST}`),
-          }),
-        ]),
-    ]);
-}
-
-function generateOEmbedJSON(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));
-}
-
-async function writePage({
-  html,
-  oEmbedJSON = '',
-  paths,
-}) {
-  await mkdir(paths.output.directory, {recursive: true});
-
-  await Promise.all(
-    [
-      writeFile(paths.output.documentHTML, html),
-
-      oEmbedJSON &&
-        writeFile(paths.output.oEmbedJSON, oEmbedJSON),
-    ].filter(Boolean)
-  );
-}
-
-function getPagePaths({
-  baseDirectory,
-  fullKey,
-  urlArgs,
-
-  file = 'index.html',
-}) {
-  const [groupKey, subKey] = fullKey.split('.');
-
-  const pathname =
-    groupKey === 'localized' && baseDirectory
-      ? urls
-          .from('shared.root')
-          .toDevice(
-            'localizedWithBaseDirectory.' + subKey,
-            baseDirectory,
-            ...urlArgs)
-      : urls
-          .from('shared.root')
-          .toDevice(fullKey, ...urlArgs);
-
-  // Needed for the rare path arguments which themselves contains one or more
-  // slashes, e.g. for listings, with arguments like 'albums/by-name'.
-  const subdirectoryPrefix =
-    '../'.repeat(urlArgs.join('/').split('/').length - 1);
-
-  const outputDirectory = path.join(outputPath, pathname);
-
-  const output = {
-    directory: outputDirectory,
-    documentHTML: path.join(outputDirectory, file),
-    oEmbedJSON: path.join(outputDirectory, OEMBED_JSON_FILE)
-  };
+async function main() {
+  Error.stackTraceLimit = Infinity;
 
-  return {
-    urlPath: [fullKey, ...urlArgs],
+  const selectedBuildModeFlags = Object.keys(
+    await parseOptions(process.argv.slice(2), {
+      [parseOptions.handleUnknown]: () => {},
 
-    output,
-    pathname,
-    subdirectoryPrefix,
-  };
-}
+      ...Object.fromEntries(Object.keys(buildModes)
+        .map((key) => [key, {type: 'flag'}])),
+    }));
 
-async function writeFavicon() {
-  try {
-    await stat(path.join(mediaPath, FAVICON_FILE));
-  } catch (error) {
-    return;
-  }
+  let selectedBuildModeFlag;
 
-  try {
-    await copyFile(
-      path.join(mediaPath, FAVICON_FILE),
-      path.join(outputPath, FAVICON_FILE)
-    );
-  } catch (error) {
-    logWarn`Failed to copy favicon! ${error.message}`;
+  if (empty(selectedBuildModeFlags)) {
+    selectedBuildModeFlag = 'static-build';
+    logInfo`No build mode specified, using default: ${selectedBuildModeFlag}`;
+  } 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;
+  } else {
+    selectedBuildModeFlag = selectedBuildModeFlags[0];
+    logInfo`Using specified build mode: ${selectedBuildModeFlag}`;
   }
 
-  logInfo`Copied favicon to site root.`;
-}
+  const selectedBuildMode = buildModes[selectedBuildModeFlag];
 
-function writeSymlinks() {
-  return progressPromiseAll('Writing site symlinks.', [
-    link(path.join(__dirname, UTILITY_DIRECTORY), 'shared.utilityRoot'),
-    link(path.join(__dirname, STATIC_DIRECTORY), 'shared.staticRoot'),
-    link(mediaPath, 'media.root'),
-  ]);
-
-  async function link(directory, urlKey) {
-    const pathname = urls.from('shared.root').toDevice(urlKey);
-    const file = path.join(outputPath, pathname);
-    try {
-      await unlink(file);
-    } catch (error) {
-      if (error.code !== 'ENOENT') {
-        throw error;
-      }
-    }
-    try {
-      await symlink(path.resolve(directory), file);
-    } catch (error) {
-      if (error.code === 'EPERM') {
-        await symlink(path.resolve(directory), file, 'junction');
-      }
-    }
-  }
-}
-
-function writeSharedFilesAndPages({language, wikiData}) {
-  const {groupData, wikiInfo} = wikiData;
-
-  const redirect = async (title, from, urlKey, directory) => {
-    const target = path.relative(
-      from,
-      urls.from('shared.root').to(urlKey, directory)
-    );
-    const content = generateRedirectHTML(title, target, {language});
-    await mkdir(path.join(outputPath, from), {recursive: true});
-    await writeFile(path.join(outputPath, from, 'index.html'), content);
+  // This is about to get a whole lot more stuff put in it.
+  const wikiData = {
+    listingSpec,
+    listingTargetSpec,
   };
 
-  return progressPromiseAll(`Writing files & pages shared across languages.`, [
-    groupData?.some((group) => group.directory === 'fandom') &&
-      redirect(
-        'Fandom - Gallery',
-        'albums/fandom',
-        'localized.groupGallery',
-        'fandom'
-      ),
-
-    groupData?.some((group) => group.directory === 'official') &&
-      redirect(
-        'Official - Gallery',
-        'albums/official',
-        'localized.groupGallery',
-        'official'
-      ),
-
-    wikiInfo.enableListings &&
-      redirect(
-        'Album Commentary',
-        'list/all-commentary',
-        'localized.commentaryIndex',
-        ''
-      ),
-
-    writeFile(
-      path.join(outputPath, 'data.json'),
-      (
-        '{\n' +
-        [
-          `"albumData": ${stringifyThings(wikiData.albumData)},`,
-          wikiInfo.enableFlashesAndGames &&
-            `"flashData": ${stringifyThings(wikiData.flashData)},`,
-          `"artistData": ${stringifyThings(wikiData.artistData)}`,
-        ]
-          .filter(Boolean)
-          .map(line => '  ' + line)
-          .join('\n') +
-        '\n}')),
-  ].filter(Boolean));
-}
-
-function generateRedirectHTML(title, target, {language}) {
-  return `<!DOCTYPE html>\n` + html.tag('html', [
-    html.tag('head', [
-      html.tag('title', language.$('redirectPage.title', {title})),
-      html.tag('meta', {charset: 'utf-8'}),
-
-      html.tag('meta', {
-        'http-equiv': 'refresh',
-        content: `0;url=${target}`,
-      }),
-
-      // TODO: Is this OK for localized pages?
-      html.tag('link', {
-        rel: 'canonical',
-        href: target,
-      }),
-    ]),
-
-    html.tag('body',
-      html.tag('main', [
-        html.tag('h1',
-          language.$('redirectPage.title', {title})),
-        html.tag('p',
-          language.$('redirectPage.infoLine', {
-            target: html.tag('a', {href: target}, target),
-          })),
-      ])),
-  ]);
-}
-
-// 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;
-
-  const entries = Object.entries(languagesToRun).filter(
-    ([key]) => key !== 'default'
-  );
-
-  for (let i = 0; i < entries.length; i++) {
-    const [_key, language] = entries[i];
-
-    await fn(language, i, entries);
-  }
-}
-
-async function main() {
-  Error.stackTraceLimit = Infinity;
-
-  const WD = wikiData;
-
-  WD.listingSpec = listingSpec;
-  WD.listingTargetSpec = listingTargetSpec;
+  const cliOptions = await parseOptions(process.argv.slice(2), {
+    ...selectedBuildMode.getCLIOptions(),
 
-  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!
@@ -1685,16 +171,6 @@ async function main() {
       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.
@@ -1715,20 +191,6 @@ async function main() {
       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
@@ -1770,12 +232,23 @@ async function main() {
     [parseOptions.handleUnknown]: () => {},
   });
 
-  dataPath = miscOptions['data-path'] || process.env.HSMUSIC_DATA;
-  mediaPath = miscOptions['media-path'] || process.env.HSMUSIC_MEDIA;
-  langPath = miscOptions['lang-path'] || process.env.HSMUSIC_LANG; // Can 8e left unset!
-  outputPath = miscOptions['out-path'] || process.env.HSMUSIC_OUT;
+  const 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!
+
+  const skipThumbs = cliOptions['skip-thumbs'] ?? false;
+  const thumbsOnly = cliOptions['thumbs-only'] ?? false;
+  const noBuild = cliOptions['no-build'] ?? false;
 
-  const writeOneLanguage = miscOptions['lang'];
+  const showAggregateTraces = cliOptions['show-traces'] ?? false;
+
+  const precacheData = cliOptions['precache-data'] ?? false;
+  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'] ?? 500);
 
   {
     let errored = false;
@@ -1787,46 +260,11 @@ async function main() {
     };
     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;
     }
   }
 
-  const appendIndexHTML = miscOptions['append-index-html'] ?? false;
-  if (appendIndexHTML) {
-    logWarn`Appending index.html to link hrefs. (Note: not recommended for production release!)`;
-    unbound_link.globalOptions.appendIndexHTML = true;
-  }
-
-  const skipThumbs = miscOptions['skip-thumbs'] ?? false;
-  const thumbsOnly = miscOptions['thumbs-only'] ?? false;
-  const noBuild = miscOptions['no-build'] ?? false;
-  const showAggregateTraces = miscOptions['show-traces'] ?? false;
-  const precacheData = miscOptions['precache-data'] ?? false;
-
-  // NOT for ena8ling or disa8ling specific features of the site!
-  // This is only in charge of what general groups of files to 8uild.
-  // They're here to make development quicker when you're only working
-  // on some particular area(s) of the site rather than making changes
-  // across all of them.
-  const writeFlags = await parseOptions(process.argv.slice(2), {
-    all: {type: 'flag'}, // Defaults to true if none 8elow specified.
-
-    // Kinda a hack t8h!
-    ...Object.fromEntries(
-      Object.keys(pageSpecs).map((key) => [key, {type: 'flag'}])
-    ),
-
-    [parseOptions.handleUnknown]: () => {},
-  });
-
-  const writeAll = !Object.keys(writeFlags).length || writeFlags.all;
-
-  logInfo`Writing site pages: ${
-    writeAll ? 'all' : Object.keys(writeFlags).join(', ')
-  }`;
-
   const niceShowAggregate = (error, ...opts) => {
     showAggregate(error, {
       showTraces: showAggregateTraces,
@@ -1850,9 +288,6 @@ async function main() {
     if (thumbsOnly) return;
   }
 
-  const showInvalidPropertyAccesses =
-    miscOptions['show-invalid-property-accesses'] ?? false;
-
   if (showInvalidPropertyAccesses) {
     CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true;
   }
@@ -1911,7 +346,7 @@ async function main() {
     }
   }
 
-  if (!WD.wikiInfo) {
+  if (!wikiData.wikiInfo) {
     logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`;
     return;
   }
@@ -1984,9 +419,7 @@ async function main() {
     progressCallAll('Caching all data values', Object.entries(wikiData)
       .filter(([key]) =>
         key !== 'listingSpec' &&
-        key !== 'listingTargetSpec' &&
-        key !== 'officialAlbumData' &&
-        key !== 'fandomAlbumData')
+        key !== 'listingTargetSpec')
       .map(([key, value]) =>
         key === 'wikiInfo' ? [key, [value]] :
         key === 'homepageLayout' ? [key, [value]] :
@@ -1996,8 +429,7 @@ async function main() {
   }
 
   const internalDefaultLanguage = await processLanguageFile(
-    path.join(__dirname, DEFAULT_STRINGS_FILE)
-  );
+    path.join(__dirname, DEFAULT_STRINGS_FILE));
 
   let languages;
   if (langPath) {
@@ -2005,28 +437,25 @@ async function main() {
       filter: (f) => path.extname(f) === '.json',
     });
 
-    const results = await progressPromiseAll(
-      `Reading & processing language files.`,
-      languageDataFiles.map((file) => processLanguageFile(file))
-    );
+    const results = await progressPromiseAll(`Reading & processing language files.`,
+      languageDataFiles.map((file) => processLanguageFile(file)));
 
     languages = Object.fromEntries(
-      results.map((language) => [language.code, language])
-    );
+      results.map((language) => [language.code, language]));
   } else {
     languages = {};
   }
 
   const customDefaultLanguage =
-    languages[WD.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code];
+    languages[wikiData.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!`;
+  } else if (wikiData.wikiInfo.defaultLanguage) {
+    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 {
@@ -2050,22 +479,15 @@ async function main() {
 
   if (noBuild) {
     logInfo`Not generating any site or page files this run (--no-build passed).`;
-  } else if (writeOneLanguage && !(writeOneLanguage in languages)) {
-    logError`Specified to write only ${writeOneLanguage}, but there is no strings file with this language code!`;
-    return;
-  } else if (writeOneLanguage) {
-    logInfo`Writing only language ${writeOneLanguage} this run.`;
-  } else {
-    logInfo`Writing all languages.`;
   }
 
   {
     const tagRefs = new Set(
-      [...WD.trackData, ...WD.albumData]
+      [...wikiData.trackData, ...wikiData.albumData]
         .flatMap((thing) => thing.artTagsByRef ?? []));
 
     for (const ref of tagRefs) {
-      if (find.artTag(ref, WD.artTagData)) {
+      if (find.artTag(ref, wikiData.artTagData)) {
         tagRefs.delete(ref);
       }
     }
@@ -2078,10 +500,7 @@ async function main() {
     }
   }
 
-  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);
 
   const fileSizePreloader = new FileSizePreloader();
 
@@ -2093,7 +512,7 @@ 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) =>
+    ...wikiData.albumData.flatMap((album) =>
       [
         ...(album.additionalFiles ?? []),
         ...album.tracks.flatMap((track) => track.additionalFiles ?? []),
@@ -2129,502 +548,55 @@ async function main() {
 
   if (noBuild) return;
 
-  // 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.
-  queueSize = +(miscOptions['queue-size'] ?? 500);
-
-  const buildDictionary = pageSpecs;
-
-  await writeFavicon();
-  await writeSymlinks();
-  await writeSharedFilesAndPages({language: finalDefaultLanguage, wikiData});
-
-  const buildSteps = writeAll
-    ? Object.entries(buildDictionary)
-    : Object.entries(buildDictionary).filter(([flag]) => writeFlags[flag]);
-
-  let writes;
-  {
-    let error = false;
-
-    const buildStepsWithTargets = buildSteps
-      .map(([flag, pageSpec]) => {
-        // Condition not met: skip this build step altogether.
-        if (pageSpec.condition && !pageSpec.condition({wikiData})) {
-          return null;
-        }
-
-        // May still call writeTargetless if present.
-        if (!pageSpec.targets) {
-          return {flag, pageSpec, targets: []};
-        }
-
-        if (!pageSpec.write) {
-          logError`${flag + '.targets'} is specified, but ${flag + '.write'} is missing!`;
-          error = true;
-          return null;
-        }
-
-        const targets = pageSpec.targets({wikiData});
-        if (!Array.isArray(targets)) {
-          logError`${flag + '.targets'} was called, but it didn't return an array! (${typeof targets})`;
-          error = true;
-          return null;
-        }
-
-        return {flag, pageSpec, targets};
-      })
-      .filter(Boolean);
-
-    if (error) {
-      return;
-    }
-
-    const validateWrites = (writes, fnName) => {
-      // Do a quick valid8tion! If one of the writeThingPages functions go
-      // wrong, this will stall out early and tell us which did.
-
-      if (!Array.isArray(writes)) {
-        logError`${fnName} didn't return an array!`;
-        error = true;
-        return false;
-      }
-
-      if (
-        !(
-          writes.every((obj) => typeof obj === 'object') &&
-          writes.every((obj) => {
-            const result = validateWriteObject(obj);
-            if (result.error) {
-              logError`Validating write object failed: ${result.error}`;
-              return false;
-            } else {
-              return true;
-            }
-          })
-        )
-      ) {
-        logError`${fnName} returned invalid entries!`;
-        error = true;
-        return false;
-      }
-
-      return true;
-    };
-
-    // return;
-
-    writes = progressCallAll('Computing page & data writes.', buildStepsWithTargets.flatMap(({flag, pageSpec, targets}) => {
-      const writesFns = targets.map(target => () => {
-        const writes = pageSpec.write(target, {wikiData})?.slice() || [];
-        return validateWrites(writes, flag + '.write') ? writes : [];
-      });
-
-      if (pageSpec.writeTargetless) {
-        writesFns.push(() => {
-          const writes = pageSpec.writeTargetless({wikiData});
-          return validateWrites(writes, flag + '.writeTargetless') ? writes : [];
-        });
-      }
-
-      return writesFns;
-    })).flat();
-
-    if (error) {
-      return;
-    }
-  }
-
-  const pageWrites = writes.filter(({type}) => type === 'page');
-  const dataWrites = writes.filter(({type}) => type === 'data');
-  const redirectWrites = writes.filter(({type}) => type === 'redirect');
-
-  if (writes.length) {
-    logInfo`Total of ${writes.length} writes returned. (${pageWrites.length} page, ${dataWrites.length} data [currently skipped], ${redirectWrites.length} redirect)`;
-  } else {
-    logWarn`No writes returned at all, so exiting early. This is probably a bug!`;
-    return;
-  }
+  const developersComment = generateDevelopersCommentHTML({
+    buildTime: BUILD_TIME,
+    commit: COMMIT,
+    wikiData,
+  });
 
-  /*
-  await progressPromiseAll(`Writing data files shared across languages.`, queue(
-    dataWrites.map(({path, data}) => () => {
-      const bound = {};
-
-      bound.serializeLink = bindOpts(serializeLink, {});
-
-      bound.serializeContribs = bindOpts(serializeContribs, {});
-
-      bound.serializeImagePaths = bindOpts(serializeImagePaths, {
-        thumb
-      });
-
-      bound.serializeCover = bindOpts(serializeCover, {
-        [bindOpts.bindIndex]: 2,
-        serializeImagePaths: bound.serializeImagePaths,
-        urls
-      });
-
-      bound.serializeGroupsForAlbum = bindOpts(serializeGroupsForAlbum, {
-        serializeLink
-      });
-
-      bound.serializeGroupsForTrack = bindOpts(serializeGroupsForTrack, {
-        serializeLink
-      });
-
-      // 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((props) => () => {
-        const {path, page} = props;
-
-        const pageSubKey = path[0];
-        const urlArgs = path.slice(1);
-
-        const localizedPaths = Object.fromEntries(
-          Object.entries(languages)
-            .filter(([key, language]) =>
-              key !== 'default' &&
-              !language.hidden)
-            .map(([_key, language]) => [
-              language.code,
-              getPagePaths({
-                baseDirectory:
-                  (language === finalDefaultLanguage
-                    ? ''
-                    : language.code),
-                fullKey: 'localized.' + pageSubKey,
-                urlArgs,
-              }),
-            ]));
-
-        const paths = getPagePaths({
-          baseDirectory,
-          fullKey: 'localized.' + pageSubKey,
-          urlArgs,
-        });
-
-        const to = getURLsFrom({
-          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.html = html;
-
-        bound.getColors = bindOpts(getColors, {
-          chroma,
-        });
-
-        bound.getLinkThemeString = bindOpts(unbound_getLinkThemeString, {
-          getColors: bound.getColors,
-        });
-
-        bound.getThemeString = bindOpts(unbound_getThemeString, {
-          getColors: bound.getColors,
-        });
-
-        bound.link = withEntries(unbound_link, (entries) =>
-          entries
-            .map(([key, fn]) => [key, bindOpts(fn, {
-              getLinkThemeString: bound.getLinkThemeString,
-              to,
-            })]));
-
-        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, {
-          html,
-          language,
-          to,
-        });
-
-        bound.fancifyURL = bindOpts(fancifyURL, {
-          html,
-          language,
-        });
-
-        bound.fancifyFlashURL = bindOpts(fancifyFlashURL, {
-          [bindOpts.bindIndex]: 2,
-          html,
-          language,
-
-          fancifyURL: bound.fancifyURL,
-        });
-
-        bound.getRevealStringFromWarnings = bindOpts(getRevealStringFromWarnings, {
-          html,
-          language,
-        });
-
-        bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, {
-          language,
-
-          getRevealStringFromWarnings: bound.getRevealStringFromWarnings,
-        });
-
-        bound.getArtistString = bindOpts(getArtistString, {
-          html,
-          link: bound.link,
-          language,
-
-          iconifyURL: bound.iconifyURL,
-        });
-
-        bound.getAlbumCover = bindOpts(getAlbumCover, {
-          to,
-        });
-
-        bound.getTrackCover = bindOpts(getTrackCover, {
-          to,
-        });
-
-        bound.getFlashCover = bindOpts(getFlashCover, {
-          to,
-        });
-
-        bound.getArtistAvatar = bindOpts(getArtistAvatar, {
-          to,
-        });
-
-        bound.generateAdditionalFilesShortcut = bindOpts(generateAdditionalFilesShortcut, {
-          html,
-          language,
-        });
-
-        bound.generateAdditionalFilesList = bindOpts(generateAdditionalFilesList, {
-          html,
-          language,
-        });
-
-        bound.generateNavigationLinks = bindOpts(generateNavigationLinks, {
-          link: bound.link,
-          language,
-        });
-
-        bound.generateStickyHeadingContainer = bindOpts(generateStickyHeadingContainer, {
-          [bindOpts.bindIndex]: 0,
-          getRevealStringFromTags: bound.getRevealStringFromTags,
-          html,
-          img,
-        });
-
-        bound.generateChronologyLinks = bindOpts(generateChronologyLinks, {
-          html,
-          language,
-          link: bound.link,
-          wikiData,
-
-          generateNavigationLinks: bound.generateNavigationLinks,
-        });
-
-        bound.generateCoverLink = bindOpts(generateCoverLink, {
-          [bindOpts.bindIndex]: 0,
-          html,
-          img,
-          link: bound.link,
-          language,
-          to,
-          wikiData,
-
-          getRevealStringFromTags: bound.getRevealStringFromTags,
-        });
-
-        bound.generateInfoGalleryLinks = bindOpts(generateInfoGalleryLinks, {
-          [bindOpts.bindIndex]: 2,
-          link: bound.link,
-          language,
-        });
-
-        bound.generateTrackListDividedByGroups = bindOpts(generateTrackListDividedByGroups, {
-          html,
-          language,
-          wikiData,
-        });
-
-        bound.getGridHTML = bindOpts(getGridHTML, {
-          [bindOpts.bindIndex]: 0,
-          img,
-          html,
-          language,
-
-          getRevealStringFromTags: bound.getRevealStringFromTags,
-        });
-
-        bound.getAlbumGridHTML = bindOpts(getAlbumGridHTML, {
-          [bindOpts.bindIndex]: 0,
-          link: bound.link,
-          language,
-
-          getAlbumCover: bound.getAlbumCover,
-          getGridHTML: bound.getGridHTML,
-        });
-
-        bound.getFlashGridHTML = bindOpts(getFlashGridHTML, {
-          [bindOpts.bindIndex]: 0,
-          link: bound.link,
-
-          getFlashCover: bound.getFlashCover,
-          getGridHTML: bound.getGridHTML,
-        });
-
-        bound.getCarouselHTML = bindOpts(getCarouselHTML, {
-          [bindOpts.bindIndex]: 0,
-          img,
-          html,
-        })
-
-        bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, {
-          to,
-        });
-
-        const pageInfo = page({
-          ...bound,
-
-          language,
-
-          absoluteTo,
-          relativeTo: to,
-          to,
-          urls,
-
-          getSizeOfAdditionalFile,
-        });
-
-        const oEmbedJSON = generateOEmbedJSON(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 pageHTML = generateDocumentHTML(pageInfo, {
-          defaultLanguage: finalDefaultLanguage,
-          getThemeString: bound.getThemeString,
-          language,
-          languages,
-          localizedPaths,
-          oEmbedJSONHref,
-          paths,
-          to,
-          transformMultiline: bound.transformMultiline,
-          wikiData,
-        });
-
-        return writePage({
-          html: pageHTML,
-          oEmbedJSON,
-          paths,
-        });
-      }),
-      ...redirectWrites.map(({fromPath, toPath, title: titleFn}) => () => {
-        const title = titleFn({
-          language,
-        });
-
-        const from = getPagePaths({
-          baseDirectory,
-          fullKey: 'localized.' + fromPath[0],
-          urlArgs: fromPath.slice(1),
-        });
-
-        const to = getURLsFrom({
-          baseDirectory,
-          pageSubKey: fromPath[0],
-          paths: from,
-        });
-
-        const target = to('localized.' + toPath[0], ...toPath.slice(1));
-        const html = generateRedirectHTML(title, target, {language});
-        return writePage({html, paths: from});
-      }),
-    ], queueSize));
-  };
+  return selectedBuildMode.go({
+    cliOptions,
+    dataPath,
+    mediaPath,
+    queueSize,
+    srcRootPath: __dirname,
 
-  await wrapLanguages(perLanguageFn, {
+    defaultLanguage: finalDefaultLanguage,
     languages,
-    writeOneLanguage,
-  });
+    wikiData,
+    urls,
+    urlSpec,
 
-  // The single most important step.
-  logInfo`Written!`;
+    cachebust: '?' + CACHEBUST,
+    developersComment,
+    getSizeOfAdditionalFile,
+  });
 }
 
 // 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') {
-  main()
-    .catch((error) => {
+  (async () => {
+    let result;
+
+    try {
+      result = await main();
+    } catch (error) {
       if (error instanceof AggregateError) {
         showAggregate(error);
       } else {
         console.error(error);
       }
-    })
-    .then(() => {
-      decorateTime.displayTime();
-      CacheableObject.showInvalidAccesses();
-    });
+    }
+
+    if (result !== true) {
+      process.exit(1);
+      return;
+    }
+
+    decorateTime.displayTime();
+    CacheableObject.showInvalidAccesses();
+
+    process.exit(0);
+  })();
 }
diff --git a/src/util/magic-constants.js b/src/util/magic-constants.js
index 73fdbc6..83dd7db 100644
--- a/src/util/magic-constants.js
+++ b/src/util/magic-constants.js
@@ -7,4 +7,3 @@
 // (TM).
 
 export const OFFICIAL_GROUP_DIRECTORY = 'official';
-export const FANDOM_GROUP_DIRECTORY = 'fandom';
diff --git a/src/util/transform-content.js b/src/util/transform-content.js
new file mode 100644
index 0000000..d1d0f51
--- /dev/null
+++ b/src/util/transform-content.js
@@ -0,0 +1,453 @@
+// See also replacer.js, which covers the actual syntax parser and node
+// interpreter. This file works with replacer.js to provide higher-level
+// interfaces for converting various content found in wiki data to HTML for
+// display on the site.
+
+import * as html from './html.js';
+export {transformInline} from './replacer.js';
+
+export const replacerSpec = {
+  album: {
+    find: 'album',
+    link: 'album',
+  },
+  'album-commentary': {
+    find: 'album',
+    link: 'albumCommentary',
+  },
+  'album-gallery': {
+    find: 'album',
+    link: 'albumGallery',
+  },
+  artist: {
+    find: 'artist',
+    link: 'artist',
+  },
+  'artist-gallery': {
+    find: 'artist',
+    link: 'artistGallery',
+  },
+  'commentary-index': {
+    find: null,
+    link: 'commentaryIndex',
+  },
+  date: {
+    find: null,
+    value: (ref) => new Date(ref),
+    html: (date, {language}) =>
+      html.tag('time',
+        {datetime: date.toString()},
+        language.formatDate(date)),
+  },
+  'flash-index': {
+    find: null,
+    link: 'flashIndex',
+  },
+  flash: {
+    find: 'flash',
+    link: 'flash',
+    transformName(name, node, input) {
+      const nextCharacter = input[node.iEnd];
+      const lastCharacter = name[name.length - 1];
+      if (![' ', '\n', '<'].includes(nextCharacter) && lastCharacter === '.') {
+        return name.slice(0, -1);
+      } else {
+        return name;
+      }
+    },
+  },
+  group: {
+    find: 'group',
+    link: 'groupInfo',
+  },
+  'group-gallery': {
+    find: 'group',
+    link: 'groupGallery',
+  },
+  home: {
+    find: null,
+    link: 'home',
+  },
+  'listing-index': {
+    find: null,
+    link: 'listingIndex',
+  },
+  listing: {
+    find: 'listing',
+    link: 'listing',
+  },
+  media: {
+    find: null,
+    link: 'media',
+  },
+  'news-index': {
+    find: null,
+    link: 'newsIndex',
+  },
+  'news-entry': {
+    find: 'newsEntry',
+    link: 'newsEntry',
+  },
+  root: {
+    find: null,
+    link: 'root',
+  },
+  site: {
+    find: null,
+    link: 'site',
+  },
+  static: {
+    find: 'staticPage',
+    link: 'staticPage',
+  },
+  string: {
+    find: null,
+    value: (ref) => ref,
+    html: (ref, {language, args}) => language.$(ref, args),
+  },
+  tag: {
+    find: 'artTag',
+    link: 'tag',
+  },
+  track: {
+    find: 'track',
+    link: 'track',
+  },
+};
+
+function splitLines(text) {
+  return text.split(/\r\n|\r|\n/);
+}
+
+function joinLineBreaks(sourceLines) {
+  const outLines = [];
+
+  let lineSoFar = '';
+  for (let i = 0; i < sourceLines.length; i++) {
+    const line = sourceLines[i];
+    lineSoFar += line;
+    if (!line.endsWith('<br>')) {
+      outLines.push(lineSoFar);
+      lineSoFar = '';
+    }
+  }
+
+  if (lineSoFar) {
+    outLines.push(lineSoFar);
+  }
+
+  return outLines;
+}
+
+function parseAttributes(string, {to}) {
+  const attributes = Object.create(null);
+  const skipWhitespace = (i) => {
+    const ws = /\s/;
+    if (ws.test(string[i])) {
+      const match = string.slice(i).match(/[^\s]/);
+      if (match) {
+        return i + match.index;
+      } else {
+        return string.length;
+      }
+    } else {
+      return i;
+    }
+  };
+
+  for (let i = 0; i < string.length; ) {
+    i = skipWhitespace(i);
+    const aStart = i;
+    const aEnd = i + string.slice(i).match(/[\s=]|$/).index;
+    const attribute = string.slice(aStart, aEnd);
+    i = skipWhitespace(aEnd);
+    if (string[i] === '=') {
+      i = skipWhitespace(i + 1);
+      let end, endOffset;
+      if (string[i] === '"' || string[i] === "'") {
+        end = string[i];
+        endOffset = 1;
+        i++;
+      } else {
+        end = '\\s';
+        endOffset = 0;
+      }
+      const vStart = i;
+      const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index;
+      const value = string.slice(vStart, vEnd);
+      i = vEnd + endOffset;
+      if (attribute === 'src' && value.startsWith('media/')) {
+        attributes[attribute] = to('media.path', value.slice('media/'.length));
+      } else {
+        attributes[attribute] = value;
+      }
+    } else {
+      attributes[attribute] = attribute;
+    }
+  }
+  return Object.fromEntries(
+    Object.entries(attributes).map(([key, val]) => [
+      key,
+      val === 'true'
+        ? true
+        : val === 'false'
+        ? false
+        : val === key
+        ? true
+        : val,
+    ])
+  );
+}
+
+function unbound_transformMultiline(text, {
+  img,
+  to,
+  transformInline,
+
+  thumb = null,
+}) {
+  // Heck yes, HTML magics.
+
+  text = transformInline(text.trim());
+
+  const outLines = [];
+
+  const indentString = ' '.repeat(4);
+
+  let levelIndents = [];
+  const openLevel = (indent) => {
+    // opening a sublist is a pain: to be semantically *and* visually
+    // correct, we have to append the <ul> at the end of the existing
+    // previous <li>
+    const previousLine = outLines[outLines.length - 1];
+    if (previousLine?.endsWith('</li>')) {
+      // we will re-close the <li> later
+      outLines[outLines.length - 1] = previousLine.slice(0, -5) + ' <ul>';
+    } else {
+      // if the previous line isn't a list item, this is the opening of
+      // the first list level, so no need for indent
+      outLines.push('<ul>');
+    }
+    levelIndents.push(indent);
+  };
+  const closeLevel = () => {
+    levelIndents.pop();
+    if (levelIndents.length) {
+      // closing a sublist, so close the list item containing it too
+      outLines.push(indentString.repeat(levelIndents.length) + '</ul></li>');
+    } else {
+      // closing the final list level! no need for indent here
+      outLines.push('</ul>');
+    }
+  };
+
+  // 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,
+        ...parseAttributes(attributes, {to}),
+      })
+    );
+
+    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('\\>', '>');
+      }
+    }
+
+    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 = '';
+      }
+
+      // for sticky headings!
+      if (elementMatch && elementMatch[1] === 'h2') {
+        lineContent = lineContent.replace(/<h2(.*?)>/g, (match, attributes) => {
+          const parsedAttributes = parseAttributes(attributes, {to});
+          return `<h2 ${html.attributes({
+            ...parsedAttributes,
+            class: [...parsedAttributes.class?.split(' ') ?? [], 'content-heading'],
+          })}>`;
+        });
+      }
+    }
+
+    let pushString = indentString.repeat(indentThisLine);
+    if (lineTag) {
+      pushString += `<${lineTag}>${lineContent}</${lineTag}>`;
+    } else {
+      pushString += lineContent;
+    }
+    outLines.push(pushString);
+  }
+
+  // after processing all lines...
+
+  // if still in a list, close all levels
+  while (levelIndents.length) closeLevel();
+
+  // if still in a blockquote, close its tag
+  if (inBlockquote) {
+    inBlockquote = false;
+    outLines.push('</blockquote>');
+  }
+
+  return outLines.join('\n');
+}
+
+function unbound_transformLyrics(text, {
+  transformInline,
+  transformMultiline,
+}) {
+  // Different from transformMultiline 'cuz it joins multiple lines together
+  // with line 8reaks (<br>); transformMultiline treats each line as its own
+  // complete paragraph (or list, etc).
+
+  // If it looks like old data, then like, oh god.
+  // Use the normal transformMultiline tool.
+  if (text.includes('<br')) {
+    return transformMultiline(text);
+  }
+
+  text = transformInline(text.trim());
+
+  let buildLine = '';
+  const addLine = () => outLines.push(`<p>${buildLine}</p>`);
+  const outLines = [];
+  for (const line of text.split('\n')) {
+    if (line.length) {
+      if (buildLine.length) {
+        buildLine += '<br>';
+      }
+      buildLine += line;
+    } else if (buildLine.length) {
+      addLine();
+      buildLine = '';
+    }
+  }
+  if (buildLine.length) {
+    addLine();
+  }
+  return outLines.join('\n');
+}
+
+export {
+  unbound_transformLyrics as transformLyrics,
+  unbound_transformMultiline as transformMultiline
+}
diff --git a/src/util/urls.js b/src/util/urls.js
index 1f9cd9c..c2119b8 100644
--- a/src/util/urls.js
+++ b/src/util/urls.js
@@ -136,3 +136,121 @@ export const thumb = {
   medium: thumbnailHelper('.medium'),
   small: thumbnailHelper('.small'),
 };
+
+// Makes the generally-used and wiki-specialized "to" page utility.
+// "to" returns a relative path from the current page to the target.
+export function getURLsFrom({
+  baseDirectory,
+  pagePath,
+  urls,
+}) {
+  const pageSubKey = pagePath[0];
+  const subdirectoryPrefix = getPageSubdirectoryPrefix({pagePath});
+
+  return (targetFullKey, ...args) => {
+    const [groupKey, subKey] = targetFullKey.split('.');
+    let from, 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;
+    }
+
+    return (
+      subdirectoryPrefix +
+      urls.from(from).to(to, ...args));
+  };
+}
+
+// Makes the generally-used and wiki-specialized "absoluteTo" page utility.
+// "absoluteTo" returns an absolute path, starting at site root (/) leading
+// to the target.
+export function getURLsFromRoot({
+  baseDirectory,
+  urls,
+}) {
+  const {to} = urls.from('shared.root');
+
+  return (targetFullKey, ...args) => {
+    const [groupKey, subKey] = targetFullKey.split('.');
+    return (
+      '/' +
+      (groupKey === 'localized' && baseDirectory
+        ? to(
+            'localizedWithBaseDirectory.' + subKey,
+            baseDirectory,
+            ...args
+          )
+        : to(targetFullKey, ...args))
+    );
+  };
+}
+
+export function getPagePathname({
+  baseDirectory,
+  device = false,
+  pagePath,
+  urls,
+}) {
+  const {[device ? 'toDevice' : 'to']: to} = urls.from('shared.root');
+
+  return (baseDirectory
+    ? to('localizedWithBaseDirectory.' + pagePath[0], baseDirectory, ...pagePath.slice(1))
+    : to('localized.' + pagePath[0], ...pagePath.slice(1)));
+}
+
+export function getPagePathnameAcrossLanguages({
+  defaultLanguage,
+  languages,
+  pagePath,
+  urls,
+}) {
+  return withEntries(languages, entries => entries
+    .filter(([key, language]) => key !== 'default' && !language.hidden)
+    .map(([_key, language]) => [
+      language.code,
+      getPagePathname({
+        baseDirectory:
+          (language === defaultLanguage
+            ? ''
+            : language.code),
+        pagePath,
+        urls,
+      }),
+    ]));
+}
+
+// Needed for the rare path arguments which themselves contains one or more
+// slashes, e.g. for listings, with arguments like 'albums/by-name'.
+export function getPageSubdirectoryPrefix({
+  pagePath,
+}) {
+  const timesNestedDeeply = (pagePath
+    .slice(1) // skip URL key, only check arguments
+    .join('/')
+    .split('/')
+    .length - 1);
+  return '../'.repeat(timesNestedDeeply);
+}
diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js
new file mode 100644
index 0000000..6212b82
--- /dev/null
+++ b/src/write/bind-utilities.js
@@ -0,0 +1,272 @@
+// Ties lots and lots of functions together in a convenient package accessible
+// to page write functions. This is kept in a separate file from other write
+// areas to keep imports neat and isolated.
+
+import chroma from 'chroma-js';
+
+import {
+  fancifyFlashURL,
+  fancifyURL,
+  getAlbumGridHTML,
+  getAlbumStylesheet,
+  getArtistString,
+  getCarouselHTML,
+  getFlashGridHTML,
+  getGridHTML,
+  getRevealStringFromTags,
+  getRevealStringFromWarnings,
+  getThemeString,
+  generateAdditionalFilesList,
+  generateAdditionalFilesShortcut,
+  generateChronologyLinks,
+  generateCoverLink,
+  generateInfoGalleryLinks,
+  generateTrackListDividedByGroups,
+  generateNavigationLinks,
+  generateStickyHeadingContainer,
+  iconifyURL,
+  img,
+} from '../misc-templates.js';
+
+import {
+  replacerSpec,
+  transformInline,
+  transformLyrics,
+  transformMultiline,
+} from '../util/transform-content.js';
+
+import * as html from '../util/html.js';
+
+import {bindOpts, withEntries} from '../util/sugar.js';
+import {getColors} from '../util/colors.js';
+import {bindFind} from '../util/find.js';
+
+import link, {getLinkThemeString} from '../util/link.js';
+
+import {
+  getAlbumCover,
+  getArtistAvatar,
+  getFlashCover,
+  getTrackCover,
+} from '../util/wiki-data.js';
+
+export function bindUtilities({
+  absoluteTo,
+  defaultLanguage,
+  getSizeOfAdditionalFile,
+  language,
+  languages,
+  to,
+  urls,
+  wikiData,
+}) {
+  // TODO: Is there some nicer way to define these,
+  // may8e without totally re-8inding everything for
+  // each page?
+  const bound = {};
+
+  Object.assign(bound, {
+    absoluteTo,
+    defaultLanguage,
+    getSizeOfAdditionalFile,
+    html,
+    language,
+    languages,
+    to,
+    urls,
+    wikiData,
+  })
+
+  bound.img = bindOpts(img, {
+    [bindOpts.bindIndex]: 0,
+    html,
+  });
+
+  bound.getColors = bindOpts(getColors, {
+    chroma,
+  });
+
+  bound.getLinkThemeString = bindOpts(getLinkThemeString, {
+    getColors: bound.getColors,
+  });
+
+  bound.getThemeString = bindOpts(getThemeString, {
+    getColors: bound.getColors,
+  });
+
+  bound.link = withEntries(link, (entries) =>
+    entries
+      .map(([key, fn]) => [key, bindOpts(fn, {
+        getLinkThemeString: bound.getLinkThemeString,
+        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, {
+    img: bound.img,
+    to,
+    transformInline: bound.transformInline,
+  });
+
+  bound.transformLyrics = bindOpts(transformLyrics, {
+    transformInline: bound.transformInline,
+    transformMultiline: bound.transformMultiline,
+  });
+
+  bound.iconifyURL = bindOpts(iconifyURL, {
+    html,
+    language,
+    to,
+  });
+
+  bound.fancifyURL = bindOpts(fancifyURL, {
+    html,
+    language,
+  });
+
+  bound.fancifyFlashURL = bindOpts(fancifyFlashURL, {
+    [bindOpts.bindIndex]: 2,
+    html,
+    language,
+
+    fancifyURL: bound.fancifyURL,
+  });
+
+  bound.getRevealStringFromWarnings = bindOpts(getRevealStringFromWarnings, {
+    html,
+    language,
+  });
+
+  bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, {
+    language,
+
+    getRevealStringFromWarnings: bound.getRevealStringFromWarnings,
+  });
+
+  bound.getArtistString = bindOpts(getArtistString, {
+    html,
+    link: bound.link,
+    language,
+
+    iconifyURL: bound.iconifyURL,
+  });
+
+  bound.getAlbumCover = bindOpts(getAlbumCover, {
+    to,
+  });
+
+  bound.getTrackCover = bindOpts(getTrackCover, {
+    to,
+  });
+
+  bound.getFlashCover = bindOpts(getFlashCover, {
+    to,
+  });
+
+  bound.getArtistAvatar = bindOpts(getArtistAvatar, {
+    to,
+  });
+
+  bound.generateAdditionalFilesShortcut = bindOpts(generateAdditionalFilesShortcut, {
+    html,
+    language,
+  });
+
+  bound.generateAdditionalFilesList = bindOpts(generateAdditionalFilesList, {
+    html,
+    language,
+  });
+
+  bound.generateNavigationLinks = bindOpts(generateNavigationLinks, {
+    link: bound.link,
+    language,
+  });
+
+  bound.generateStickyHeadingContainer = bindOpts(generateStickyHeadingContainer, {
+    [bindOpts.bindIndex]: 0,
+    getRevealStringFromTags: bound.getRevealStringFromTags,
+    html,
+    img: bound.img,
+  });
+
+  bound.generateChronologyLinks = bindOpts(generateChronologyLinks, {
+    html,
+    language,
+    link: bound.link,
+    wikiData,
+
+    generateNavigationLinks: bound.generateNavigationLinks,
+  });
+
+  bound.generateCoverLink = bindOpts(generateCoverLink, {
+    [bindOpts.bindIndex]: 0,
+    html,
+    img: bound.img,
+    link: bound.link,
+    language,
+    to,
+    wikiData,
+
+    getRevealStringFromTags: bound.getRevealStringFromTags,
+  });
+
+  bound.generateInfoGalleryLinks = bindOpts(generateInfoGalleryLinks, {
+    [bindOpts.bindIndex]: 2,
+    link: bound.link,
+    language,
+  });
+
+  bound.generateTrackListDividedByGroups = bindOpts(generateTrackListDividedByGroups, {
+    html,
+    language,
+    wikiData,
+  });
+
+  bound.getGridHTML = bindOpts(getGridHTML, {
+    [bindOpts.bindIndex]: 0,
+    img: bound.img,
+    html,
+    language,
+
+    getRevealStringFromTags: bound.getRevealStringFromTags,
+  });
+
+  bound.getAlbumGridHTML = bindOpts(getAlbumGridHTML, {
+    [bindOpts.bindIndex]: 0,
+    link: bound.link,
+    language,
+
+    getAlbumCover: bound.getAlbumCover,
+    getGridHTML: bound.getGridHTML,
+  });
+
+  bound.getFlashGridHTML = bindOpts(getFlashGridHTML, {
+    [bindOpts.bindIndex]: 0,
+    link: bound.link,
+
+    getFlashCover: bound.getFlashCover,
+    getGridHTML: bound.getGridHTML,
+  });
+
+  bound.getCarouselHTML = bindOpts(getCarouselHTML, {
+    [bindOpts.bindIndex]: 0,
+    img: bound.img,
+    html,
+  })
+
+  bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, {
+    to,
+  });
+
+  return bound;
+}
diff --git a/src/write/build-modes/index.js b/src/write/build-modes/index.js
new file mode 100644
index 0000000..91e3900
--- /dev/null
+++ b/src/write/build-modes/index.js
@@ -0,0 +1,2 @@
+export * as 'live-dev-server' from './live-dev-server.js';
+export * as 'static-build' from './static-build.js';
diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js
new file mode 100644
index 0000000..81ae5b8
--- /dev/null
+++ b/src/write/build-modes/live-dev-server.js
@@ -0,0 +1,373 @@
+import * as http from 'http';
+import {createReadStream} from 'fs';
+import {stat} from 'fs/promises';
+import * as path from 'path';
+import {pipeline} from 'stream/promises'
+
+import {bindUtilities} from '../bind-utilities.js';
+
+import {serializeThings} from '../../data/serialize.js';
+
+import * as pageSpecs from '../../page/index.js';
+
+import {logInfo, logWarn, progressCallAll} from '../../util/cli.js';
+
+import {
+  getPagePathname,
+  getPagePathnameAcrossLanguages,
+  getURLsFrom,
+  getURLsFromRoot,
+} from '../../util/urls.js';
+
+import {
+  generateDocumentHTML,
+  generateGlobalWikiDataJSON,
+  generateRedirectHTML,
+} from '../page-template.js';
+
+export function getCLIOptions() {
+  return {
+    host: {
+      type: 'value',
+    },
+
+    port: {
+      type: 'value',
+      validate(size) {
+        if (parseInt(size) !== parseFloat(size)) return 'an integer';
+        if (parseInt(size) < 1024 || parseInt(size) > 49151) return 'a user/registered port (1024-49151)';
+        return true;
+      },
+    },
+  };
+}
+
+export async function go({
+  cliOptions,
+  _dataPath,
+  mediaPath,
+
+  defaultLanguage,
+  languages,
+  srcRootPath,
+  urls,
+  wikiData,
+
+  cachebust,
+  developersComment,
+  getSizeOfAdditionalFile,
+}) {
+  const host = cliOptions['host'] ?? '0.0.0.0';
+  const port = parseInt(cliOptions['port'] ?? 8002);
+
+  let targetSpecPairs = getPageSpecsWithTargets({wikiData});
+  const pages = progressCallAll(`Computing page data & paths for ${targetSpecPairs.length} targets.`,
+    targetSpecPairs.map(({
+      pageSpec,
+      target,
+      targetless,
+    }) => () =>
+      targetless
+        ? pageSpec.writeTargetless({wikiData})
+        : pageSpec.write(target, {wikiData}))).flat();
+
+  logInfo`Will be serving a total of ${pages.length} pages.`;
+
+  const urlToPageMap = Object.fromEntries(pages
+    .filter(page => page.type === 'page' || page.type === 'redirect')
+    .flatMap(page => {
+      let servePath;
+      if (page.type === 'page')
+        servePath = page.path;
+      else if (page.type === 'redirect')
+        servePath = page.fromPath;
+
+      return Object.values(languages).map(language => {
+        const baseDirectory =
+          language === defaultLanguage ? '' : language.code;
+
+        const pathname = getPagePathname({
+          baseDirectory,
+          pagePath: servePath,
+          urls,
+        });
+
+        return [pathname, {
+          baseDirectory,
+          language,
+          page,
+          servePath,
+        }];
+      });
+    }));
+
+  const server = http.createServer(async (request, response) => {
+    const contentTypeHTML = {'Content-Type': 'text/html; charset=utf-8'};
+    const contentTypeJSON = {'Content-Type': 'application/json; charset=utf-8'};
+    const contentTypePlain = {'Content-Type': 'text/plain; charset=utf-8'};
+
+    const requestTime = new Date().toLocaleDateString('en-US', {hour: '2-digit', minute: '2-digit', second: '2-digit'});
+    const requestHead = `${requestTime} - ${request.socket.remoteAddress}`;
+
+    let url;
+    try {
+      url = new URL(request.url, `http://${request.headers.host}`);
+    } catch (error) {
+      response.writeHead(500, contentTypePlain);
+      response.end('Failed to parse request URL\n');
+      return;
+    }
+
+    const {pathname} = url;
+
+    // Specialized routes
+
+    if (pathname === '/data.json') {
+      try {
+        const json = generateGlobalWikiDataJSON({
+          serializeThings,
+          wikiData,
+        });
+        response.writeHead(200, contentTypeJSON);
+        response.end(json);
+        console.log(`${requestHead} [200] /data.json`);
+      } catch (error) {
+        response.writeHead(500, contentTypeJSON);
+        response.end({error: `Internal error serializing wiki JSON`});
+        console.error(`${requestHead} [500] /data.json`);
+        console.error(error);
+      }
+      return;
+    }
+
+    const {
+      area: localFileArea,
+      path: localFilePath
+    } = pathname.match(/^\/(?<area>static|util|media)\/(?<path>.*)/)?.groups ?? {};
+
+    if (localFileArea) {
+      // Not security tested, man, this is a dev server!!
+      const safePath = path.posix.resolve('/', localFilePath).replace(/^\//, '');
+
+      let localDirectory;
+      if (localFileArea === 'static' || localFileArea === 'util') {
+        localDirectory = path.join(srcRootPath, localFileArea);
+      } else if (localFileArea === 'media') {
+        localDirectory = mediaPath;
+      }
+
+      const filePath = path.resolve(localDirectory, safePath.split('/').join(path.sep));
+
+      try {
+        await stat(filePath);
+      } catch (error) {
+        if (error.code === 'ENOENT') {
+          response.writeHead(404, contentTypePlain);
+          response.end(`No ${localFileArea} file found for: ${safePath}`);
+          console.log(`${requestHead} [404] ${pathname}`);
+          console.log(`ENOENT for stat: ${filePath}`);
+        } else {
+          response.writeHead(500, contentTypePlain);
+          response.end(`Internal error accessing ${localFileArea} file for: ${safePath}`);
+          console.error(`${requestHead} [500] ${pathname}`);
+          console.error(error);
+        }
+        return;
+      }
+
+      const extname = path.extname(safePath).slice(1).toLowerCase();
+
+      const contentType = {
+        // BRB covering all my bases
+        'aac': 'audio/aac',
+        'bmp': 'image/bmp',
+        'css': 'text/css',
+        'csv': 'text/csv',
+        'gif': 'image/gif',
+        'ico': 'image/vnd.microsoft.icon',
+        'jpg': 'image/jpeg',
+        'jpeg:': 'image/jpeg',
+        'js': 'text/javascript',
+        'mjs': 'text/javascript',
+        'mp3': 'audio/mpeg',
+        'mp4': 'video/mp4',
+        'oga': 'audio/ogg',
+        'ogg': 'audio/ogg',
+        'ogv': 'video/ogg',
+        'opus': 'audio/opus',
+        'png': 'image/png',
+        'pdf': 'application/pdf',
+        'svg': 'image/svg+xml',
+        'ttf': 'font/ttf',
+        'txt': 'text/plain',
+        'wav': 'audio/wav',
+        'weba': 'audio/webm',
+        'webm': 'video/webm',
+        'woff': 'font/woff',
+        'woff2': 'font/woff2',
+        'xml': 'application/xml',
+        'zip': 'application/zip',
+      }[extname];
+
+      try {
+        response.writeHead(200, contentType ? {'Content-Type': contentType} : {});
+        await pipeline(
+          createReadStream(filePath),
+          response);
+        console.log(`${requestHead} [200] ${pathname}`);
+      } catch (error) {
+        response.writeHead(500, contentTypePlain);
+        response.end(`Failed during file-to-response pipeline`);
+        console.error(`${requestHead} [500] ${pathname}`);
+        console.error(error);
+      }
+      return;
+    }
+
+    // Other routes determined by page and URL specs
+
+    // URL to page map expects trailing slash but no leading slash.
+    const pathnameKey = pathname.replace(/^\//, '') + (pathname.endsWith('/') ? '' : '/');
+
+    if (!Object.hasOwn(urlToPageMap, pathnameKey)) {
+      response.writeHead(404, contentTypePlain);
+      response.end(`No page found for: ${pathnameKey}\n`);
+      console.log(`${requestHead} [404] ${pathname}`);
+      return;
+    }
+
+    // All pages expect to be served at a URL with a trailing slash, which must
+    // be fulfilled for relative URLs (ex. href="../lofam5/") to work. Redirect
+    // if there is no trailing slash in the request URL.
+    if (!pathname.endsWith('/')) {
+      const target = pathname + '/';
+      response.writeHead(301, {
+        ...contentTypePlain,
+        'Location': target,
+      });
+      response.end(`Redirecting to: ${target}\n`);
+      console.log(`${requestHead} [301] (trl. slash) ${pathname}`);
+      return;
+    }
+
+    const {
+      baseDirectory,
+      language,
+      page,
+      servePath,
+    } = urlToPageMap[pathnameKey];
+
+    const to = getURLsFrom({
+      baseDirectory,
+      pagePath: servePath,
+      urls,
+    });
+
+    const absoluteTo = getURLsFromRoot({
+      baseDirectory,
+      urls,
+    });
+
+    try {
+      if (page.type === 'redirect') {
+        const target = to('localized.' + page.toPath[0], ...page.toPath.slice(1));
+
+        response.writeHead(301, {
+          ...contentTypeHTML,
+          'Location': target,
+        });
+
+        const redirectHTML = generateRedirectHTML(page.title, target, {language});
+
+        response.end(redirectHTML);
+
+        console.log(`${requestHead} [301] (redirect) ${pathname}`);
+        return;
+      }
+
+      response.writeHead(200, contentTypeHTML);
+
+      const localizedPathnames = getPagePathnameAcrossLanguages({
+        defaultLanguage,
+        languages,
+        pagePath: servePath,
+        urls,
+      });
+
+      const bound = bindUtilities({
+        absoluteTo,
+        defaultLanguage,
+        getSizeOfAdditionalFile,
+        language,
+        languages,
+        to,
+        urls,
+        wikiData,
+      });
+
+      const pageInfo = page.page(bound);
+
+      const pageHTML = generateDocumentHTML(pageInfo, {
+        ...bound,
+        cachebust,
+        developersComment,
+        localizedPathnames,
+        oEmbedJSONHref: null, // No oEmbed support for live dev server
+        pagePath: servePath,
+        pathname,
+      });
+
+      console.log(`${requestHead} [200] ${pathname}`);
+      response.end(pageHTML);
+    } catch (error) {
+      response.writeHead(500, contentTypePlain);
+      response.end(`Error generating page, view server log for details\n`);
+      console.error(`${requestHead} [500] ${pathname}`);
+      console.error(error);
+    }
+  });
+
+  const address = `http://${host}:${port}/`;
+
+  server.on('error', error => {
+    if (error.code === 'EADDRINUSE') {
+      logWarn`Port ${port} is already in use - will (continually) retry after 10 seconds.`;
+      logWarn`Press ^C here (control+C) to exit and change ${'--port'} number, or stop the server currently running on port ${port}.`;
+      setTimeout(() => {
+        server.close();
+        server.listen(port, host);
+      }, 10_000);
+    } else {
+      console.error(`Server error detected (code: ${error.code})`);
+      console.error(error);
+    }
+  });
+
+  server.on('listening', () => {
+    logInfo`${'All done!'} Listening at: ${address}`;
+    logInfo`Press ^C here (control+C) to stop the server and exit.`;
+  });
+
+  server.listen(port, host);
+
+  // Just keep going... forever!!!
+  await new Promise(() => {});
+
+  return true;
+}
+
+function getPageSpecsWithTargets({
+  wikiData,
+}) {
+  return Object.values(pageSpecs)
+    .filter(pageSpec => pageSpec.condition?.({wikiData}) ?? true)
+    .flatMap(pageSpec => [
+      ...pageSpec.targets
+        ? pageSpec.targets({wikiData})
+            .map(target => ({pageSpec, target}))
+        : [],
+      Object.hasOwn(pageSpec, 'writeTargetless') &&
+        {pageSpec, targetless: true},
+    ])
+    .filter(Boolean);
+}
diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js
new file mode 100644
index 0000000..220e51f
--- /dev/null
+++ b/src/write/build-modes/static-build.js
@@ -0,0 +1,527 @@
+import * as path from 'path';
+
+import {bindUtilities} from '../bind-utilities.js';
+import {validateWrites} from '../validate-writes.js';
+
+import {
+  generateDocumentHTML,
+  generateGlobalWikiDataJSON,
+  generateOEmbedJSON,
+  generateRedirectHTML,
+} from '../page-template.js';
+
+import {serializeThings} from '../../data/serialize.js';
+
+import * as pageSpecs from '../../page/index.js';
+
+import link from '../../util/link.js';
+import {empty, queue} from '../../util/sugar.js';
+
+import {
+  logError,
+  logInfo,
+  logWarn,
+  progressCallAll,
+  progressPromiseAll,
+} from '../../util/cli.js';
+
+import {
+  getPagePathname,
+  getPagePathnameAcrossLanguages,
+  getURLsFrom,
+  getURLsFromRoot,
+} from '../../util/urls.js';
+
+const pageFlags = Object.keys(pageSpecs);
+
+export function getCLIOptions() {
+  return {
+    // 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',
+    },
+
+    // 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',
+    },
+
+    // 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',
+    },
+
+    // NOT for neatly ena8ling or disa8ling specific features of the site!
+    // This is only in charge of what general groups of files to write.
+    // 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.
+    ...Object.fromEntries(pageFlags.map((key) => [key, {type: 'flag'}])),
+  };
+}
+
+export async function go({
+  cliOptions,
+  _dataPath,
+  mediaPath,
+  queueSize,
+
+  defaultLanguage,
+  languages,
+  srcRootPath,
+  urls,
+  urlSpec,
+  wikiData,
+
+  cachebust,
+  developersComment,
+  getSizeOfAdditionalFile,
+}) {
+  const outputPath = cliOptions['out-path'] || process.env.HSMUSIC_OUT;
+  const appendIndexHTML = cliOptions['append-index-html'] ?? false;
+  const writeOneLanguage = cliOptions['lang'] ?? null;
+
+  if (!outputPath) {
+    logError`Expected ${'--out-path'} option or ${'HSMUSIC_OUT'} to be set`;
+    return false;
+  }
+
+  if (appendIndexHTML) {
+    logWarn`Appending index.html to link hrefs. (Note: not recommended for production release!)`;
+    link.globalOptions.appendIndexHTML = true;
+  }
+
+  if (writeOneLanguage && !(writeOneLanguage in languages)) {
+    logError`Specified to write only ${writeOneLanguage}, but there is no strings file with this language code!`;
+    return false;
+  } else if (writeOneLanguage) {
+    logInfo`Writing only language ${writeOneLanguage} this run.`;
+  } else {
+    logInfo`Writing all languages.`;
+  }
+
+  const selectedPageFlags = Object.keys(cliOptions)
+    .filter(key => pageFlags.includes(key));
+
+  const writeAll = empty(selectedPageFlags) || selectedPageFlags.includes('all');
+  logInfo`Writing site pages: ${writeAll ? 'all' : selectedPageFlags.join(', ')}`;
+
+  await writeSymlinks({
+    srcRootPath,
+    mediaPath,
+    outputPath,
+    urls,
+  });
+
+  await writeFavicon({
+    mediaPath,
+    outputPath,
+  });
+
+  await writeSharedFilesAndPages({
+    language: defaultLanguage,
+    outputPath,
+    urls,
+    wikiData,
+    wikiDataJSON: generateGlobalWikiDataJSON({
+      serializeThings,
+      wikiData,
+    })
+  });
+
+  const buildSteps = writeAll
+    ? Object.entries(pageSpecs)
+    : Object.entries(pageSpecs)
+        .filter(([flag]) => selectedPageFlags.includes(flag));
+
+  let writes;
+  {
+    let error = false;
+
+    const buildStepsWithTargets = buildSteps
+      .map(([flag, pageSpec]) => {
+        // Condition not met: skip this build step altogether.
+        if (pageSpec.condition && !pageSpec.condition({wikiData})) {
+          return null;
+        }
+
+        // May still call writeTargetless if present.
+        if (!pageSpec.targets) {
+          return {flag, pageSpec, targets: []};
+        }
+
+        if (!pageSpec.write) {
+          logError`${flag + '.targets'} is specified, but ${flag + '.write'} is missing!`;
+          error = true;
+          return null;
+        }
+
+        const targets = pageSpec.targets({wikiData});
+        if (!Array.isArray(targets)) {
+          logError`${flag + '.targets'} was called, but it didn't return an array! (${typeof targets})`;
+          error = true;
+          return null;
+        }
+
+        return {flag, pageSpec, targets};
+      })
+      .filter(Boolean);
+
+    if (error) {
+      return false;
+    }
+
+    writes = progressCallAll('Computing page & data writes.', buildStepsWithTargets.flatMap(({flag, pageSpec, targets}) => {
+      const writesFns = targets.map(target => () => {
+        const writes = pageSpec.write(target, {wikiData})?.slice() || [];
+        const valid = validateWrites(writes, {
+          functionName: flag + '.write',
+          urlSpec,
+        });
+        error ||=! valid;
+        return valid ? writes : [];
+      });
+
+      if (pageSpec.writeTargetless) {
+        writesFns.push(() => {
+          const writes = pageSpec.writeTargetless({wikiData});
+          const valid = validateWrites(writes, {
+            functionName: flag + '.writeTargetless',
+            urlSpec,
+          });
+          error ||=! valid;
+          return valid ? writes : [];
+        });
+      }
+
+      return writesFns;
+    })).flat();
+
+    if (error) {
+      return false;
+    }
+  }
+
+  const pageWrites = writes.filter(({type}) => type === 'page');
+  const dataWrites = writes.filter(({type}) => type === 'data');
+  const redirectWrites = writes.filter(({type}) => type === 'redirect');
+
+  if (writes.length) {
+    logInfo`Total of ${writes.length} writes returned. (${pageWrites.length} page, ${dataWrites.length} data [currently skipped], ${redirectWrites.length} redirect)`;
+  } else {
+    logWarn`No writes returned at all, so exiting early. This is probably a bug!`;
+    return false;
+  }
+
+  /*
+  await progressPromiseAll(`Writing data files shared across languages.`, queue(
+    dataWrites.map(({path, data}) => () => {
+      const bound = {};
+
+      bound.serializeLink = bindOpts(serializeLink, {});
+
+      bound.serializeContribs = bindOpts(serializeContribs, {});
+
+      bound.serializeImagePaths = bindOpts(serializeImagePaths, {
+        thumb
+      });
+
+      bound.serializeCover = bindOpts(serializeCover, {
+        [bindOpts.bindIndex]: 2,
+        serializeImagePaths: bound.serializeImagePaths,
+        urls
+      });
+
+      bound.serializeGroupsForAlbum = bindOpts(serializeGroupsForAlbum, {
+        serializeLink
+      });
+
+      bound.serializeGroupsForTrack = bindOpts(serializeGroupsForTrack, {
+        serializeLink
+      });
+
+      // 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 === defaultLanguage ? '' : 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(page => () => {
+        const pagePath = page.path;
+
+        const localizedPathnames = getPagePathnameAcrossLanguages({
+          defaultLanguage,
+          languages,
+          pagePath,
+          urls,
+        });
+
+        const pathname = getPagePathname({
+          baseDirectory,
+          pagePath,
+          urls,
+        });
+
+        const to = getURLsFrom({
+          baseDirectory,
+          pagePath,
+          urls,
+        });
+
+        const absoluteTo = getURLsFromRoot({
+          baseDirectory,
+          urls,
+        });
+
+        const bound = bindUtilities({
+          absoluteTo,
+          defaultLanguage,
+          getSizeOfAdditionalFile,
+          language,
+          languages,
+          to,
+          urls,
+          wikiData,
+        });
+
+        const pageInfo = page.page(bound);
+
+        const oEmbedJSON = generateOEmbedJSON(pageInfo, {
+          language,
+          wikiData,
+        });
+
+        const oEmbedJSONHref =
+          oEmbedJSON &&
+          wikiData.wikiInfo.canonicalBase &&
+            wikiData.wikiInfo.canonicalBase +
+            urls
+              .from('shared.root')
+              .to('shared.path', pathname + 'oembed.json');
+
+        const pageHTML = generateDocumentHTML(pageInfo, {
+          ...bound,
+          cachebust,
+          developersComment,
+          localizedPathnames,
+          oEmbedJSONHref,
+          pagePath,
+          pathname,
+        });
+
+        return writePage({
+          html: pageHTML,
+          oEmbedJSON,
+          outputDirectory: path.join(outputPath, getPagePathname({
+            baseDirectory,
+            device: true,
+            pagePath,
+            urls,
+          })),
+        });
+      }),
+      ...redirectWrites.map(({fromPath, toPath, title: titleFn}) => () => {
+        const title = titleFn({
+          language,
+        });
+
+        const to = getURLsFrom({
+          baseDirectory,
+          pagePath: fromPath,
+          urls,
+        });
+
+        const target = to('localized.' + toPath[0], ...toPath.slice(1));
+        const html = generateRedirectHTML(title, target, {language});
+
+        return writePage({
+          html,
+          outputDirectory: path.join(outputPath, getPagePathname({
+            baseDirectory,
+            device: true,
+            pagePath: fromPath,
+            urls,
+          })),
+        });
+      }),
+    ], queueSize));
+  };
+
+  await wrapLanguages(perLanguageFn, {
+    languages,
+    writeOneLanguage,
+  });
+
+  // The single most important step.
+  logInfo`Written!`;
+  return true;
+}
+
+// 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;
+
+  const entries = Object.entries(languagesToRun).filter(
+    ([key]) => key !== 'default'
+  );
+
+  for (let i = 0; i < entries.length; i++) {
+    const [_key, language] = entries[i];
+
+    await fn(language, i, entries);
+  }
+}
+
+import {
+  copyFile,
+  mkdir,
+  stat,
+  symlink,
+  writeFile,
+  unlink,
+} from 'fs/promises';
+
+async function writePage({
+  html,
+  oEmbedJSON = '',
+  outputDirectory,
+}) {
+  await mkdir(outputDirectory, {recursive: true});
+
+  await Promise.all([
+    writeFile(path.join(outputDirectory, 'index.html'), html),
+
+    oEmbedJSON &&
+      writeFile(path.join(outputDirectory, 'oembed.json'), oEmbedJSON),
+  ].filter(Boolean));
+}
+
+function writeSymlinks({
+  srcRootPath,
+  mediaPath,
+  outputPath,
+  urls,
+}) {
+  return progressPromiseAll('Writing site symlinks.', [
+    link(path.join(srcRootPath, 'util'), 'shared.utilityRoot'),
+    link(path.join(srcRootPath, 'static'), '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');
+      }
+    }
+  }
+}
+
+async function writeFavicon({
+  mediaPath,
+  outputPath,
+}) {
+  const faviconFile = 'favicon.ico';
+
+  try {
+    await stat(path.join(mediaPath, faviconFile));
+  } catch (error) {
+    return;
+  }
+
+  try {
+    await copyFile(
+      path.join(mediaPath, faviconFile),
+      path.join(outputPath, faviconFile));
+  } catch (error) {
+    logWarn`Failed to copy favicon! ${error.message}`;
+    return;
+  }
+
+  logInfo`Copied favicon to site root.`;
+}
+
+async function writeSharedFilesAndPages({
+  language,
+  outputPath,
+  urls,
+  wikiData,
+  wikiDataJSON,
+}) {
+  const {groupData, wikiInfo} = wikiData;
+
+  return progressPromiseAll(`Writing files & pages shared across languages.`, [
+    groupData?.some((group) => group.directory === 'fandom') &&
+      redirect(
+        'Fandom - Gallery',
+        'albums/fandom',
+        'localized.groupGallery',
+        'fandom'
+      ),
+
+    groupData?.some((group) => group.directory === 'official') &&
+      redirect(
+        'Official - Gallery',
+        'albums/official',
+        'localized.groupGallery',
+        'official'
+      ),
+
+    wikiInfo.enableListings &&
+      redirect(
+        'Album Commentary',
+        'list/all-commentary',
+        'localized.commentaryIndex',
+        ''
+      ),
+
+    wikiDataJSON &&
+      writeFile(
+        path.join(outputPath, 'data.json'),
+        wikiDataJSON),
+  ].filter(Boolean));
+
+  async function redirect(title, from, urlKey, directory) {
+    const target = path.relative(
+      from,
+      urls.from('shared.root').to(urlKey, directory)
+    );
+    const content = generateRedirectHTML(title, target, {language});
+    await mkdir(path.join(outputPath, from), {recursive: true});
+    await writeFile(path.join(outputPath, from, 'index.html'), content);
+  }
+}
diff --git a/src/write/page-template.js b/src/write/page-template.js
new file mode 100644
index 0000000..6ed9fcf
--- /dev/null
+++ b/src/write/page-template.js
@@ -0,0 +1,631 @@
+import chroma from 'chroma-js';
+
+import * as html from '../util/html.js';
+import {getColors} from '../util/colors.js';
+
+import {
+  getFooterLocalizationLinks,
+  getRevealStringFromWarnings,
+  img,
+} from '../misc-templates.js';
+
+export function generateDevelopersCommentHTML({
+  buildTime,
+  commit,
+  wikiData,
+}) {
+  const {name, canonicalBase} = wikiData.wikiInfo;
+  return `<!--\n` + [
+    canonicalBase
+      ? `hsmusic.wiki - ${name}, ${canonicalBase}`
+      : `hsmusic.wiki - ${name}`,
+    'Code copyright 2019-2023 Quasar Nebula et al (MIT License)',
+    ...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/',
+    ],
+    '***',
+    buildTime &&
+      `Site built: ${buildTime.toLocaleString('en-US', {
+        dateStyle: 'long',
+        timeStyle: 'long',
+      })}`,
+    commit &&
+      `Latest code commit: ${commit}`,
+  ]
+    .filter(Boolean)
+    .map(line => `    ` + line)
+    .join('\n') + `\n-->`;
+}
+
+export function generateDocumentHTML(pageInfo, {
+  cachebust,
+  defaultLanguage,
+  developersComment,
+  getThemeString,
+  language,
+  languages,
+  localizedPathnames,
+  oEmbedJSONHref,
+  pagePath,
+  pathname,
+  to,
+  transformMultiline,
+  wikiData,
+}) {
+  const {wikiInfo} = wikiData;
+
+  let {
+    title = '',
+    meta = {},
+    theme = '',
+    stylesheet = '',
+
+    showWikiNameInTitle = true,
+    themeColor = '',
+
+    // missing properties are auto-filled, see below!
+    body = {},
+    banner = {},
+    main = {},
+    sidebarLeft = {},
+    sidebarRight = {},
+    nav = {},
+    secondaryNav = {},
+    footer = {},
+    socialEmbed = {},
+  } = pageInfo;
+
+  body.style ??= '';
+
+  theme = theme || getThemeString(wikiInfo.color);
+
+  banner ||= {};
+  banner.classes ??= [];
+  banner.src ??= '';
+  banner.position ??= '';
+  banner.dimensions ??= [0, 0];
+
+  main.classes ??= [];
+  main.content ??= '';
+
+  sidebarLeft ??= {};
+  sidebarRight ??= {};
+
+  for (const sidebar of [sidebarLeft, sidebarRight]) {
+    sidebar.classes ??= [];
+    sidebar.content ??= '';
+    sidebar.collapse ??= true;
+  }
+
+  nav.classes ??= [];
+  nav.content ??= '';
+  nav.bottomRowContent ??= '';
+  nav.links ??= [];
+  nav.linkContainerClasses ??= [];
+
+  secondaryNav ??= {};
+  secondaryNav.content ??= '';
+  secondaryNav.content ??= '';
+
+  footer.classes ??= [];
+  footer.content ??= wikiInfo.footerContent
+    ? transformMultiline(wikiInfo.footerContent)
+    : '';
+
+  const colors = themeColor
+    ? getColors(themeColor, {chroma})
+    : null;
+
+  const canonical = wikiInfo.canonicalBase
+    ? wikiInfo.canonicalBase + (pathname === '/' ? '' : pathname)
+    : '';
+
+  const localizedCanonical = wikiInfo.canonicalBase
+    ? Object.entries(localizedPathnames).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 =
+    html.tag('footer',
+      {
+        [html.onlyIfContent]: true,
+        id: 'footer',
+        class: footer.classes,
+      },
+      [
+        html.tag('div',
+          {
+            [html.onlyIfContent]: true,
+            class: 'footer-content',
+          },
+          footer.content),
+
+        getFooterLocalizationLinks({
+          defaultLanguage,
+          html,
+          language,
+          languages,
+          pagePath,
+          to,
+        }),
+      ]);
+
+  const generateSidebarHTML = (id, {
+    content,
+    multiple,
+    classes,
+    collapse = true,
+    wide = false,
+
+    // 'last' - last or only sidebar box is sticky
+    // 'column' - entire column, incl. multiple boxes from top, is sticky
+    // 'none' - sidebar not sticky at all, stays at top of page
+    stickyMode = 'last',
+  }) =>
+    content
+      ? html.tag('div',
+          {
+            id,
+            class: [
+              'sidebar-column',
+              'sidebar',
+              wide && 'wide',
+              !collapse && 'no-hide',
+              stickyMode !== 'none' && 'sticky-' + stickyMode,
+              ...classes,
+            ],
+          },
+          content)
+      : multiple
+      ? html.tag('div',
+          {
+            id,
+            class: [
+              'sidebar-column',
+              'sidebar-multiple',
+              wide && 'wide',
+              !collapse && 'no-hide',
+              stickyMode !== 'none' && 'sticky-' + stickyMode,
+            ],
+          },
+          multiple
+            .map((infoOrContent) =>
+              (typeof infoOrContent === 'object' && !Array.isArray(infoOrContent))
+                ? infoOrContent
+                : {content: infoOrContent})
+            .filter(({content}) => content)
+            .map(({
+              content,
+              classes: classes2 = [],
+            }) =>
+              html.tag('div',
+                {
+                  class: ['sidebar', ...classes, ...classes2],
+                },
+                html.fragment(content))))
+      : '';
+
+  const sidebarLeftHTML = generateSidebarHTML('sidebar-left', sidebarLeft);
+  const sidebarRightHTML = generateSidebarHTML('sidebar-right', sidebarRight);
+
+  if (nav.simple) {
+    nav.linkContainerClasses = ['nav-links-hierarchy'];
+    nav.links = [{toHome: true}, {toCurrentPage: true}];
+  }
+
+  const links = (nav.links || []).filter(Boolean);
+
+  const navLinkParts = [];
+  for (let i = 0; i < links.length; i++) {
+    let cur = links[i];
+
+    let {title: linkTitle} = cur;
+
+    if (cur.toHome) {
+      linkTitle ??= wikiInfo.nameShort;
+    } else if (cur.toCurrentPage) {
+      linkTitle ??= title;
+    }
+
+    let partContent;
+
+    if (typeof cur.html === 'string') {
+      partContent = cur.html;
+    } else {
+      const attributes = {
+        class: (cur.toCurrentPage || i === links.length - 1) && 'current',
+        href: cur.toCurrentPage
+          ? ''
+          : cur.toHome
+          ? to('localized.home')
+          : cur.path
+          ? to(...cur.path)
+          : null,
+      };
+      if (attributes.href === null) {
+        throw new Error(
+          `Expected some href specifier for link to ${linkTitle} (${JSON.stringify(
+            cur
+          )})`
+        );
+      }
+      partContent = html.tag('a', attributes, linkTitle);
+    }
+
+    if (!partContent) continue;
+
+    const part = html.tag('span',
+      {class: cur.divider === false && 'no-divider'},
+      partContent);
+
+    navLinkParts.push(part);
+  }
+
+  const navHTML = html.tag('nav',
+    {
+      [html.onlyIfContent]: true,
+      id: 'header',
+      class: [
+        ...nav.classes,
+        links.length && 'nav-has-main-links',
+        nav.content && 'nav-has-content',
+        nav.bottomRowContent && 'nav-has-bottom-row',
+      ],
+    },
+    [
+      links.length &&
+        html.tag(
+          'div',
+          {class: ['nav-main-links', ...nav.linkContainerClasses]},
+          navLinkParts
+        ),
+      nav.bottomRowContent &&
+        html.tag('div', {class: 'nav-bottom-row'}, nav.bottomRowContent),
+      nav.content && html.tag('div', {class: 'nav-content'}, nav.content),
+    ]);
+
+  const secondaryNavHTML = html.tag('nav',
+    {
+      [html.onlyIfContent]: true,
+      id: 'secondary-nav',
+      class: secondaryNav.classes,
+    },
+    secondaryNav.content);
+
+  const bannerSrc = banner.src
+    ? banner.src
+    : banner.path
+    ? to(...banner.path)
+    : null;
+
+  const bannerHTML =
+    banner.position &&
+    bannerSrc &&
+    html.tag('div',
+      {
+        id: 'banner',
+        class: banner.classes,
+      },
+      html.tag('img', {
+        src: bannerSrc,
+        alt: banner.alt,
+        width: banner.dimensions[0] || 1100,
+        height: banner.dimensions[1] || 200,
+      }));
+
+  const layoutHTML = [
+    navHTML,
+    banner.position === 'top' && bannerHTML,
+    secondaryNavHTML,
+    html.tag('div',
+      {
+        class: [
+          'layout-columns',
+          !collapseSidebars && 'vertical-when-thin',
+          (sidebarLeftHTML || sidebarRightHTML) && 'has-one-sidebar',
+          (sidebarLeftHTML && sidebarRightHTML) && 'has-two-sidebars',
+          !(sidebarLeftHTML || sidebarRightHTML) && 'has-zero-sidebars',
+          sidebarLeftHTML && 'has-sidebar-left',
+          sidebarRightHTML && 'has-sidebar-right',
+        ],
+      },
+      [
+        sidebarLeftHTML,
+        mainHTML,
+        sidebarRightHTML,
+      ]),
+    banner.position === 'bottom' && bannerHTML,
+    footerHTML,
+  ].filter(Boolean).join('\n');
+
+  const infoCardHTML = html.tag('div', {id: 'info-card-container'},
+    html.tag('div', {id: 'info-card-decor'},
+      html.tag('div', {id: 'info-card'}, [
+        html.tag('div', {class: ['info-card-art-container', 'no-reveal']},
+          img({
+            html,
+            class: 'info-card-art',
+            src: '',
+            link: true,
+            square: true,
+          })),
+        html.tag('div', {class: ['info-card-art-container', 'reveal']},
+          img({
+            html,
+            class: 'info-card-art',
+            src: '',
+            link: true,
+            square: true,
+            reveal: getRevealStringFromWarnings(
+              html.tag('span', {class: 'info-card-art-warnings'}),
+              {html, language}),
+          })),
+        html.tag('h1', {class: 'info-card-name'},
+          html.tag('a')),
+        html.tag('p', {class: 'info-card-album'},
+          language.$('releaseInfo.from', {
+            album: html.tag('a'),
+          })),
+        html.tag('p', {class: 'info-card-artists'},
+          language.$('releaseInfo.by', {
+            artists: html.tag('span'),
+          })),
+        html.tag('p', {class: 'info-card-cover-artists'},
+          language.$('releaseInfo.coverArtBy', {
+            artists: html.tag('span'),
+          })),
+      ])));
+
+  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}),
+
+    ...html.fragment(
+      colors && [
+        html.tag('meta', {
+          name: 'theme-color',
+          content: colors.dark,
+          media: '(prefers-color-scheme: dark)',
+        }),
+
+        html.tag('meta', {
+          name: 'theme-color',
+          content: colors.light,
+          media: '(prefers-color-scheme: light)',
+        }),
+
+        html.tag('meta', {
+          name: 'theme-color',
+          content: colors.primary,
+        }),
+      ]),
+
+    oEmbedJSONHref &&
+      html.tag('link', {
+        type: 'application/json+oembed',
+        href: oEmbedJSONHref,
+      }),
+  ].filter(Boolean).join('\n');
+
+  return `<!DOCTYPE html>\n` + html.tag('html',
+    {
+      lang: language.intlCode,
+      'data-language-code': language.code,
+      'data-url-key': 'localized.' + pagePath[0],
+      ...Object.fromEntries(
+        pagePath.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'),
+    },
+    [
+      developersComment,
+
+      html.tag('head', [
+        html.tag('title',
+          showWikiNameInTitle
+            ? language.formatString('misc.pageTitle.withWikiName', {
+                title,
+                wikiName: wikiInfo.nameShort,
+              })
+            : language.formatString('misc.pageTitle', {title})),
+
+        html.tag('meta', {charset: 'utf-8'}),
+        html.tag('meta', {
+          name: 'viewport',
+          content: 'width=device-width, initial-scale=1',
+        }),
+
+        ...(
+          Object.entries(meta)
+            .filter(([key, value]) => value)
+            .map(([key, value]) => html.tag('meta', {[key]: value}))),
+
+        canonical &&
+          html.tag('link', {
+            rel: 'canonical',
+            href: canonical,
+          }),
+
+        ...(
+          localizedCanonical
+            .map(({lang, href}) => html.tag('link', {
+              rel: 'alternate',
+              hreflang: lang,
+              href,
+            }))),
+
+        socialEmbedHTML,
+
+        html.tag('link', {
+          rel: 'stylesheet',
+          href: to('shared.staticFile', `site2.css?${cachebust}`),
+        }),
+
+        html.tag('style',
+          {[html.onlyIfContent]: true},
+          [
+            theme,
+            stylesheet,
+          ]),
+
+        html.tag('script', {
+          src: to('shared.staticFile', `lazy-loading.js?${cachebust}`),
+        }),
+      ]),
+
+      html.tag('body',
+        {style: body.style || ''},
+        [
+          html.tag('div', {id: 'page-container'}, [
+            mainHTML &&
+              html.tag('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]) =>
+                    html.tag('span', {class: 'skipper'},
+                      html.tag('a', {href}, title)))),
+            layoutHTML,
+          ]),
+
+          infoCardHTML,
+
+          html.tag('script', {
+            type: 'module',
+            src: to('shared.staticFile', `client.js?${cachebust}`),
+          }),
+        ]),
+    ]);
+}
+
+export function generateOEmbedJSON(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));
+}
+
+export function generateRedirectHTML(title, target, {
+  language,
+}) {
+  return `<!DOCTYPE html>\n` + html.tag('html', [
+    html.tag('head', [
+      html.tag('title', language.$('redirectPage.title', {title})),
+      html.tag('meta', {charset: 'utf-8'}),
+
+      html.tag('meta', {
+        'http-equiv': 'refresh',
+        content: `0;url=${target}`,
+      }),
+
+      // TODO: Is this OK for localized pages?
+      html.tag('link', {
+        rel: 'canonical',
+        href: target,
+      }),
+    ]),
+
+    html.tag('body',
+      html.tag('main', [
+        html.tag('h1',
+          language.$('redirectPage.title', {title})),
+        html.tag('p',
+          language.$('redirectPage.infoLine', {
+            target: html.tag('a', {href: target}, target),
+          })),
+      ])),
+  ]);
+}
+
+export function generateGlobalWikiDataJSON({
+  serializeThings,
+  wikiData,
+}) {
+  return '{\n' +
+    ([
+      `"albumData": ${stringifyThings(wikiData.albumData)},`,
+      wikiData.wikiInfo.enableFlashesAndGames &&
+        `"flashData": ${stringifyThings(wikiData.flashData)},`,
+      `"artistData": ${stringifyThings(wikiData.artistData)}`,
+    ]
+      .filter(Boolean)
+      .map(line => '  ' + line)
+      .join('\n')) +
+    '\n}';
+
+  function stringifyThings(thingData) {
+    return JSON.stringify(serializeThings(thingData));
+  }
+}
diff --git a/src/write/validate-writes.js b/src/write/validate-writes.js
new file mode 100644
index 0000000..5d61d0e
--- /dev/null
+++ b/src/write/validate-writes.js
@@ -0,0 +1,134 @@
+import {logError} from '../util/cli.js';
+
+function validateWritePath(path, urlGroup) {
+  if (!Array.isArray(path)) {
+    return {error: `Expected array, got ${path}`};
+  }
+
+  const {paths} = urlGroup;
+
+  const definedKeys = Object.keys(paths);
+  const specifiedKey = path[0];
+
+  if (!definedKeys.includes(specifiedKey)) {
+    return {error: `Specified key ${specifiedKey} isn't defined`};
+  }
+
+  const expectedArgs = paths[specifiedKey].match(/<>/g)?.length ?? 0;
+  const specifiedArgs = path.length - 1;
+
+  if (specifiedArgs !== expectedArgs) {
+    return {
+      error: `Expected ${expectedArgs} arguments, got ${specifiedArgs}`,
+    };
+  }
+
+  return {success: true};
+}
+
+function validateWriteObject(obj, {
+  urlSpec,
+}) {
+  if (typeof obj !== 'object') {
+    return {error: `Expected object, got ${typeof obj}`};
+  }
+
+  if (typeof obj.type !== 'string') {
+    return {error: `Expected type to be string, got ${obj.type}`};
+  }
+
+  switch (obj.type) {
+    case 'legacy': {
+      if (typeof obj.write !== 'function') {
+        return {error: `Expected write to be string, got ${obj.write}`};
+      }
+
+      break;
+    }
+
+    case 'page': {
+      const path = validateWritePath(obj.path, urlSpec.localized);
+      if (path.error) {
+        return {error: `Path validation failed: ${path.error}`};
+      }
+
+      if (typeof obj.page !== 'function') {
+        return {error: `Expected page to be function, got ${obj.content}`};
+      }
+
+      break;
+    }
+
+    case 'data': {
+      const path = validateWritePath(obj.path, urlSpec.data);
+      if (path.error) {
+        return {error: `Path validation failed: ${path.error}`};
+      }
+
+      if (typeof obj.data !== 'function') {
+        return {error: `Expected data to be function, got ${obj.data}`};
+      }
+
+      break;
+    }
+
+    case 'redirect': {
+      const fromPath = validateWritePath(obj.fromPath, urlSpec.localized);
+      if (fromPath.error) {
+        return {
+          error: `Path (fromPath) validation failed: ${fromPath.error}`,
+        };
+      }
+
+      const toPath = validateWritePath(obj.toPath, urlSpec.localized);
+      if (toPath.error) {
+        return {error: `Path (toPath) validation failed: ${toPath.error}`};
+      }
+
+      if (typeof obj.title !== 'function') {
+        return {error: `Expected title to be function, got ${obj.title}`};
+      }
+
+      break;
+    }
+
+    default: {
+      return {error: `Unknown type: ${obj.type}`};
+    }
+  }
+
+  return {success: true};
+}
+
+export function validateWrites(writes, {
+  functionName,
+  urlSpec,
+}) {
+  // 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`${functionName} didn't return an array!`;
+    return false;
+  }
+
+  if (!(
+    writes.every((obj) => typeof obj === 'object') &&
+    writes.every((obj) => {
+      const result = validateWriteObject(obj, {
+        urlSpec,
+      });
+      if (result.error) {
+        logError`Validating write object failed: ${result.error}`;
+        return false;
+      } else {
+        return true;
+      }
+    })
+  )) {
+    logError`${functionName} returned invalid entries!`;
+    return false;
+  }
+
+  return true;
+}