« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/content/dependencies/generateAlbumCommentaryPage.js1
-rw-r--r--src/content/dependencies/generateAlbumNavAccent.js2
-rw-r--r--src/content/dependencies/generateAlbumSidebar.js8
-rw-r--r--src/content/dependencies/generateFlashActSidebar.js24
-rw-r--r--src/content/dependencies/generateGroupSidebar.js1
-rw-r--r--src/content/dependencies/generateListingSidebar.js1
-rw-r--r--src/content/dependencies/generatePageLayout.js31
-rw-r--r--src/content/dependencies/generateWikiHomeNewsBox.js1
-rw-r--r--src/content/dependencies/generateWikiHomePage.js1
-rw-r--r--src/content/dependencies/listRandomPageLinks.js9
-rw-r--r--src/static/client3.js (renamed from src/static/client2.js)275
-rw-r--r--src/static/site5.css10
-rw-r--r--src/strings-default.yaml5
-rw-r--r--src/write/build-modes/live-dev-server.js11
-rw-r--r--src/write/build-modes/static-build.js18
-rw-r--r--src/write/common-templates.js40
16 files changed, 246 insertions, 192 deletions
diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js
index 3ad1549e..e2415516 100644
--- a/src/content/dependencies/generateAlbumCommentaryPage.js
+++ b/src/content/dependencies/generateAlbumCommentaryPage.js
@@ -201,6 +201,7 @@ export default {
         ],
 
         leftSidebarStickyMode: 'column',
+        leftSidebarClass: 'commentary-track-list-sidebar-box',
         leftSidebarContent: [
           html.tag('h1', relations.sidebarAlbumLink),
           relations.sidebarTrackSections.map(section =>
diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js
index 7eb1dac0..01c88bf7 100644
--- a/src/content/dependencies/generateAlbumNavAccent.js
+++ b/src/content/dependencies/generateAlbumNavAccent.js
@@ -92,7 +92,7 @@ export default {
         html.tag('a',
           {
             href: '#',
-            'data-random': 'track-in-album',
+            'data-random': 'track-in-sidebar',
             id: 'random-button',
           },
           (data.isTrackPage
diff --git a/src/content/dependencies/generateAlbumSidebar.js b/src/content/dependencies/generateAlbumSidebar.js
index a84f4357..5ef4501b 100644
--- a/src/content/dependencies/generateAlbumSidebar.js
+++ b/src/content/dependencies/generateAlbumSidebar.js
@@ -30,6 +30,7 @@ export default {
 
   generate(data, relations, {html}) {
     const trackListBox = {
+      class: 'track-list-sidebar-box',
       content:
         html.tags([
           html.tag('h1', relations.albumLink),
@@ -40,8 +41,10 @@ export default {
     if (data.isAlbumPage) {
       const groupBoxes =
         relations.groupBoxes
-          .map(content => content.slot('mode', 'album'))
-          .map(content => ({content}));
+          .map(content => ({
+            class: 'individual-group-sidebar-box',
+            content: content.slot('mode', 'album'),
+          }));
 
       return {
         leftSidebarMultiple: [
@@ -52,6 +55,7 @@ export default {
     }
 
     const conjoinedGroupBox = {
+      class: 'conjoined-group-sidebar-box',
       content:
         relations.groupBoxes
           .flatMap((content, i, {length}) => [
diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js
index 80072483..29379644 100644
--- a/src/content/dependencies/generateFlashActSidebar.js
+++ b/src/content/dependencies/generateFlashActSidebar.js
@@ -137,7 +137,7 @@ export default {
   }),
 
   generate(data, relations, {getColors, html, language}) {
-    const currentActBox = html.tags([
+    const currentActBoxContent = html.tags([
       html.tag('h1', relations.currentActLink),
 
       html.tag('details',
@@ -160,7 +160,7 @@ export default {
         ]),
     ]);
 
-    const sideMapBox = html.tags([
+    const sideMapBoxContent = html.tags([
       html.tag('h1', relations.flashIndexLink),
 
       stitchArrays({
@@ -188,17 +188,21 @@ export default {
           ])),
     ]);
 
+    const sideMapBox = {
+      class: 'flash-act-map-sidebar-box',
+      content: sideMapBoxContent,
+    };
+
+    const currentActBox = {
+      class: 'flash-current-act-sidebar-box',
+      content: currentActBoxContent,
+    };
+
     return {
       leftSidebarMultiple:
         (data.isFlashActPage
-          ? [
-              {content: sideMapBox},
-              {content: currentActBox},
-            ]
-          : [
-              {content: currentActBox},
-              {content: sideMapBox},
-            ]),
+          ? [sideMapBox, currentActBox]
+          : [currentActBox, sideMapBox]),
     };
   },
 };
diff --git a/src/content/dependencies/generateGroupSidebar.js b/src/content/dependencies/generateGroupSidebar.js
index 6baf37f4..98b288fa 100644
--- a/src/content/dependencies/generateGroupSidebar.js
+++ b/src/content/dependencies/generateGroupSidebar.js
@@ -22,6 +22,7 @@ export default {
 
   generate(relations, slots, {html, language}) {
     return {
+      leftSidebarClass: 'category-map-sidebar-box',
       leftSidebarContent: [
         html.tag('h1',
           language.$('groupSidebar.title')),
diff --git a/src/content/dependencies/generateListingSidebar.js b/src/content/dependencies/generateListingSidebar.js
index fe2a08fa..1cdd236b 100644
--- a/src/content/dependencies/generateListingSidebar.js
+++ b/src/content/dependencies/generateListingSidebar.js
@@ -11,6 +11,7 @@ export default {
 
   generate(relations, {html}) {
     return {
+      leftSidebarClass: 'listing-map-sidebar-box',
       leftSidebarContent: [
         html.tag('h1', relations.listingIndexLink),
         relations.listingIndexList.slot('mode', 'sidebar'),
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index 5fa6e751..95551f3e 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -6,12 +6,19 @@ function sidebarSlots(side) {
     // if specified.
     [side + 'Content']: {type: 'html'},
 
-    // Multiple is an array of {content: (HTML)} objects. Each of these
-    // will generate one sidebar section.
+    // A single class to apply to the whole sidebar. If specifying multiple
+    // sections, this be added to the containing sidebar-column - specify a
+    // class on each section if that's more suitable.
+    [side + 'Class']: {type: 'string'},
+
+    // Multiple is an array of objects, each specifying content (HTML) and
+    // optionally class (a string). Each of these will generate one sidebar
+    // section.
     [side + 'Multiple']: {
       validate: v =>
         v.sparseArrayOf(
           v.validateProperties({
+            class: v.optional(v.isString),
             content: v.isHTML,
           })),
     },
@@ -27,6 +34,7 @@ function sidebarSlots(side) {
     // the whole section's containing box (or the sidebar column as a whole).
     [side + 'StickyMode']: {
       validate: v => v.is('last', 'column', 'static'),
+      default: 'static',
     },
 
     // Collapsing sidebars disappear when the viewport is sufficiently
@@ -354,6 +362,7 @@ export default {
 
     const generateSidebarHTML = (side, id) => {
       const content = slots[side + 'Content'];
+      const topClass = slots[side + 'Class'];
       const multiple = slots[side + 'Multiple'];
       const stickyMode = slots[side + 'StickyMode'];
       const wide = slots[side + 'Wide'];
@@ -363,20 +372,18 @@ export default {
       let sidebarContent = html.blank();
 
       if (!html.isBlank(content)) {
-        sidebarClasses = ['sidebar'];
+        sidebarClasses = ['sidebar', topClass];
         sidebarContent = content;
       } else if (multiple) {
-        sidebarClasses = ['sidebar-multiple'];
+        sidebarClasses = ['sidebar-multiple', topClass];
         sidebarContent =
           multiple
             .filter(Boolean)
-            .map(({content}) =>
-              html.tag('div',
-                {
-                  [html.onlyIfContent]: true,
-                  class: 'sidebar',
-                },
-                content));
+            .map(box =>
+              html.tag('div', {
+                [html.onlyIfContent]: true,
+                class: ['sidebar', box.class],
+              }, box.content));
       }
 
       if (html.isBlank(sidebarContent)) {
@@ -648,7 +655,7 @@ export default {
 
               html.tag('script', {
                 type: 'module',
-                src: to('shared.staticFile', 'client2.js', cachebust),
+                src: to('shared.staticFile', 'client3.js', cachebust),
               }),
             ]),
         ])
diff --git a/src/content/dependencies/generateWikiHomeNewsBox.js b/src/content/dependencies/generateWikiHomeNewsBox.js
index 8acd426c..0d8303f1 100644
--- a/src/content/dependencies/generateWikiHomeNewsBox.js
+++ b/src/content/dependencies/generateWikiHomeNewsBox.js
@@ -42,6 +42,7 @@ export default {
     }
 
     return {
+      class: 'latest-news-sidebar-box',
       content: [
         html.tag('h1', language.$('homepage.news.title')),
 
diff --git a/src/content/dependencies/generateWikiHomePage.js b/src/content/dependencies/generateWikiHomePage.js
index 40a6b1c5..36fcc6f2 100644
--- a/src/content/dependencies/generateWikiHomePage.js
+++ b/src/content/dependencies/generateWikiHomePage.js
@@ -75,6 +75,7 @@ export default {
       leftSidebarMultiple: [
         (relations.customSidebarContent
           ? {
+              class: 'custom-content-sidebar-box',
               content:
                 relations.customSidebarContent
                   .slot('mode', 'multiline'),
diff --git a/src/content/dependencies/listRandomPageLinks.js b/src/content/dependencies/listRandomPageLinks.js
index 0b904019..18585696 100644
--- a/src/content/dependencies/listRandomPageLinks.js
+++ b/src/content/dependencies/listRandomPageLinks.js
@@ -114,13 +114,14 @@ export default {
               language.$('listingPage.other.randomPages.chooseLinkLine.browserSupportPart'),
           })),
 
-        html.tag('p',
-          {class: 'js-hide-once-data'},
+        html.tag('p', {id: 'data-loading-line'},
           language.$('listingPage.other.randomPages.dataLoadingLine')),
 
-        html.tag('p',
-          {class: 'js-show-once-data'},
+        html.tag('p', {id: 'data-loaded-line'},
           language.$('listingPage.other.randomPages.dataLoadedLine')),
+
+        html.tag('p', {id: 'data-error-line'},
+          language.$('listingPage.other.randomPages.dataErrorLine')),
       ],
 
       showSkipToSection: true,
diff --git a/src/static/client2.js b/src/static/client3.js
index 0ec052bd..8372a268 100644
--- a/src/static/client2.js
+++ b/src/static/client3.js
@@ -7,16 +7,7 @@
 
 import {getColors} from '../util/colors.js';
 import {empty, stitchArrays} from '../util/sugar.js';
-
-import {
-  filterMultipleArrays,
-  getArtistNumContributions,
-} from '../util/wiki-data.js';
-
-let albumData, artistData;
-let officialAlbumData, fandomAlbumData, beyondAlbumData;
-
-let ready = false;
+import {filterMultipleArrays} from '../util/wiki-data.js';
 
 const clientInfo = window.hsmusicClientInfo = Object.create(null);
 
@@ -76,15 +67,6 @@ function cssProp(el, key) {
   return getComputedStyle(el).getPropertyValue(key).trim();
 }
 
-function getRefDirectory(ref) {
-  return ref.split(':')[1];
-}
-
-function getAlbum(el) {
-  const directory = cssProp(el, '--album-directory');
-  return albumData.find((album) => album.directory === directory);
-}
-
 // TODO: These should pro8a8ly access some shared urlSpec path. We'd need to
 // separ8te the tooling around that into common-shared code too.
 const getLinkHref = (type, directory) => rebase(`${type}/${directory}`);
@@ -108,6 +90,13 @@ const scriptedLinkInfo = clientInfo.scriptedLinkInfo = {
   nextLink: null,
   previousLink: null,
   randomLink: null,
+
+  state: {
+    albumDirectories: null,
+    albumTrackDirectories: null,
+    artistDirectories: null,
+    artistNumContributions: null,
+  },
 };
 
 function getScriptedLinkReferences() {
@@ -129,109 +118,130 @@ function getScriptedLinkReferences() {
 
 function addRandomLinkListeners() {
   for (const a of scriptedLinkInfo.randomLinks ?? []) {
-    a.addEventListener('click', evt => {
-      if (!ready) {
-        evt.preventDefault();
-        return;
-      }
+    a.addEventListener('click', domEvent => {
+      handleRandomLinkClicked(a, domEvent);
+    });
+  }
+}
 
-      const tracks = albumData =>
-        albumData
-          .map(album => album.tracks)
-          .reduce((acc, tracks) => acc.concat(tracks), []);
+function handleRandomLinkClicked(a, domEvent) {
+  const href = determineRandomLinkHref(a);
 
-      setTimeout(() => {
-        a.href = rebase('js-disabled');
-      });
+  if (!href) {
+    domEvent.preventDefault();
+    return;
+  }
 
-      switch (a.dataset.random) {
-        case 'album':
-          a.href = openAlbum(pick(albumData).directory);
-          break;
+  setTimeout(() => {
+    a.href = '#'
+  });
 
-        case 'track':
-          a.href = openTrack(getRefDirectory(pick(tracks(albumData))));
-          break;
+  a.href = href;
+}
 
-        case 'album-in-group-dl': {
-          const albumLinks =
-            Array.from(a
-              .closest('dt')
-              .nextElementSibling
-              .querySelectorAll('li a'))
+function determineRandomLinkHref(a) {
+  const {state} = scriptedLinkInfo;
 
-          const albumDirectories =
-            albumLinks.map(a =>
-              getComputedStyle(a).getPropertyValue('--album-directory'));
+  const trackDirectoriesFromAlbumDirectories = albumDirectories =>
+    albumDirectories
+      .map(directory => state.albumDirectories.indexOf(directory))
+      .map(index => state.albumTrackDirectories[index])
+      .reduce((acc, trackDirectories) => acc.concat(trackDirectories, []));
 
-          a.href = openAlbum(pick(albumDirectories));
-          break;
-        }
+  switch (a.dataset.random) {
+    case 'album': {
+      const {albumDirectories} = state;
+      if (!albumDirectories) return null;
+
+      return openAlbum(pick(albumDirectories));
+    }
 
-        case 'track-in-group-dl': {
-          const albumLinks =
-            Array.from(a
-              .closest('dt')
-              .nextElementSibling
-              .querySelectorAll('li a'))
+    case 'track': {
+      const {albumDirectories} = state;
+      if (!albumDirectories) return null;
 
-          const albumDirectories =
-            albumLinks.map(a =>
-              getComputedStyle(a).getPropertyValue('--album-directory'));
+      const trackDirectories =
+        trackDirectoriesFromAlbumDirectories(
+          albumDirectories);
 
-          const filteredAlbumData =
-            albumData.filter(album =>
-              albumDirectories.includes(album.directory));
+      return openTrack(pick(trackDirectories));
+    }
 
-          a.href = openTrack(getRefDirectory(pick(tracks(filteredAlbumData))));
-          break;
-        }
+    case 'album-in-group-dl': {
+      const albumLinks =
+        Array.from(a
+          .closest('dt')
+          .nextElementSibling
+          .querySelectorAll('li a'))
 
-        /* Legacy links, for old versions             *
-         * of generateListRandomPageLinksGroupSection */
+      const listAlbumDirectories =
+        albumLinks
+          .map(a => cssProp(a, '--album-directory'));
 
-        case 'album-in-official':
-          a.href = openAlbum(pick(officialAlbumData).directory);
-          break;
+      return openAlbum(pick(listAlbumDirectories));
+    }
 
-        case 'album-in-fandom':
-          a.href = openAlbum(pick(fandomAlbumData).directory);
-          break;
+    case 'track-in-group-dl': {
+      const {albumDirectories} = state;
+      if (!albumDirectories) return null;
 
-        case 'album-in-beyond':
-          a.href = openAlbum(pick(beyondAlbumData).directory);
-          break;
+      const albumLinks =
+        Array.from(a
+          .closest('dt')
+          .nextElementSibling
+          .querySelectorAll('li a'))
 
-        /* End legacy links */
+      const listAlbumDirectories =
+        albumLinks
+          .map(a => cssProp(a, '--album-directory'));
 
-        case 'track-in-album':
-          a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks)));
-          break;
+      const trackDirectories =
+        trackDirectoriesFromAlbumDirectories(
+          listAlbumDirectories);
 
-        case 'track-in-official':
-          a.href = openTrack(getRefDirectory(pick(tracks(officialAlbumData))));
-          break;
+      return openTrack(pick(trackDirectories));
+    }
 
-        case 'track-in-fandom':
-          a.href = openTrack(getRefDirectory(pick(tracks(fandomAlbumData))));
-          break;
+    case 'track-in-sidebar': {
+      // Note that the container for track links may be <ol> or <ul>, and
+      // they can't be identified by href, since links from one track to
+      // another don't include "track" in the href.
+      const trackLinks =
+        Array.from(document
+          .querySelector('.track-list-sidebar-box')
+          .querySelectorAll('li a'));
 
-        case 'track-in-beyond':
-          a.href = openTrack(getRefDirectory(pick(tracks(beyondAlbumData))));
-          break;
+      return pick(trackLinks).href;
+    }
 
-        case 'artist':
-          a.href = openArtist(pick(artistData).directory);
-          break;
+    case 'track-in-album': {
+      const {albumDirectories, albumTrackDirectories} = state;
+      if (!albumDirectories || !albumTrackDirectories) return null;
 
-        case 'artist-more-than-one-contrib':
-          a.href =
-            openArtist(
-              pick(artistData.filter((artist) => getArtistNumContributions(artist) > 1))
-                .directory);
-          break;
-      }
-    });
+      const albumDirectory = cssProp(a, '--album-directory');
+      const albumIndex = albumDirectories.indexOf(albumDirectory);
+      const trackDirectories = albumTrackDirectories[albumIndex];
+
+      return openTrack(pick(trackDirectories));
+    }
+
+    case 'artist': {
+      const {artistDirectories} = state;
+      if (!artistDirectories) return null;
+
+      return openArtist(pick(artistDirectories));
+    }
+
+    case 'artist-more-than-one-contrib': {
+      const {artistDirectories, artistNumContributions} = state;
+      if (!artistDirectories || !artistNumContributions) return null;
+
+      const filteredArtistDirectories =
+        artistDirectories
+          .filter((_artist, index) => artistNumContributions[index] > 1);
+
+      return openArtist(pick(filteredArtistDirectories));
+    }
   }
 }
 
@@ -255,9 +265,7 @@ function addNavigationKeyPressListeners() {
       } else if (event.charCode === 'P'.charCodeAt(0)) {
         scriptedLinkInfo.previousNavLink?.click();
       } else if (event.charCode === 'R'.charCodeAt(0)) {
-        if (ready) {
-          scriptedLinkInfo.randomNavLink?.click();
-        }
+        scriptedLinkInfo.randomNavLink?.click();
       }
     }
   });
@@ -282,31 +290,44 @@ clientSteps.addPageListeners.push(addNavigationKeyPressListeners);
 clientSteps.addPageListeners.push(addRevealLinkClickListeners);
 clientSteps.mutatePageContent.push(mutateNavigationLinkContent);
 
-const elements1 = document.getElementsByClassName('js-hide-once-data');
-const elements2 = document.getElementsByClassName('js-show-once-data');
-
-for (const element of elements1) element.style.display = 'block';
-
-fetch(rebase('data.json', 'rebaseShared'))
-  .then((data) => data.json())
-  .then((data) => {
-    albumData = data.albumData;
-    artistData = data.artistData;
-
-    const albumsInGroup = directory =>
-      albumData
-        .filter(album =>
-          album.groups.includes(`group:${directory}`));
-
-    officialAlbumData = albumsInGroup('official');
-    fandomAlbumData = albumsInGroup('fandom');
-    beyondAlbumData = albumsInGroup('beyond');
-
-    for (const element of elements1) element.style.display = 'none';
-    for (const element of elements2) element.style.display = 'block';
+if (
+  document.documentElement.dataset.urlKey === 'localized.listing' &&
+  document.documentElement.dataset.urlValue0 === 'random'
+) {
+  const dataLoadingLine = document.getElementById('data-loading-line');
+  const dataLoadedLine = document.getElementById('data-loaded-line');
+  const dataErrorLine = document.getElementById('data-error-line');
+
+  dataLoadingLine.style.display = 'block';
+
+  fetch(rebase('random-link-data.json', 'rebaseShared'))
+    .then(data => data.json())
+    .then(data => {
+      const {state} = scriptedLinkInfo;
+
+      Object.assign(state, {
+        albumDirectories: data.albumDirectories,
+        albumTrackDirectories: data.albumTrackDirectories,
+        artistDirectories: data.artistDirectories,
+        artistNumContributions: data.artistNumContributions,
+      });
 
-    ready = true;
-  });
+      dataLoadingLine.style.display = 'none';
+      dataLoadedLine.style.display = 'block';
+    }, () => {
+      dataLoadingLine.style.display = 'none';
+      dataErrorLine.style.display = 'block';
+    })
+    .then(() => {
+      const {randomLinks} = scriptedLinkInfo;
+      for (const a of randomLinks) {
+        const href = determineRandomLinkHref(a);
+        if (!href) {
+          a.removeAttribute('href');
+        }
+      }
+    });
+}
 
 // Data & info card ---------------------------------------
 
diff --git a/src/static/site5.css b/src/static/site5.css
index 1ffe5044..ba44ec37 100644
--- a/src/static/site5.css
+++ b/src/static/site5.css
@@ -803,6 +803,16 @@ html[data-url-key="localized.albumCommentary"] li.no-commentary {
   opacity: 0.7;
 }
 
+html[data-url-key="localized.listing"][data-url-value0="random"] #data-loading-line,
+html[data-url-key="localized.listing"][data-url-value0="random"] #data-loaded-line,
+html[data-url-key="localized.listing"][data-url-value0="random"] #data-error-line {
+  display: none;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="random"] #content a:not([href]) {
+  opacity: 0.7;
+}
+
 /* Images */
 
 .image-container {
diff --git a/src/strings-default.yaml b/src/strings-default.yaml
index eccfc80c..f83412e9 100644
--- a/src/strings-default.yaml
+++ b/src/strings-default.yaml
@@ -1568,7 +1568,7 @@ listingPage:
           If your browser doesn't support relatively modern JavaScript
           or you've disabled it, these links won't work - sorry.
 
-      # dataLoadingLine, dataLoadedLine:
+      # dataLoadingLine, dataLoadedLine, dataErrorLine:
       #   Since the links on this page depend on access to a fairly
       #   large data file that is downloaded separately and in the
       #   background, these messages indicate the status of that
@@ -1580,6 +1580,9 @@ listingPage:
       dataLoadedLine: >-
         (Data files have finished being downloaded. The links should work!)
 
+      dataErrorLine: >-
+        (Data files failed to download. Sorry, some of these links won't work right now!)
+
       chunk:
 
         title:
diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js
index ab6ceecb..8828a5bd 100644
--- a/src/write/build-modes/live-dev-server.js
+++ b/src/write/build-modes/live-dev-server.js
@@ -16,7 +16,7 @@ import {
 } from '#urls';
 
 import {bindUtilities} from '../bind-utilities.js';
-import {generateGlobalWikiDataJSON, generateRedirectHTML} from '../common-templates.js';
+import {generateRandomLinkDataJSON, generateRedirectHTML} from '../common-templates.js';
 
 const defaultHost = '0.0.0.0';
 const defaultPort = 8002;
@@ -157,19 +157,20 @@ export async function go({
 
     // Specialized routes
 
-    if (pathname === '/data.json') {
+    if (pathname === '/random-link-data.json') {
       try {
-        const json = generateGlobalWikiDataJSON({
+        const json = generateRandomLinkDataJSON({
           serializeThings,
           wikiData,
         });
+
         response.writeHead(200, contentTypeJSON);
         response.end(json);
-        if (loudResponses) console.log(`${requestHead} [200] /data.json`);
+        if (loudResponses) console.log(`${requestHead} [200] ${pathname}`);
       } catch (error) {
         response.writeHead(500, contentTypeJSON);
         response.end(`Internal error serializing wiki JSON`);
-        console.error(`${requestHead} [500] /data.json`);
+        console.error(`${requestHead} [500] ${pathname}`);
         showError(error);
       }
       return;
diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js
index b6dc9643..a8e0eb23 100644
--- a/src/write/build-modes/static-build.js
+++ b/src/write/build-modes/static-build.js
@@ -31,7 +31,7 @@ import {
 } from '#urls';
 
 import {bindUtilities} from '../bind-utilities.js';
-import {generateRedirectHTML, generateGlobalWikiDataJSON} from '../common-templates.js';
+import {generateRedirectHTML, generateRandomLinkDataJSON} from '../common-templates.js';
 
 const pageFlags = Object.keys(pageSpecs);
 
@@ -145,14 +145,8 @@ export async function go({
   });
 
   await writeSharedFilesAndPages({
-    language: defaultLanguage,
     outputPath,
-    urls,
-    wikiData,
-    wikiDataJSON: generateGlobalWikiDataJSON({
-      serializeThings,
-      wikiData,
-    }),
+    randomLinkDataJSON: generateRandomLinkDataJSON({wikiData}),
   });
 
   const buildSteps = writeAll
@@ -477,12 +471,12 @@ async function writeFavicon({
 
 async function writeSharedFilesAndPages({
   outputPath,
-  wikiDataJSON,
+  randomLinkDataJSON,
 }) {
   return progressPromiseAll(`Writing files & pages shared across languages.`, [
-    wikiDataJSON &&
+    randomLinkDataJSON &&
       writeFile(
-        path.join(outputPath, 'data.json'),
-        wikiDataJSON),
+        path.join(outputPath, 'random-link-data.json'),
+        randomLinkDataJSON),
   ].filter(Boolean));
 }
diff --git a/src/write/common-templates.js b/src/write/common-templates.js
index 2dd4c924..d897a73b 100644
--- a/src/write/common-templates.js
+++ b/src/write/common-templates.js
@@ -1,4 +1,5 @@
 import * as html from '#html';
+import {getArtistNumContributions} from '#wiki-data';
 
 export function generateRedirectHTML(title, target, {language}) {
   return `<!DOCTYPE html>\n` + html.tag('html', [
@@ -30,22 +31,25 @@ export function generateRedirectHTML(title, target, {language}) {
   ]);
 }
 
-export function generateGlobalWikiDataJSON({
-  serializeThings,
-  wikiData,
-}) {
-  const stringifyThings = thingData =>
-    JSON.stringify(serializeThings(thingData));
-
-  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}';
+export function generateRandomLinkDataJSON({wikiData}) {
+  const {albumData, artistData} = wikiData;
+
+  return JSON.stringify({
+    albumDirectories:
+      albumData
+        .map(album => album.directory),
+
+    albumTrackDirectories:
+      albumData
+        .map(album => album.tracks
+          .map(track => track.directory)),
+
+    artistDirectories:
+      artistData
+        .map(artist => artist.directory),
+
+    artistNumContributions:
+      artistData
+        .map(artist => getArtistNumContributions(artist)),
+  });
 }