« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/write
diff options
context:
space:
mode:
Diffstat (limited to 'src/write')
-rw-r--r--src/write/bind-utilities.js168
-rw-r--r--src/write/build-modes/live-dev-server.js175
-rw-r--r--src/write/build-modes/static-build.js209
-rw-r--r--src/write/page-template.js150
-rw-r--r--src/write/validate-writes.js2
5 files changed, 316 insertions, 388 deletions
diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js
index ffaaa7a7..d6053353 100644
--- a/src/write/bind-utilities.js
+++ b/src/write/bind-utilities.js
@@ -5,59 +5,28 @@
 import chroma from 'chroma-js';
 
 import {
-  fancifyFlashURL,
-  fancifyURL,
-  getAlbumGridHTML,
-  getAlbumStylesheet,
-  getArtistString,
-  getCarouselHTML,
-  getFlashGridHTML,
-  getGridHTML,
-  getRevealStringFromArtTags,
-  getRevealStringFromContentWarningMessage,
-  getThemeString,
-  generateAdditionalFilesList,
-  generateAdditionalFilesShortcut,
-  generateChronologyLinks,
-  generateContentHeading,
-  generateCoverLink,
-  generateInfoGalleryLinks,
-  generateTrackListDividedByGroups,
-  generateNavigationLinks,
-  generateStickyHeadingContainer,
-  iconifyURL,
-  img,
-} from '../misc-templates.js';
-
-import {
   replacerSpec,
   transformInline,
-  transformLyrics,
-  transformMultiline,
+  // transformLyrics,
+  // transformMultiline,
 } from '../util/transform-content.js';
 
 import * as html from '../util/html.js';
 
-import {bindOpts, withEntries} from '../util/sugar.js';
+import {bindOpts} 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';
+import {thumb} from '../util/urls.js';
 
 export function bindUtilities({
   absoluteTo,
+  cachebust,
   defaultLanguage,
   getSizeOfAdditionalFile,
   getSizeOfImageFile,
   language,
   languages,
+  pagePath,
   to,
   urls,
   wikiData,
@@ -69,42 +38,22 @@ export function bindUtilities({
 
   Object.assign(bound, {
     absoluteTo,
+    cachebust,
     defaultLanguage,
     getSizeOfAdditionalFile,
     getSizeOfImageFile,
     html,
     language,
     languages,
+    pagePath,
+    thumb,
     to,
     urls,
     wikiData,
-  })
-
-  bound.img = bindOpts(img, {
-    [bindOpts.bindIndex]: 0,
-    getSizeOfImageFile,
-    html,
-    to,
-  });
-
-  bound.getColors = bindOpts(getColors, {
-    chroma,
-  });
-
-  bound.getLinkThemeString = bindOpts(getLinkThemeString, {
-    getColors: bound.getColors,
+    wikiInfo: wikiData.wikiInfo,
   });
 
-  bound.getThemeString = bindOpts(getThemeString, {
-    getColors: bound.getColors,
-  });
-
-  bound.link = withEntries(link, (entries) =>
-    entries
-      .map(([key, fn]) => [key, bindOpts(fn, {
-        getLinkThemeString: bound.getLinkThemeString,
-        to,
-      })]));
+  bound.getColors = bindOpts(getColors, {chroma});
 
   bound.find = bindFind(wikiData, {mode: 'warn'});
 
@@ -117,6 +66,7 @@ export function bindUtilities({
     wikiData,
   });
 
+  /*
   bound.transformMultiline = bindOpts(transformMultiline, {
     img: bound.img,
     to,
@@ -127,81 +77,14 @@ export function bindUtilities({
     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.getRevealStringFromContentWarningMessage = bindOpts(getRevealStringFromContentWarningMessage, {
-    html,
-    language,
-  });
-
-  bound.getRevealStringFromArtTags = bindOpts(getRevealStringFromArtTags, {
-    language,
-
-    getRevealStringFromContentWarningMessage: bound.getRevealStringFromContentWarningMessage,
-  });
-
-  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.generateContentHeading = bindOpts(generateContentHeading, {
-    [bindOpts.bindIndex]: 0,
-    html,
-  });
-
   bound.generateStickyHeadingContainer = bindOpts(generateStickyHeadingContainer, {
     [bindOpts.bindIndex]: 0,
     html,
@@ -217,30 +100,12 @@ export function bindUtilities({
     generateNavigationLinks: bound.generateNavigationLinks,
   });
 
-  bound.generateCoverLink = bindOpts(generateCoverLink, {
-    [bindOpts.bindIndex]: 0,
-    html,
-    img: bound.img,
-    link: bound.link,
-    language,
-    to,
-    wikiData,
-
-    getRevealStringFromArtTags: bound.getRevealStringFromArtTags,
-  });
-
   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,
@@ -271,11 +136,8 @@ export function bindUtilities({
     [bindOpts.bindIndex]: 0,
     img: bound.img,
     html,
-  })
-
-  bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, {
-    to,
   });
+  */
 
   return bound;
 }
diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js
index 6dfa7d71..d7c33d87 100644
--- a/src/write/build-modes/live-dev-server.js
+++ b/src/write/build-modes/live-dev-server.js
@@ -11,7 +11,7 @@ import {serializeThings} from '../../data/serialize.js';
 import * as pageSpecs from '../../page/index.js';
 
 import {logInfo, logWarn, progressCallAll} from '../../util/cli.js';
-
+import {empty} from '../../util/sugar.js';
 import {
   getPagePathname,
   getPagePathnameAcrossLanguages,
@@ -20,11 +20,21 @@ import {
 } from '../../util/urls.js';
 
 import {
-  generateDocumentHTML,
   generateGlobalWikiDataJSON,
   generateRedirectHTML,
 } from '../page-template.js';
 
+import {
+  watchContentDependencies,
+} from '../../content/dependencies/index.js';
+
+import {
+  fillRelationsLayoutFromSlotResults,
+  flattenRelationsTree,
+  getRelationsTree,
+  getNeededContentDependencyNames,
+} from '../../content-function.js';
+
 const defaultHost = '0.0.0.0';
 const defaultPort = 8002;
 
@@ -64,20 +74,39 @@ export async function go({
   developersComment,
   getSizeOfAdditionalFile,
   getSizeOfImageFile,
+  niceShowAggregate,
 }) {
+  const showError = (error) => {
+    if (error instanceof AggregateError && niceShowAggregate) {
+      niceShowAggregate(error);
+    } else {
+      console.error(error);
+    }
+  };
+
   const host = cliOptions['host'] ?? defaultHost;
   const port = parseInt(cliOptions['port'] ?? defaultPort);
 
+  const contentDependenciesWatcher = await watchContentDependencies();
+  const {contentDependencies: allContentDependencies} = contentDependenciesWatcher;
+
+  contentDependenciesWatcher.on('error', () => {});
+  await new Promise(resolve => contentDependenciesWatcher.once('ready', resolve));
+
   let targetSpecPairs = getPageSpecsWithTargets({wikiData});
   const pages = progressCallAll(`Computing page data & paths for ${targetSpecPairs.length} targets.`,
-    targetSpecPairs.map(({
+    targetSpecPairs.flatMap(({
       pageSpec,
       target,
       targetless,
-    }) => () =>
-      targetless
-        ? pageSpec.writeTargetless({wikiData})
-        : pageSpec.write(target, {wikiData}))).flat();
+    }) => () => {
+      if (targetless) {
+        const result = pageSpec.pathsTargetless({wikiData});
+        return Array.isArray(result) ? result : [result];
+      } else {
+        return pageSpec.pathsForTarget(target);
+      }
+    })).flat();
 
   logInfo`Will be serving a total of ${pages.length} pages.`;
 
@@ -143,7 +172,7 @@ export async function go({
         response.writeHead(500, contentTypeJSON);
         response.end({error: `Internal error serializing wiki JSON`});
         console.error(`${requestHead} [500] /data.json`);
-        console.error(error);
+        showError(error);
       }
       return;
     }
@@ -186,7 +215,7 @@ export async function go({
           response.writeHead(500, contentTypePlain);
           response.end(`Internal error accessing ${localFileArea} file for: ${safePath}`);
           console.error(`${requestHead} [500] ${pathname}`);
-          console.error(error);
+          showError(error);
         }
         return;
       }
@@ -239,7 +268,7 @@ export async function go({
         response.writeHead(500, contentTypePlain);
         response.end(`Failed during file-to-response pipeline`);
         console.error(`${requestHead} [500] ${pathname}`);
-        console.error(error);
+        showError(error);
       }
       return;
     }
@@ -305,8 +334,6 @@ export async function go({
         return;
       }
 
-      response.writeHead(200, contentTypeHTML);
-
       const localizedPathnames = getPagePathnameAcrossLanguages({
         defaultLanguage,
         languages,
@@ -314,37 +341,135 @@ export async function go({
         urls,
       });
 
+      const {name, args = []} = page.contentFunction;
+
       const bound = bindUtilities({
         absoluteTo,
+        cachebust,
         defaultLanguage,
         getSizeOfAdditionalFile,
         getSizeOfImageFile,
         language,
         languages,
+        pagePath: servePath,
         to,
         urls,
         wikiData,
       });
 
-      const pageInfo = page.page(bound);
-
-      const pageHTML = generateDocumentHTML(pageInfo, {
+      const allExtraDependencies = {
         ...bound,
-        cachebust,
-        developersComment,
-        localizedPathnames,
-        oEmbedJSONHref: null, // No oEmbed support for live dev server
-        pagePath: servePath,
-        pathname,
-      });
+
+        appendIndexHTML: false,
+      };
+
+      // NOTE: ALL THIS STUFF IS PASTED, REVIEW AND INTEGRATE SOON(TM)
+
+      const treeInfo = getRelationsTree(allContentDependencies, name, wikiData, ...args);
+      const flatTreeInfo = flattenRelationsTree(treeInfo);
+      const {root, relationIdentifier, flatRelationSlots} = flatTreeInfo;
+
+      const neededContentDependencyNames =
+        getNeededContentDependencyNames(allContentDependencies, name);
+
+      // Content functions aren't recursive, so by following the set above
+      // sequentually, we will always provide fulfilled content functions as the
+      // dependencies for later content functions.
+      const fulfilledContentDependencies = {};
+      for (const name of neededContentDependencyNames) {
+        const unfulfilledContentFunction = allContentDependencies[name];
+        if (!unfulfilledContentFunction) continue;
+
+        const {contentDependencies, extraDependencies} = unfulfilledContentFunction;
+
+        if (empty(contentDependencies) && empty(extraDependencies)) {
+          fulfilledContentDependencies[name] = unfulfilledContentFunction;
+          continue;
+        }
+
+        const fulfillments = {};
+
+        for (const dependencyName of contentDependencies ?? []) {
+          if (dependencyName in fulfilledContentDependencies) {
+            fulfillments[dependencyName] =
+              fulfilledContentDependencies[dependencyName];
+          }
+        }
+
+        for (const dependencyName of extraDependencies ?? []) {
+          if (dependencyName in allExtraDependencies) {
+            fulfillments[dependencyName] =
+              allExtraDependencies[dependencyName];
+          }
+        }
+
+        fulfilledContentDependencies[name] =
+          unfulfilledContentFunction.fulfill(fulfillments);
+      }
+
+      // There might still be unfulfilled content functions if dependencies weren't
+      // provided as part of allContentDependencies or allExtraDependencies.
+      // Catch and report these early, together in an aggregate error.
+      const unfulfilledErrors = [];
+      const unfulfilledNames = [];
+      for (const name of neededContentDependencyNames) {
+        const contentFunction = fulfilledContentDependencies[name];
+        if (!contentFunction) continue;
+        if (!contentFunction.fulfilled) {
+          try {
+            contentFunction();
+          } catch (error) {
+            error.message = `(${name}) ${error.message}`;
+            unfulfilledErrors.push(error);
+            unfulfilledNames.push(name);
+          }
+        }
+      }
+
+      if (!empty(unfulfilledErrors)) {
+        throw new AggregateError(unfulfilledErrors, `Content functions unfulfilled (${unfulfilledNames.join(', ')})`);
+      }
+
+      const slotResults = {};
+
+      function runContentFunction({name, args, relations: layout}) {
+        const contentFunction = fulfilledContentDependencies[name];
+
+        if (!contentFunction) {
+          throw new Error(`Content function ${name} unfulfilled or not listed`);
+        }
+
+        const generateArgs = [];
+
+        if (contentFunction.data) {
+          generateArgs.push(contentFunction.data(...args));
+        }
+
+        if (layout) {
+          generateArgs.push(fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, layout));
+        }
+
+        return contentFunction(...generateArgs);
+      }
+
+      for (const slot of Object.getOwnPropertySymbols(flatRelationSlots)) {
+        slotResults[slot] = runContentFunction(flatRelationSlots[slot]);
+      }
+
+      const topLevelResult = runContentFunction(root);
+
+      // END PASTE
+
+      const pageHTML = topLevelResult.toString();
 
       console.log(`${requestHead} [200] ${pathname}`);
+      response.writeHead(200, contentTypeHTML);
       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);
+      showError(error);
     }
   });
 
@@ -360,7 +485,7 @@ export async function go({
       }, 10_000);
     } else {
       console.error(`Server error detected (code: ${error.code})`);
-      console.error(error);
+      showError(error);
     }
   });
 
@@ -387,7 +512,7 @@ function getPageSpecsWithTargets({
         ? pageSpec.targets({wikiData})
             .map(target => ({pageSpec, target}))
         : [],
-      Object.hasOwn(pageSpec, 'writeTargetless') &&
+      Object.hasOwn(pageSpec, 'pathsTargetless') &&
         {pageSpec, targetless: true},
     ])
     .filter(Boolean);
diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js
index 8e02342c..2fb82b84 100644
--- a/src/write/build-modes/static-build.js
+++ b/src/write/build-modes/static-build.js
@@ -1,14 +1,18 @@
 import * as path from 'path';
 
 import {bindUtilities} from '../bind-utilities.js';
-import {validateWrites} from '../validate-writes.js';
+// import {validateWrites} from '../validate-writes.js';
 
 import {
-  generateDocumentHTML,
-  generateGlobalWikiDataJSON,
-  generateOEmbedJSON,
-  generateRedirectHTML,
-} from '../page-template.js';
+  quickLoadContentDependencies,
+} from '../../content/dependencies/index.js';
+
+import {
+  fillRelationsLayoutFromSlotResults,
+  flattenRelationsTree,
+  getRelationsTree,
+  getNeededContentDependencyNames,
+} from '../../content-function.js';
 
 import {serializeThings} from '../../data/serialize.js';
 
@@ -143,10 +147,12 @@ export async function go({
     outputPath,
     urls,
     wikiData,
+    /*
     wikiDataJSON: generateGlobalWikiDataJSON({
       serializeThings,
       wikiData,
     })
+    */
   });
 
   const buildSteps = writeAll
@@ -158,64 +164,47 @@ export async function go({
   {
     let error = false;
 
-    const buildStepsWithTargets = buildSteps
+    // TODO: Port this to aggregate error
+    writes = 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 paths = [];
 
-        const targets = pageSpec.targets({wikiData});
-        if (!Array.isArray(targets)) {
-          logError`${flag + '.targets'} was called, but it didn't return an array! (${typeof targets})`;
-          error = true;
-          return null;
+        if (pageSpec.pathsTargetless) {
+          const result = pageSpec.pathsTargetless({wikiData});
+          if (Array.isArray(result)) {
+            paths.push(...result);
+          } else {
+            paths.push(result);
+          }
         }
 
-        return {flag, pageSpec, targets};
-      })
-      .filter(Boolean);
+        if (pageSpec.targets) {
+          if (!pageSpec.pathsForTarget) {
+            logError`${flag + '.targets'} is specified, but ${flag + '.pathsForTarget'} is missing!`;
+            error = true;
+            return null;
+          }
 
-    if (error) {
-      return false;
-    }
+          const targets = pageSpec.targets({wikiData});
 
-    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 (!Array.isArray(targets)) {
+            logError`${flag + '.targets'} was called, but it didn't return an array! (${targets})`;
+            error = true;
+            return null;
+          }
 
-      if (pageSpec.writeTargetless) {
-        writesFns.push(() => {
-          const writes = pageSpec.writeTargetless({wikiData});
-          const valid = validateWrites(writes, {
-            functionName: flag + '.writeTargetless',
-            urlSpec,
-          });
-          error ||=! valid;
-          return valid ? writes : [];
-        });
-      }
+          paths.push(...targets.flatMap(target => pageSpec.pathsForTarget(target)));
+          // TODO: Validate each pathsForTargets entry
+        }
 
-      return writesFns;
-    })).flat();
+        return paths;
+      })
+      .filter(Boolean)
+      .flat();
 
     if (error) {
       return false;
@@ -267,6 +256,8 @@ export async function go({
   ));
   */
 
+  const allContentDependencies = await quickLoadContentDependencies();
+
   const perLanguageFn = async (language, i, entries) => {
     const baseDirectory =
       language === defaultLanguage ? '' : language.code;
@@ -303,16 +294,19 @@ export async function go({
 
         const bound = bindUtilities({
           absoluteTo,
+          cachebust,
           defaultLanguage,
           getSizeOfAdditionalFile,
           getSizeOfImageFile,
           language,
           languages,
+          pagePath,
           to,
           urls,
           wikiData,
         });
 
+        /*
         const pageInfo = page.page(bound);
 
         const oEmbedJSON = generateOEmbedJSON(pageInfo, {
@@ -327,20 +321,111 @@ export async function go({
             urls
               .from('shared.root')
               .to('shared.path', pathname + 'oembed.json');
+        */
 
-        const pageHTML = generateDocumentHTML(pageInfo, {
+        const allExtraDependencies = {
           ...bound,
-          cachebust,
-          developersComment,
-          localizedPathnames,
-          oEmbedJSONHref,
-          pagePath,
-          pathname,
-        });
+          appendIndexHTML: false,
+        };
+
+        const {name, args = []} = page.contentFunction;
+        const treeInfo = getRelationsTree(allContentDependencies, name, wikiData, ...args);
+        const flatTreeInfo = flattenRelationsTree(treeInfo);
+        const {root, relationIdentifier, flatRelationSlots} = flatTreeInfo;
+
+        const neededContentDependencyNames =
+          getNeededContentDependencyNames(allContentDependencies, name);
+
+        // Content functions aren't recursive, so by following the set above
+        // sequentually, we will always provide fulfilled content functions as the
+        // dependencies for later content functions.
+        const fulfilledContentDependencies = {};
+        for (const name of neededContentDependencyNames) {
+          const unfulfilledContentFunction = allContentDependencies[name];
+          if (!unfulfilledContentFunction) continue;
+
+          const {contentDependencies, extraDependencies} = unfulfilledContentFunction;
+
+          if (empty(contentDependencies) && empty(extraDependencies)) {
+            fulfilledContentDependencies[name] = unfulfilledContentFunction;
+            continue;
+          }
+
+          const fulfillments = {};
+
+          for (const dependencyName of contentDependencies ?? []) {
+            if (dependencyName in fulfilledContentDependencies) {
+              fulfillments[dependencyName] =
+                fulfilledContentDependencies[dependencyName];
+            }
+          }
+
+          for (const dependencyName of extraDependencies ?? []) {
+            if (dependencyName in allExtraDependencies) {
+              fulfillments[dependencyName] =
+                allExtraDependencies[dependencyName];
+            }
+          }
+
+          fulfilledContentDependencies[name] =
+            unfulfilledContentFunction.fulfill(fulfillments);
+        }
+
+        // There might still be unfulfilled content functions if dependencies weren't
+        // provided as part of allContentDependencies or allExtraDependencies.
+        // Catch and report these early, together in an aggregate error.
+        const unfulfilledErrors = [];
+        const unfulfilledNames = [];
+        for (const name of neededContentDependencyNames) {
+          const contentFunction = fulfilledContentDependencies[name];
+          if (!contentFunction) continue;
+          if (!contentFunction.fulfilled) {
+            try {
+              contentFunction();
+            } catch (error) {
+              error.message = `(${name}) ${error.message}`;
+              unfulfilledErrors.push(error);
+              unfulfilledNames.push(name);
+            }
+          }
+        }
+
+        if (!empty(unfulfilledErrors)) {
+          throw new AggregateError(unfulfilledErrors, `Content functions unfulfilled (${unfulfilledNames.join(', ')})`);
+        }
+
+        const slotResults = {};
+
+        function runContentFunction({name, args, relations: layout}) {
+          const contentFunction = fulfilledContentDependencies[name];
+
+          if (!contentFunction) {
+            throw new Error(`Content function ${name} unfulfilled or not listed`);
+          }
+
+          const generateArgs = [];
+
+          if (contentFunction.data) {
+            generateArgs.push(contentFunction.data(...args));
+          }
+
+          if (layout) {
+            generateArgs.push(fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, layout));
+          }
+
+          return contentFunction(...generateArgs);
+        }
+
+        for (const slot of Object.getOwnPropertySymbols(flatRelationSlots)) {
+          slotResults[slot] = runContentFunction(flatRelationSlots[slot]);
+        }
+
+        const topLevelResult = runContentFunction(root);
+        const pageHTML = topLevelResult.toString();
 
         return writePage({
           html: pageHTML,
-          oEmbedJSON,
+          // oEmbedJSON,
           outputDirectory: path.join(outputPath, getPagePathname({
             baseDirectory,
             device: true,
@@ -497,6 +582,7 @@ async function writeSharedFilesAndPages({
   const {groupData, wikiInfo} = wikiData;
 
   return progressPromiseAll(`Writing files & pages shared across languages.`, [
+    /*
     groupData?.some((group) => group.directory === 'fandom') &&
       redirect(
         'Fandom - Gallery',
@@ -520,6 +606,7 @@ async function writeSharedFilesAndPages({
         'localized.commentaryIndex',
         ''
       ),
+    */
 
     wikiDataJSON &&
       writeFile(
diff --git a/src/write/page-template.js b/src/write/page-template.js
index fcd8759c..d3d7b098 100644
--- a/src/write/page-template.js
+++ b/src/write/page-template.js
@@ -3,11 +3,6 @@ import chroma from 'chroma-js';
 import * as html from '../util/html.js';
 import {getColors} from '../util/colors.js';
 
-import {
-  getFooterLocalizationLinks,
-  getRevealStringFromContentWarningMessage,
-} from '../misc-templates.js';
-
 export function generateDevelopersCommentHTML({
   buildTime,
   commit,
@@ -153,40 +148,7 @@ export function generateDocumentHTML(pageInfo, {
   const collapseSidebars =
     sidebarLeft.collapse !== false && sidebarRight.collapse !== false;
 
-  const mainHTML =
-    html.tag('main', {
-      id: 'content',
-      class: main.classes,
-    }, [
-      ...html.fragment(
-          !title ?
-            null
-        : main.headingMode === 'sticky' ?
-            generateStickyHeadingContainer({
-              coverSrc: cover.src,
-              coverAlt: cover.alt,
-              coverArtTags: cover.artTags,
-              title,
-            })
-        : main.headingMode === 'static' ?
-            html.tag('h1', title)
-        : null),
-
-      ...html.fragment(
-        cover.src &&
-          generateCoverLink({
-            src: cover.src,
-            alt: cover.alt,
-            tags: cover.artTags,
-          })),
 
-      html.tag('div',
-        {
-          [html.onlyIfContent]: true,
-          class: 'main-content-container',
-        },
-        main.content),
-    ]);
 
   const footerHTML =
     html.tag('footer',
@@ -378,31 +340,6 @@ export function generateDocumentHTML(pageInfo, {
         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 processSkippers = skipperList =>
     skipperList
       .filter(Boolean)
@@ -612,92 +549,7 @@ export function generateDocumentHTML(pageInfo, {
       }),
   ].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', 'site3.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 &&
-            skippersHTML,
-            layoutHTML,
-          ]),
-
-          infoCardHTML,
-          imageOverlayHTML,
-
-          html.tag('script', {
-            type: 'module',
-            src: to('shared.staticFile', 'client.js', cachebust),
-          }),
-        ]),
-    ]);
+  return `<!DOCTYPE html>\n`
 }
 
 export function generateOEmbedJSON(pageInfo, {language, wikiData}) {
diff --git a/src/write/validate-writes.js b/src/write/validate-writes.js
index 5d61d0e7..52c7dfab 100644
--- a/src/write/validate-writes.js
+++ b/src/write/validate-writes.js
@@ -1,3 +1,5 @@
+// TODO: All this is for an outdated spec + should use aggregate errors
+
 import {logError} from '../util/cli.js';
 
 function validateWritePath(path, urlGroup) {