« 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/content-function.js2
-rw-r--r--src/content/dependencies/generateAlbumInfoPage.js11
-rw-r--r--src/content/dependencies/generateAlbumInfoPageContent.js65
-rw-r--r--src/content/dependencies/generateAlbumSocialEmbed.js2
-rw-r--r--src/content/dependencies/generateColorStyleRules.js3
-rw-r--r--src/misc-templates.js63
-rw-r--r--src/page/album.js216
-rw-r--r--src/page/index.js22
-rw-r--r--src/util/link.js2
-rw-r--r--src/write/bind-utilities.js157
-rw-r--r--src/write/build-modes/live-dev-server.js126
-rw-r--r--src/write/page-template.js1
12 files changed, 251 insertions, 419 deletions
diff --git a/src/content-function.js b/src/content-function.js
index 53cb13cf..bdf9cd29 100644
--- a/src/content-function.js
+++ b/src/content-function.js
@@ -316,7 +316,7 @@ export function fillRelationsLayoutFromSlotResults(relationIdentifier, results,
   return recursive(layout);
 }
 
-function getNeededContentDependencyNames(contentDependencies, name) {
+export function getNeededContentDependencyNames(contentDependencies, name) {
   const set = new Set();
 
   function recursive(name) {
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index 74d80e33..59c314a1 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -21,7 +21,16 @@ export default {
     return relations;
   },
 
-  generate(relations, {
+  data(album) {
+    const data = {};
+
+    data.name = album.name;
+    data.color = album.color;
+
+    return data;
+  },
+
+  generate(data, relations, {
     language,
   }) {
     const page = {};
diff --git a/src/content/dependencies/generateAlbumInfoPageContent.js b/src/content/dependencies/generateAlbumInfoPageContent.js
index e959f274..a17a33f1 100644
--- a/src/content/dependencies/generateAlbumInfoPageContent.js
+++ b/src/content/dependencies/generateAlbumInfoPageContent.js
@@ -56,11 +56,11 @@ export default {
         relation('linkAlbumCommentary', album);
     }
 
-    if (!empty(album.urls)) {
-      relations.externalLinks =
-        album.urls.map(url =>
-          relation('linkExternal', url, {type: 'album'}));
-    }
+    relations.externalLinks =
+      (empty(album.urls)
+        ? null
+        : album.urls.map(url =>
+            relation('linkExternal', url, {type: 'album'})));
 
     relations.trackList = relation('generateAlbumTrackList', album);
 
@@ -237,3 +237,58 @@ export default {
     return content;
   },
 };
+
+/*
+  banner: !empty(album.bannerArtistContribs) && {
+    dimensions: album.bannerDimensions,
+    path: [
+      'media.albumBanner',
+      album.directory,
+      album.bannerFileExtension,
+    ],
+    alt: language.$('misc.alt.albumBanner'),
+    position: 'top',
+  },
+
+  sidebarLeft: generateAlbumSidebar(album, null, {
+    fancifyURL,
+    getLinkThemeString,
+    html,
+    link,
+    language,
+    transformMultiline,
+    wikiData,
+  }),
+
+  nav: {
+    linkContainerClasses: ['nav-links-hierarchy'],
+    links: [
+      {toHome: true},
+      {
+        html: language.$('albumPage.nav.album', {
+          album: link.album(album, {class: 'current'}),
+        }),
+      },
+      {
+        divider: false,
+        html: generateAlbumNavLinks(album, null, {
+          generateNavigationLinks,
+          html,
+          language,
+          link,
+        }),
+      }
+    ],
+    content: generateAlbumChronologyLinks(album, null, {
+      generateChronologyLinks,
+      html,
+    }),
+  },
+
+  secondaryNav: generateAlbumSecondaryNav(album, null, {
+    getLinkThemeString,
+    html,
+    language,
+    link,
+  }),
+*/
diff --git a/src/content/dependencies/generateAlbumSocialEmbed.js b/src/content/dependencies/generateAlbumSocialEmbed.js
index 8786a336..656bd997 100644
--- a/src/content/dependencies/generateAlbumSocialEmbed.js
+++ b/src/content/dependencies/generateAlbumSocialEmbed.js
@@ -74,7 +74,7 @@ export default {
     if (data.hasImage) {
       const imagePath = urls
         .from('shared.root')
-        .to('media.albumColor', data.coverArtDirectory, data.coverArtFileExtension);
+        .to('media.albumCover', data.coverArtDirectory, data.coverArtFileExtension);
       socialEmbed.image = '/' + imagePath;
     }
 
diff --git a/src/content/dependencies/generateColorStyleRules.js b/src/content/dependencies/generateColorStyleRules.js
index 02c3380e..44600935 100644
--- a/src/content/dependencies/generateColorStyleRules.js
+++ b/src/content/dependencies/generateColorStyleRules.js
@@ -10,7 +10,7 @@ export default {
   generate(data, {
     getColors,
   }) {
-    if (!color) return '';
+    if (!data.color) return '';
 
     const {
       primary,
@@ -30,7 +30,6 @@ export default {
       `--bg-color: ${bg}`,
       `--bg-black-color: ${bgBlack}`,
       `--shadow-color: ${shadow}`,
-      ...additionalVariables,
     ];
 
     return [
diff --git a/src/misc-templates.js b/src/misc-templates.js
index 885efa9e..18429709 100644
--- a/src/misc-templates.js
+++ b/src/misc-templates.js
@@ -18,24 +18,6 @@ import {
   sortChronologically,
 } from './util/wiki-data.js';
 
-// "Additional Files" listing
-
-function unbound_generateAdditionalFilesShortcut(additionalFiles, {
-  html,
-  language,
-}) {
-  if (empty(additionalFiles)) return '';
-
-  return language.$('releaseInfo.additionalFiles.shortcut', {
-    anchorLink:
-      html.tag('a',
-        {href: '#additional-files'},
-        language.$('releaseInfo.additionalFiles.shortcut.anchorLink')),
-    titles: language.formatUnitList(
-      additionalFiles.map(g => g.title)),
-  });
-}
-
 // Chronology links
 
 function unbound_generateChronologyLinks(currentThing, {
@@ -109,34 +91,6 @@ function unbound_generateChronologyLinks(currentThing, {
     });
 }
 
-// Content warning tags
-
-function unbound_getRevealStringFromContentWarningMessage(warnings, {
-  html,
-  language,
-}) {
-  return (
-    language.$('misc.contentWarnings', {warnings}) +
-    html.tag('br') +
-    html.tag('span', {class: 'reveal-interaction'},
-      language.$('misc.contentWarnings.reveal'))
-  );
-}
-
-function unbound_getRevealStringFromArtTags(tags, {
-  getRevealStringFromContentWarningMessage,
-  language,
-}) {
-  return (
-    tags?.some(tag => tag.isContentWarning) &&
-      getRevealStringFromContentWarningMessage(
-        language.formatUnitList(
-          tags
-            .filter(tag => tag.isContentWarning)
-            .map(tag => tag.name)))
-  );
-}
-
 // Divided track lists
 
 function unbound_generateTrackListDividedByGroups(tracks, {
@@ -548,36 +502,19 @@ function unbound_getFooterLocalizationLinks({
 // Exports
 
 export {
-  unbound_generateAdditionalFilesList as generateAdditionalFilesList,
-  unbound_generateAdditionalFilesShortcut as generateAdditionalFilesShortcut,
-
   unbound_generateChronologyLinks as generateChronologyLinks,
 
-  unbound_getRevealStringFromContentWarningMessage as getRevealStringFromContentWarningMessage,
-  unbound_getRevealStringFromArtTags as getRevealStringFromArtTags,
-
-  unbound_generateCoverLink as generateCoverLink,
-
-  unbound_getThemeString as getThemeString,
-
   unbound_generateTrackListDividedByGroups as generateTrackListDividedByGroups,
 
-  unbound_fancifyURL as fancifyURL,
-  unbound_fancifyFlashURL as fancifyFlashURL,
-  unbound_iconifyURL as iconifyURL,
-
   unbound_getGridHTML as getGridHTML,
   unbound_getAlbumGridHTML as getAlbumGridHTML,
   unbound_getFlashGridHTML as getFlashGridHTML,
 
   unbound_getCarouselHTML as getCarouselHTML,
 
-  unbound_img as img,
-
   unbound_generateInfoGalleryLinks as generateInfoGalleryLinks,
   unbound_generateNavigationLinks as generateNavigationLinks,
 
-  unbound_generateContentHeading as generateContentHeading,
   unbound_generateStickyHeadingContainer as generateStickyHeadingContainer,
 
   unbound_getFooterLocalizationLinks as getFooterLocalizationLinks,
diff --git a/src/page/album.js b/src/page/album.js
index 4ed4dfcb..6b82f84f 100644
--- a/src/page/album.js
+++ b/src/page/album.js
@@ -1,205 +1,59 @@
 // Album page specification.
 
-import {
-  bindOpts,
-  compareArrays,
-  empty,
-} from '../util/sugar.js';
-
-import {
-  getAlbumCover,
-  getAlbumListTag,
-  getTotalDuration,
-} from '../util/wiki-data.js';
-
-import {
-  u_generateAlbumStylesheet,
-} from '../misc-templates.js';
-
 export const description = `per-album info & track artwork gallery pages`;
 
 export function targets({wikiData}) {
   return wikiData.albumData;
 }
 
-export const dataSteps = {
-  contentDependencies: [
-    'generateAlbumSocialEmbed',
-    'generateAlbumStylesheet',
-  ],
-
-  computePathsForTarget(data, album) {
-    data.hasGalleryPage = album.tracks.some(t => t.hasUniqueCoverArt);
-    data.hasCommentaryPage = !!album.commentary || album.tracks.some(t => t.commentary);
-
-    return [
-      {
-        type: 'page',
-        path: ['album', album.directory],
-      },
-
-      data.hasGalleryPage && {
-        type: 'page',
-        path: ['albumGallery', album.directory],
-      },
+export function pathsForTarget(album) {
+  const hasGalleryPage = album.tracks.some(t => t.hasUniqueCoverArt);
+  const hasCommentaryPage = !!album.commentary || album.tracks.some(t => t.commentary);
 
-      data.hasCommentaryPage && {
-        type: 'page',
-        path: ['albumCommentary', album.directory],
-      },
+  return [
+    {
+      type: 'page',
+      path: ['album', album.directory],
 
-      {
-        type: 'data',
-        path: ['album', album.directory],
+      contentFunction: {
+        name: 'generateAlbumInfoPage',
+        args: [album],
       },
-    ];
-  },
-
-  computeDataCommonAcrossMixedWrites(data, album) {
-    data.albumDuration = getTotalDuration(album.tracks);
-  },
-
-  computeDataCommonAcrossPageWrites(data, album) {
-    data.listTag = getAlbumListTag(album);
-  },
-
-  computeDataForPageWrite: {
-    album(data, album, _pathArgs) {
-      // TODO: We can't use content-unfulfilled functions here.
-      // But how do we express that these need to be fulfilled
-      // from within data steps?
-      data.socialEmbedData = data.dependencies.generateAlbumSocialEmbed.data(album);
-      data.stylesheetData = data.dependencies.generateAlbumStylesheet.data(album);
-
-      data.name = album.name;
-      data.color = album.color;
-      data.directory = album.directory;
-
-      data.hasAdditionalFiles = !empty(album.additionalFiles);
-      data.numAdditionalFiles = album.additionalFiles.flatMap((g) => g.files).length;
-
-      data.displayTrackSections =
-        album.trackSections &&
-          (album.trackSections.length > 1 ||
-            !album.trackSections[0]?.isDefaultTrackSection);
-    },
-  },
-
-  computeContentForPageWrite: {
-    album(data, {
-      absoluteTo,
-      fancifyURL,
-      generateAdditionalFilesShortcut,
-      generateAdditionalFilesList,
-      generateChronologyLinks,
-      generateContributionLinks,
-      generateContentHeading,
-      generateNavigationLinks,
-      getAlbumCover,
-      getAlbumStylesheet,
-      getLinkThemeString,
-      getSizeOfAdditionalFile,
-      getThemeString,
-      html,
-      link,
-      language,
-      transformMultiline,
-      urls,
-    }) {
-      const generateTrackListItem = bindOpts(u_generateTrackListItem, {
-        generateContributionLinks,
-        getLinkThemeString,
-        html,
-        language,
-        link,
-      });
-
-      const generateAlbumSocialEmbedDescription = u_generateAlbumSocialEmbedDescription.fulfill({
-        language,
-      });
-
-      const generateAlbumSocialEmbed = u_generateAlbumSocialEmbed.fulfill({
-        generateSocialEmbedDescription: generateAlbumSocialEmbedDescription,
-      });
-
-      void generateTrackListItem;
     },
-  },
-};
 
-/*
-const infoPage = {
-  page: () => {
-    return {
-      banner: !empty(album.bannerArtistContribs) && {
-        dimensions: album.bannerDimensions,
-        path: [
-          'media.albumBanner',
-          album.directory,
-          album.bannerFileExtension,
-        ],
-        alt: language.$('misc.alt.albumBanner'),
-        position: 'top',
-      },
+    /*
+    hasGalleryPage && {
+      type: 'page',
+      path: ['albumGallery', album.directory],
 
-      cover: {
-        src: getAlbumCover(album),
-        alt: language.$('misc.alt.albumCover'),
-        artTags: album.artTags,
+      contentFunction: {
+        name: 'generateAlbumGalleryPage',
+        args: [album],
       },
+    },
 
-      main: {
-        headingMode: 'sticky',
+    hasCommentaryPage && {
+      type: 'page',
+      path: ['albumCommentary', album.directory],
 
-        content: [
-        ],
+      contentFunction: {
+        name: 'generateAlbumCommentaryPage',
+        args: [album],
       },
+    },
 
-      sidebarLeft: generateAlbumSidebar(album, null, {
-        fancifyURL,
-        getLinkThemeString,
-        html,
-        link,
-        language,
-        transformMultiline,
-        wikiData,
-      }),
+    {
+      type: 'data',
+      path: ['album', album.directory],
 
-      nav: {
-        linkContainerClasses: ['nav-links-hierarchy'],
-        links: [
-          {toHome: true},
-          {
-            html: language.$('albumPage.nav.album', {
-              album: link.album(album, {class: 'current'}),
-            }),
-          },
-          {
-            divider: false,
-            html: generateAlbumNavLinks(album, null, {
-              generateNavigationLinks,
-              html,
-              language,
-              link,
-            }),
-          }
-        ],
-        content: generateAlbumChronologyLinks(album, null, {
-          generateChronologyLinks,
-          html,
-        }),
+      contentFunction: {
+        name: 'generateAlbumDataFile',
+        args: [album],
       },
-
-      secondaryNav: generateAlbumSecondaryNav(album, null, {
-        getLinkThemeString,
-        html,
-        language,
-        link,
-      }),
-    };
-  },
-};
-*/
+    },
+    */
+  ];
+}
 
 /*
 export function write(album, {wikiData}) {
diff --git a/src/page/index.js b/src/page/index.js
index fc0c646c..8cf1d965 100644
--- a/src/page/index.js
+++ b/src/page/index.js
@@ -110,14 +110,14 @@
 // pertain only to site page generation.
 
 export * as album from './album.js';
-export * as albumCommentary from './album-commentary.js';
-export * as artist from './artist.js';
-export * as artistAlias from './artist-alias.js';
-export * as flash from './flash.js';
-export * as group from './group.js';
-export * as homepage from './homepage.js';
-export * as listing from './listing.js';
-export * as news from './news.js';
-export * as static from './static.js';
-export * as tag from './tag.js';
-export * as track from './track.js';
+// export * as albumCommentary from './album-commentary.js';
+// export * as artist from './artist.js';
+// export * as artistAlias from './artist-alias.js';
+// export * as flash from './flash.js';
+// export * as group from './group.js';
+// export * as homepage from './homepage.js';
+// export * as listing from './listing.js';
+// export * as news from './news.js';
+// export * as static from './static.js';
+// export * as tag from './tag.js';
+// export * as track from './track.js';
diff --git a/src/util/link.js b/src/util/link.js
index 00abc69e..a9f79c8b 100644
--- a/src/util/link.js
+++ b/src/util/link.js
@@ -109,7 +109,7 @@ function linkDirectory(key, {
   data = null,
   attr = null,
   ...conf
-}) {
+} = {}) {
   return linkHelper({
     data: thing => ({
       ...(data ? data(thing) : {}),
diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js
index ffaaa7a7..99df1e9c 100644
--- a/src/write/bind-utilities.js
+++ b/src/write/bind-utilities.js
@@ -5,51 +5,18 @@
 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,
@@ -75,36 +42,13 @@ export function bindUtilities({
     html,
     language,
     languages,
+    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,
-  });
-
-  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 +61,7 @@ export function bindUtilities({
     wikiData,
   });
 
+  /*
   bound.transformMultiline = bindOpts(transformMultiline, {
     img: bound.img,
     to,
@@ -127,81 +72,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,18 +95,6 @@ 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,
@@ -271,11 +137,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..206a2403 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,
@@ -25,6 +25,17 @@ import {
   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;
 
@@ -68,16 +79,20 @@ export async function go({
   const host = cliOptions['host'] ?? defaultHost;
   const port = parseInt(cliOptions['port'] ?? defaultPort);
 
+  const contentDependenciesWatcher = await watchContentDependencies();
+  const {contentDependencies: allContentDependencies} = contentDependenciesWatcher;
+  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();
+        ? [pageSpec.writeTargetless({wikiData})]
+        : pageSpec.pathsForTarget(target))).flat();
 
   logInfo`Will be serving a total of ${pages.length} pages.`;
 
@@ -314,6 +329,8 @@ export async function go({
         urls,
       });
 
+      const {name, args} = page.contentFunction;
+
       const bound = bindUtilities({
         absoluteTo,
         defaultLanguage,
@@ -326,8 +343,106 @@ export async function go({
         wikiData,
       });
 
-      const pageInfo = page.page(bound);
+      const allExtraDependencies = {
+        ...bound,
+
+        appendIndexHTML: false,
+        transformMultiline: text => text,
+      };
+
+      // NOTE: ALL THIS STUFF IS PASTED, REVIEW AND INTEGRATE SOON(TM)
+
+      const treeInfo = getRelationsTree(allContentDependencies, name, ...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}) {
+        const contentFunction = fulfilledContentDependencies[name];
+        const filledRelations =
+          fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, relations);
+
+        const generateArgs = [
+          contentFunction.data?.(...args),
+          filledRelations,
+        ].filter(Boolean);
+
+        return contentFunction(...generateArgs);
+      }
+
+      for (const slot of Object.getOwnPropertySymbols(flatRelationSlots)) {
+        slotResults[slot] = runContentFunction(flatRelationSlots[slot]);
+      }
+
+      const topLevelResult = runContentFunction(root);
+
+      // END PASTE
+
+      const pageHTML = topLevelResult.main.content.toString();
 
+      /*
       const pageHTML = generateDocumentHTML(pageInfo, {
         ...bound,
         cachebust,
@@ -337,6 +452,7 @@ export async function go({
         pagePath: servePath,
         pathname,
       });
+      */
 
       console.log(`${requestHead} [200] ${pathname}`);
       response.end(pageHTML);
diff --git a/src/write/page-template.js b/src/write/page-template.js
index 8a3b44e8..72300ba2 100644
--- a/src/write/page-template.js
+++ b/src/write/page-template.js
@@ -5,7 +5,6 @@ import {getColors} from '../util/colors.js';
 
 import {
   getFooterLocalizationLinks,
-  getRevealStringFromContentWarningMessage,
 } from '../misc-templates.js';
 
 export function generateDevelopersCommentHTML({