« get me outta code hell

Merge branch 'data-steps' of github.com:hsmusic/hsmusic-wiki into data-steps - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/content/dependencies
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-06-12 15:54:24 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-06-12 15:54:24 -0300
commit630af0a345f3be6c3e4aa3300ce138e48ed5ae91 (patch)
tree91cc3c76cebf93bf1042e89c05bc8f8d8442aff9 /src/content/dependencies
parent0e150bbdf4c384bd2eee6fe3e06ab7b4eb3563da (diff)
parent05df0a1199dca320e0c8b92d210e6ab6e9676dfb (diff)
Merge branch 'data-steps' of github.com:hsmusic/hsmusic-wiki into data-steps
Diffstat (limited to 'src/content/dependencies')
-rw-r--r--src/content/dependencies/generateAlbumInfoPage.js303
-rw-r--r--src/content/dependencies/generateAlbumInfoPageContent.js306
-rw-r--r--src/content/dependencies/generatePageLayout.js2
-rw-r--r--src/content/dependencies/generateReleaseInfoContributionsLine.js49
-rw-r--r--src/content/dependencies/generateStickyHeadingContainer.js10
-rw-r--r--src/content/dependencies/generateTrackCoverArtwork.js2
-rw-r--r--src/content/dependencies/generateTrackInfoPage.js660
-rw-r--r--src/content/dependencies/generateTrackInfoPageContent.js654
-rw-r--r--src/content/dependencies/linkThing.js124
9 files changed, 1028 insertions, 1082 deletions
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index 749dd2af..e317adb1 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -1,28 +1,51 @@
 import getChronologyRelations from '../util/getChronologyRelations.js';
 import {sortAlbumsTracksChronologically} from '../../util/wiki-data.js';
+import {accumulateSum, empty} from '../../util/sugar.js';
 
 export default {
   contentDependencies: [
-    'generateAlbumInfoPageContent',
+    'generateAdditionalFilesShortcut',
+    'generateAlbumAdditionalFilesList',
+    'generateAlbumCoverArtwork',
     'generateAlbumNavAccent',
     'generateAlbumSidebar',
     'generateAlbumSocialEmbed',
     'generateAlbumStyleRules',
+    'generateAlbumTrackList',
     'generateChronologyLinks',
     'generateColorStyleRules',
+    'generateContentHeading',
     'generatePageLayout',
+    'generateReleaseInfoContributionsLine',
     'linkAlbum',
+    'linkAlbumCommentary',
+    'linkAlbumGallery',
     'linkArtist',
+    'linkExternal',
     'linkTrack',
+    'transformContent',
   ],
 
-  extraDependencies: ['language'],
+  extraDependencies: ['html', 'language'],
 
   relations(relation, album) {
-    return {
-      layout: relation('generatePageLayout'),
+    const relations = {};
+    const sections = relations.sections = {};
 
-      coverArtistChronologyContributions: getChronologyRelations(album, {
+    relations.layout =
+      relation('generatePageLayout');
+
+    relations.albumStyleRules =
+      relation('generateAlbumStyleRules', album);
+
+    relations.colorStyleRules =
+      relation('generateColorStyleRules', album.color);
+
+    relations.socialEmbed =
+      relation('generateAlbumSocialEmbed', album);
+
+    relations.coverArtistChronologyContributions =
+      getChronologyRelations(album, {
         contributions: album.coverArtistContribs,
 
         linkArtist: artist => relation('linkArtist', artist),
@@ -37,26 +60,124 @@ export default {
             ...artist.albumsAsCoverArtist,
             ...artist.tracksAsCoverArtist,
           ]),
-      }),
+      });
+
+    relations.albumNavAccent =
+      relation('generateAlbumNavAccent', album, null);
+
+    relations.chronologyLinks =
+      relation('generateChronologyLinks');
+
+    relations.sidebar =
+      relation('generateAlbumSidebar', album, null);
+
+    if (album.hasCoverArt) {
+      relations.cover =
+        relation('generateAlbumCoverArtwork', album);
+    }
+
+    // Section: Release info
+
+    const releaseInfo = sections.releaseInfo = {};
+
+    releaseInfo.artistContributionsLine =
+      relation('generateReleaseInfoContributionsLine', album.artistContribs);
+
+    releaseInfo.coverArtistContributionsLine =
+      relation('generateReleaseInfoContributionsLine', album.coverArtistContribs);
+
+    releaseInfo.wallpaperArtistContributionsLine =
+      relation('generateReleaseInfoContributionsLine', album.wallpaperArtistContribs);
+
+    releaseInfo.bannerArtistContributionsLine =
+      relation('generateReleaseInfoContributionsLine', album.bannerArtistContribs);
+
+    // Section: Listen on
+
+    if (!empty(album.urls)) {
+      const listen = sections.listen = {};
+
+      listen.externalLinks =
+        album.urls.map(url =>
+          relation('linkExternal', url, {type: 'album'}));
+    }
+
+    // Section: Extra links
+
+    const extra = sections.extra = {};
+
+    if (album.tracks.some(t => t.hasUniqueCoverArt)) {
+      extra.galleryLink =
+        relation('linkAlbumGallery', album);
+    }
+
+    if (album.commentary || album.tracks.some(t => t.commentary)) {
+      extra.commentaryLink =
+        relation('linkAlbumCommentary', album);
+    }
+
+    if (!empty(album.additionalFiles)) {
+      extra.additionalFilesShortcut =
+        relation('generateAdditionalFilesShortcut', album.additionalFiles);
+    }
+
+    // Section: Track list
+
+    relations.trackList =
+      relation('generateAlbumTrackList', album);
+
+    // Section: Additional files
+
+    if (!empty(album.additionalFiles)) {
+      const additionalFiles = sections.additionalFiles = {};
+
+      additionalFiles.heading =
+        relation('generateContentHeading');
+
+      additionalFiles.additionalFilesList =
+        relation('generateAlbumAdditionalFilesList', album, album.additionalFiles);
+    }
+
+    // Section: Artist commentary
+
+    if (album.commentary) {
+      const artistCommentary = sections.artistCommentary = {};
+
+      artistCommentary.heading =
+        relation('generateContentHeading');
 
-      albumNavAccent: relation('generateAlbumNavAccent', album, null),
-      chronologyLinks: relation('generateChronologyLinks'),
+      artistCommentary.content =
+        relation('transformContent', album.commentary);
+    }
 
-      content: relation('generateAlbumInfoPageContent', album),
-      sidebar: relation('generateAlbumSidebar', album, null),
-      socialEmbed: relation('generateAlbumSocialEmbed', album),
-      albumStyleRules: relation('generateAlbumStyleRules', album),
-      colorStyleRules: relation('generateColorStyleRules', album.color),
-    };
+    return relations;
   },
 
   data(album) {
-    return {
-      name: album.name,
-    };
+    const data = {};
+
+    data.name = album.name;
+    data.date = album.date;
+
+    data.duration = accumulateSum(album.tracks, track => track.duration);
+    data.durationApproximate = album.tracks.length > 1;
+
+    if (album.coverArtDate && +album.coverArtDate !== +album.date) {
+      data.coverArtDate = album.coverArtDate;
+    }
+
+    if (!empty(album.additionalFiles)) {
+      data.numAdditionalFiles = album.additionalFiles.length;
+    }
+
+    data.dateAddedToWiki = album.dateAddedToWiki;
+
+    return data;
   },
 
-  generate(data, relations, {language}) {
+  generate(data, relations, {html, language}) {
+    const {sections: sec} = relations;
+
     return relations.layout
       .slots({
         title: language.$('albumPage.title', {album: data.name}),
@@ -65,8 +186,130 @@ export default {
         colorStyleRules: [relations.colorStyleRules],
         additionalStyleRules: [relations.albumStyleRules],
 
-        cover: relations.content.cover,
-        mainContent: relations.content.main.content,
+        cover:
+          (relations.cover
+            ? relations.cover.slots({
+                alt: language.$('misc.alt.albumCover'),
+              })
+            : null),
+
+        mainContent: [
+          html.tag('p',
+            {
+              [html.onlyIfContent]: true,
+              [html.joinChildren]: html.tag('br'),
+            },
+            [
+              sec.releaseInfo.artistContributionsLine
+                .slots({stringKey: 'releaseInfo.by'}),
+
+              sec.releaseInfo.coverArtistContributionsLine
+                .slots({stringKey: 'releaseInfo.coverArtBy'}),
+
+              sec.releaseInfo.wallpaperArtistContributionsLine
+                .slots({stringKey: 'releaseInfo.wallpaperArtBy'}),
+
+              sec.releaseInfo.bannerArtistContributionsLine
+                .slots({stringKey: 'releasInfo.bannerArtBy'}),
+
+              data.date &&
+                language.$('releaseInfo.released', {
+                  date: language.formatDate(data.date),
+                }),
+
+              data.coverArtDate &&
+                language.$('releaseInfo.artReleased', {
+                  date: language.formatDate(data.coverArtDate),
+                }),
+
+              data.duration &&
+                language.$('releaseInfo.duration', {
+                  duration:
+                    language.formatDuration(data.duration, {
+                      approximate: data.durationApproximate,
+                    }),
+                }),
+            ]),
+
+          sec.listen &&
+            html.tag('p',
+              language.$('releaseInfo.listenOn', {
+                links: language.formatDisjunctionList(sec.listen.externalLinks),
+              })),
+
+          html.tag('p',
+            {
+              [html.onlyIfContent]: true,
+              [html.joinChildren]: html.tag('br'),
+            },
+            [
+              sec.extra.additionalFilesShortcut,
+
+              sec.extra.galleryLink && sec.extra.commentaryLink &&
+                language.$('releaseInfo.viewGalleryOrCommentary', {
+                  gallery:
+                    sec.extra.galleryLink
+                      .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.gallery')),
+                  commentary:
+                    sec.extra.commentaryLink
+                      .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.commentary')),
+                }),
+
+              sec.extra.galleryLink && !sec.extra.commentaryLink &&
+                language.$('releaseInfo.viewGallery', {
+                  link:
+                    sec.extra.galleryLink
+                      .slot('content', language.$('releaseInfo.viewGallery.link')),
+                }),
+
+              !sec.extra.galleryLink && sec.extra.commentaryLink &&
+                language.$('releaseInfo.viewCommentary', {
+                  link:
+                    sec.extra.commentaryLink
+                      .slot('content', language.$('releaseInfo.viewCommentary.link')),
+                }),
+            ]),
+
+          relations.trackList,
+
+          html.tag('p',
+            {
+              [html.onlyIfContent]: true,
+              [html.joinChildren]: '<br>',
+            },
+            [
+              data.dateAddedToWiki &&
+                language.$('releaseInfo.addedToWiki', {
+                  date: language.formatDate(data.dateAddedToWiki),
+                }),
+            ]),
+
+          sec.additionalFiles && [
+            sec.additionalFiles.heading
+              .slots({
+                id: 'additional-files',
+                title:
+                  language.$('releaseInfo.additionalFiles.heading', {
+                    additionalFiles:
+                      language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}),
+                  }),
+              }),
+
+            sec.additionalFiles.additionalFilesList,
+          ],
+
+          sec.artistCommentary && [
+            sec.artistCommentary.heading
+              .slots({
+                id: 'artist-commentary',
+                title: language.$('releaseInfo.artistCommentary')
+              }),
+
+            html.tag('blockquote',
+              sec.artistCommentary.content
+                .slot('mode', 'multiline')),
+          ],
+        ],
 
         navLinkStyle: 'hierarchical',
         navLinks: [
@@ -97,3 +340,23 @@ export default {
       });
   },
 };
+
+/*
+  banner: !empty(album.bannerArtistContribs) && {
+    dimensions: album.bannerDimensions,
+    path: [
+      'media.albumBanner',
+      album.directory,
+      album.bannerFileExtension,
+    ],
+    alt: language.$('misc.alt.albumBanner'),
+    position: 'top',
+  },
+
+  secondaryNav: generateAlbumSecondaryNav(album, null, {
+    getLinkThemeString,
+    html,
+    language,
+    link,
+  }),
+*/
diff --git a/src/content/dependencies/generateAlbumInfoPageContent.js b/src/content/dependencies/generateAlbumInfoPageContent.js
deleted file mode 100644
index 230d7351..00000000
--- a/src/content/dependencies/generateAlbumInfoPageContent.js
+++ /dev/null
@@ -1,306 +0,0 @@
-import {accumulateSum, empty} from '../../util/sugar.js';
-
-export default {
-  contentDependencies: [
-    'generateAdditionalFilesShortcut',
-    'generateAlbumAdditionalFilesList',
-    'generateAlbumCoverArtwork',
-    'generateAlbumTrackList',
-    'generateContentHeading',
-    'linkAlbumCommentary',
-    'linkAlbumGallery',
-    'linkContribution',
-    'linkExternal',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
-  relations(relation, album) {
-    const relations = {};
-    const sections = relations.sections = {};
-
-    const contributionLinksRelation = contribs =>
-      contribs.map(contrib =>
-        relation('linkContribution', contrib.who, contrib.what));
-
-    // Section: Release info
-
-    const releaseInfo = sections.releaseInfo = {};
-
-    if (!empty(album.artistContribs)) {
-      releaseInfo.artistContributionLinks =
-        contributionLinksRelation(album.artistContribs);
-    }
-
-    if (album.hasCoverArt) {
-      relations.cover =
-        relation('generateAlbumCoverArtwork', album);
-      releaseInfo.coverArtistContributionLinks =
-        contributionLinksRelation(album.coverArtistContribs);
-    } else {
-      relations.cover = null;
-    }
-
-    if (album.hasWallpaperArt) {
-      releaseInfo.wallpaperArtistContributionLinks =
-        contributionLinksRelation(album.wallpaperArtistContribs);
-    }
-
-    if (album.hasBannerArt) {
-      releaseInfo.bannerArtistContributionLinks =
-        contributionLinksRelation(album.bannerArtistContribs);
-    }
-
-    // Section: Listen on
-
-    if (!empty(album.urls)) {
-      const listen = sections.listen = {};
-
-      listen.externalLinks =
-        album.urls.map(url =>
-          relation('linkExternal', url, {type: 'album'}));
-    }
-
-    // Section: Extra links
-
-    const extra = sections.extra = {};
-
-    if (album.tracks.some(t => t.hasUniqueCoverArt)) {
-      extra.galleryLink =
-        relation('linkAlbumGallery', album);
-    }
-
-    if (album.commentary || album.tracks.some(t => t.commentary)) {
-      extra.commentaryLink =
-        relation('linkAlbumCommentary', album);
-    }
-
-    if (!empty(album.additionalFiles)) {
-      extra.additionalFilesShortcut =
-        relation('generateAdditionalFilesShortcut', album.additionalFiles);
-    }
-
-    // Section: Track list
-
-    relations.trackList =
-      relation('generateAlbumTrackList', album);
-
-    // Section: Additional files
-
-    if (!empty(album.additionalFiles)) {
-      const additionalFiles = sections.additionalFiles = {};
-
-      additionalFiles.heading =
-        relation('generateContentHeading');
-
-      additionalFiles.additionalFilesList =
-        relation('generateAlbumAdditionalFilesList', album, album.additionalFiles);
-    }
-
-    // Section: Artist commentary
-
-    if (album.commentary) {
-      const artistCommentary = sections.artistCommentary = {};
-
-      artistCommentary.heading =
-        relation('generateContentHeading');
-
-      artistCommentary.content =
-        relation('transformContent', album.commentary);
-    }
-
-    return relations;
-  },
-
-  data(album) {
-    const data = {};
-
-    data.date = album.date;
-    data.duration = accumulateSum(album.tracks, track => track.duration);
-    data.durationApproximate = album.tracks.length > 1;
-
-    data.hasCoverArt = album.hasCoverArt;
-
-    if (album.hasCoverArt) {
-      data.coverArtDirectory = album.directory;
-      data.coverArtFileExtension = album.coverArtFileExtension;
-
-      if (album.coverArtDate && +album.coverArtDate !== +album.date) {
-        data.coverArtDate = album.coverArtDate;
-      }
-    }
-
-    if (!empty(album.additionalFiles)) {
-      data.numAdditionalFiles = album.additionalFiles.length;
-    }
-
-    data.dateAddedToWiki = album.dateAddedToWiki;
-
-    return data;
-  },
-
-  generate(data, relations, {
-    html,
-    language,
-  }) {
-    const content = {};
-
-    const {sections: sec} = relations;
-
-    const formatContributions =
-      (stringKey, contributionLinks, {showContribution = true, showIcons = true} = {}) =>
-        contributionLinks &&
-          language.$(stringKey, {
-            artists:
-              language.formatConjunctionList(
-                contributionLinks.map(link =>
-                  link.slots({showContribution, showIcons}))),
-          });
-
-    if (data.hasCoverArt) {
-      content.cover = relations.cover
-        .slots({
-          alt: language.$('misc.alt.albumCover'),
-        });
-    } else {
-      content.cover = null;
-    }
-
-    content.main = {
-      headingMode: 'sticky',
-      content: html.tags([
-        html.tag('p',
-          {
-            [html.onlyIfContent]: true,
-            [html.joinChildren]: html.tag('br'),
-          },
-          [
-            formatContributions('releaseInfo.by', sec.releaseInfo.artistContributionLinks),
-            formatContributions('releaseInfo.coverArtBy', sec.releaseInfo.coverArtistContributionLinks),
-            formatContributions('releaseInfo.wallpaperArtBy', sec.releaseInfo.wallpaperArtistContributionLinks),
-            formatContributions('releaseInfo.bannerArtBy', sec.releaseInfo.bannerArtistContributionLinks),
-
-            data.date &&
-              language.$('releaseInfo.released', {
-                date: language.formatDate(data.date),
-              }),
-
-            data.coverArtDate &&
-              language.$('releaseInfo.artReleased', {
-                date: language.formatDate(data.coverArtDate),
-              }),
-
-            data.duration &&
-              language.$('releaseInfo.duration', {
-                duration:
-                  language.formatDuration(data.duration, {
-                    approximate: data.durationApproximate,
-                  }),
-              }),
-          ]),
-
-        sec.listen &&
-          html.tag('p',
-            language.$('releaseInfo.listenOn', {
-              links: language.formatDisjunctionList(sec.listen.externalLinks),
-            })),
-
-        html.tag('p',
-          {
-            [html.onlyIfContent]: true,
-            [html.joinChildren]: html.tag('br'),
-          },
-          [
-            sec.extra.additionalFilesShortcut,
-
-            sec.extra.galleryLink && sec.extra.commentaryLink &&
-              language.$('releaseInfo.viewGalleryOrCommentary', {
-                gallery:
-                  sec.extra.galleryLink
-                    .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.gallery')),
-                commentary:
-                  sec.extra.commentaryLink
-                    .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.commentary')),
-              }),
-
-            sec.extra.galleryLink && !sec.extra.commentaryLink &&
-              language.$('releaseInfo.viewGallery', {
-                link:
-                  sec.extra.galleryLink
-                    .slot('content', language.$('releaseInfo.viewGallery.link')),
-              }),
-
-            !sec.extra.galleryLink && sec.extra.commentaryLink &&
-              language.$('releaseInfo.viewCommentary', {
-                link:
-                  sec.extra.commentaryLink
-                    .slot('content', language.$('releaseInfo.viewCommentary.link')),
-              }),
-          ]),
-
-        relations.trackList,
-
-        html.tag('p',
-          {
-            [html.onlyIfContent]: true,
-            [html.joinChildren]: '<br>',
-          },
-          [
-            data.dateAddedToWiki &&
-              language.$('releaseInfo.addedToWiki', {
-                date: language.formatDate(data.dateAddedToWiki),
-              }),
-          ]),
-
-        sec.additionalFiles && [
-          sec.additionalFiles.heading
-            .slots({
-              id: 'additional-files',
-              title:
-                language.$('releaseInfo.additionalFiles.heading', {
-                  additionalFiles:
-                    language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}),
-                }),
-            }),
-
-          sec.additionalFiles.additionalFilesList,
-        ],
-
-        sec.artistCommentary && [
-          sec.artistCommentary.heading
-            .slots({
-              id: 'artist-commentary',
-              title: language.$('releaseInfo.artistCommentary')
-            }),
-
-          html.tag('blockquote',
-            sec.artistCommentary.content
-              .slot('mode', 'multiline')),
-        ],
-      ]),
-    };
-
-    return content;
-  },
-};
-
-/*
-  banner: !empty(album.bannerArtistContribs) && {
-    dimensions: album.bannerDimensions,
-    path: [
-      'media.albumBanner',
-      album.directory,
-      album.bannerFileExtension,
-    ],
-    alt: language.$('misc.alt.albumBanner'),
-    position: 'top',
-  },
-
-  secondaryNav: generateAlbumSecondaryNav(album, null, {
-    getLinkThemeString,
-    html,
-    language,
-    link,
-  }),
-*/
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index 796dc1e5..84acca0b 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -104,7 +104,6 @@ export default {
         showWikiNameInTitle: {type: 'boolean', default: true},
 
         cover: {type: 'html'},
-        coverNeedsReveal: {type: 'boolean'},
 
         socialEmbed: {type: 'html'},
 
@@ -204,7 +203,6 @@ export default {
                 relations.stickyHeadingContainer.slots({
                   title: slots.title,
                   cover: slots.cover,
-                  needsReveal: slots.coverNeedsReveal,
                 });
               break;
             case 'static':
diff --git a/src/content/dependencies/generateReleaseInfoContributionsLine.js b/src/content/dependencies/generateReleaseInfoContributionsLine.js
new file mode 100644
index 00000000..2b342d09
--- /dev/null
+++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js
@@ -0,0 +1,49 @@
+import {empty} from '../../util/sugar.js';
+
+export default {
+  contentDependencies: ['linkContribution'],
+  extraDependencies: ['html', 'language'],
+
+  relations(relation, contributions) {
+    if (empty(contributions)) {
+      return {};
+    }
+
+    return {
+      contributionLinks:
+        contributions
+          .slice(0, 4)
+          .map(({who, what}) =>
+            relation('linkContribution', who, what)),
+    };
+  },
+
+  generate(relations, {html, language}) {
+    return html.template({
+      annotation: `generateReleaseInfoContributionsLine`,
+
+      slots: {
+        stringKey: {type: 'string'},
+
+        showContribution: {type: 'boolean', default: true},
+        showIcons: {type: 'boolean', default: true},
+      },
+
+      content(slots) {
+        if (!relations.contributionLinks) {
+          return html.blank();
+        }
+
+        return language.$(slots.stringKey, {
+          artists:
+            language.formatConjunctionList(
+              relations.contributionLinks.map(link =>
+                link.slots({
+                  showContribution: slots.showContribution,
+                  showIcons: slots.showIcons,
+                }))),
+        });
+      },
+    });
+  },
+};
diff --git a/src/content/dependencies/generateStickyHeadingContainer.js b/src/content/dependencies/generateStickyHeadingContainer.js
index fb6d8307..6602a2a3 100644
--- a/src/content/dependencies/generateStickyHeadingContainer.js
+++ b/src/content/dependencies/generateStickyHeadingContainer.js
@@ -8,7 +8,6 @@ export default {
       slots: {
         title: {type: 'html'},
         cover: {type: 'html'},
-        needsReveal: {type: 'boolean', default: false},
       },
 
       content(slots) {
@@ -27,13 +26,8 @@ export default {
 
               hasCover &&
                 html.tag('div', {class: 'content-sticky-heading-cover-container'},
-                  html.tag('div',
-                    {class: [
-                      'content-sticky-heading-cover',
-                      slots.needsReveal &&
-                        'content-sticky-heading-cover-needs-reveal',
-                    ]},
-                    slots.cover.slot('displayMode', 'thumbnail')))
+                  html.tag('div', {class: 'content-sticky-heading-cover'},
+                    slots.cover.slot('displayMode', 'thumbnail'))),
             ]),
 
             html.tag('div', {class: 'content-sticky-subheading-row'},
diff --git a/src/content/dependencies/generateTrackCoverArtwork.js b/src/content/dependencies/generateTrackCoverArtwork.js
index f6084f36..757ad2d6 100644
--- a/src/content/dependencies/generateTrackCoverArtwork.js
+++ b/src/content/dependencies/generateTrackCoverArtwork.js
@@ -7,7 +7,7 @@ export default {
         relation('generateCoverArtwork',
           (track.hasUniqueCoverArt
             ? track.artTags
-            : album.artTags)),
+            : track.album.artTags)),
     };
   },
 
diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js
index ee68f534..ed28edec 100644
--- a/src/content/dependencies/generateTrackInfoPage.js
+++ b/src/content/dependencies/generateTrackInfoPage.js
@@ -1,27 +1,61 @@
 import getChronologyRelations from '../util/getChronologyRelations.js';
-import {sortAlbumsTracksChronologically} from '../../util/wiki-data.js';
+
+import {
+  sortAlbumsTracksChronologically,
+  sortFlashesChronologically,
+} from '../../util/wiki-data.js';
+
+import {empty} from '../../util/sugar.js';
 
 export default {
   contentDependencies: [
-    'generateTrackInfoPageContent',
+    'generateAdditionalFilesShortcut',
+    'generateAlbumAdditionalFilesList',
     'generateAlbumNavAccent',
     'generateAlbumSidebar',
     'generateAlbumStyleRules',
     'generateChronologyLinks',
     'generateColorStyleRules',
+    'generateContentHeading',
     'generatePageLayout',
+    'generateReleaseInfoContributionsLine',
+    'generateTrackCoverArtwork',
+    'generateTrackList',
+    'generateTrackListDividedByGroups',
     'linkAlbum',
     'linkArtist',
+    'linkContribution',
+    'linkExternal',
+    'linkFlash',
     'linkTrack',
+    'transformContent',
   ],
 
-  extraDependencies: ['language'],
+  extraDependencies: ['html', 'language', 'wikiData'],
 
-  relations(relation, track) {
+  sprawl({wikiInfo}) {
     return {
-      layout: relation('generatePageLayout'),
+      divideTrackListsByGroups: wikiInfo.divideTrackListsByGroups,
+      enableFlashesAndGames: wikiInfo.enableFlashesAndGames,
+    };
+  },
+
+  relations(relation, sprawl, track) {
+    const relations = {};
+    const sections = relations.sections = {};
+    const {album} = track;
+
+    relations.layout =
+      relation('generatePageLayout');
+
+    relations.albumStyleRules =
+      relation('generateAlbumStyleRules', track.album);
+
+    relations.colorStyleRules =
+      relation('generateColorStyleRules', track.color);
 
-      artistChronologyContributions: getChronologyRelations(track, {
+    relations.artistChronologyContributions =
+      getChronologyRelations(track, {
         contributions: [...track.artistContribs, ...track.contributorContribs],
 
         linkArtist: artist => relation('linkArtist', artist),
@@ -32,9 +66,10 @@ export default {
             ...artist.tracksAsArtist,
             ...artist.tracksAsContributor,
           ]),
-      }),
+      });
 
-      coverArtistChronologyContributions: getChronologyRelations(track, {
+    relations.coverArtistChronologyContributions =
+      getChronologyRelations(track, {
         contributions: track.coverArtistContribs,
 
         linkArtist: artist => relation('linkArtist', artist),
@@ -53,28 +88,255 @@ export default {
           }),
       }),
 
-      albumLink: relation('linkAlbum', track.album),
-      trackLink: relation('linkTrack', track),
-      albumNavAccent: relation('generateAlbumNavAccent', track.album, track),
-      chronologyLinks: relation('generateChronologyLinks'),
+    relations.albumLink =
+      relation('linkAlbum', track.album);
 
-      content: relation('generateTrackInfoPageContent', track),
-      sidebar: relation('generateAlbumSidebar', track.album, track),
-      albumStyleRules: relation('generateAlbumStyleRules', track.album),
-      colorStyleRules: relation('generateColorStyleRules', track.color),
-    };
+    relations.trackLink =
+      relation('linkTrack', track);
+
+    relations.albumNavAccent =
+      relation('generateAlbumNavAccent', track.album, track);
+
+    relations.chronologyLinks =
+      relation('generateChronologyLinks');
+
+    relations.sidebar =
+      relation('generateAlbumSidebar', track.album, track);
+
+    const additionalFilesSection = additionalFiles => ({
+      heading: relation('generateContentHeading'),
+      list: relation('generateAlbumAdditionalFilesList', album, additionalFiles),
+    });
+
+    if (track.hasUniqueCoverArt || album.hasCoverArt) {
+      relations.cover =
+        relation('generateTrackCoverArtwork', track);
+    }
+
+    // Section: Release info
+
+    const releaseInfo = sections.releaseInfo = {};
+
+    releaseInfo.artistContributionLinks =
+      relation('generateReleaseInfoContributionsLine', track.artistContribs);
+
+    if (track.hasUniqueCoverArt) {
+      releaseInfo.coverArtistContributionsLine =
+        relation('generateReleaseInfoContributionsLine', track.coverArtistContribs);
+    }
+
+    // Section: Listen on
+
+    const listen = sections.listen = {};
+
+    if (!empty(track.urls)) {
+      listen.externalLinks =
+        track.urls.map(url =>
+          relation('linkExternal', url));
+    }
+
+    // Section: Extra links
+
+    const extra = sections.extra = {};
+
+    if (!empty(track.additionalFiles)) {
+      extra.additionalFilesShortcut =
+        relation('generateAdditionalFilesShortcut', track.additionalFiles);
+    }
+
+    // Section: Other releases
+
+    if (!empty(track.otherReleases)) {
+      const otherReleases = sections.otherReleases = {};
+
+      otherReleases.heading =
+        relation('generateContentHeading');
+
+      otherReleases.items =
+        track.otherReleases.map(track => ({
+          trackLink: relation('linkTrack', track),
+          albumLink: relation('linkAlbum', track.album),
+        }));
+    }
+
+    // Section: Contributors
+
+    if (!empty(track.contributorContribs)) {
+      const contributors = sections.contributors = {};
+
+      contributors.heading =
+        relation('generateContentHeading');
+
+      contributors.contributionLinks =
+        track.contributorContribs.map(({who, what}) =>
+          relation('linkContribution', who, what));
+    }
+
+    // Section: Referenced tracks
+
+    if (!empty(track.referencedTracks)) {
+      const references = sections.references = {};
+
+      references.heading =
+        relation('generateContentHeading');
+
+      references.list =
+        relation('generateTrackList', track.referencedTracks);
+    }
+
+    // Section: Tracks that reference
+
+    if (!empty(track.referencedByTracks)) {
+      const referencedBy = sections.referencedBy = {};
+
+      referencedBy.heading =
+        relation('generateContentHeading');
+
+      referencedBy.list =
+        relation('generateTrackListDividedByGroups',
+          track.referencedByTracks,
+          sprawl.divideTrackListsByGroups);
+    }
+
+    // Section: Sampled tracks
+
+    if (!empty(track.sampledTracks)) {
+      const samples = sections.samples = {};
+
+      samples.heading =
+        relation('generateContentHeading');
+
+      samples.list =
+        relation('generateTrackList', track.sampledTracks);
+    }
+
+    // Section: Tracks that sample
+
+    if (!empty(track.sampledByTracks)) {
+      const sampledBy = sections.sampledBy = {};
+
+      sampledBy.heading =
+        relation('generateContentHeading');
+
+      sampledBy.list =
+        relation('generateTrackListDividedByGroups',
+          track.sampledByTracks,
+          sprawl.divideTrackListsByGroups);
+    }
+
+    // Section: Flashes that feature
+
+    if (sprawl.enableFlashesAndGames) {
+      const sortedFeatures =
+        sortFlashesChronologically(
+          [track, ...track.otherReleases].flatMap(track =>
+            track.featuredInFlashes.map(flash => ({
+              // These aren't going to be exposed directly, they're processed
+              // into the appropriate relations after this sort.
+              flash, track,
+
+              // These properties are only used for the sort.
+              act: flash.act,
+              date: flash.date,
+            }))));
+
+      if (!empty(sortedFeatures)) {
+        const flashesThatFeature = sections.flashesThatFeature = {};
+
+        flashesThatFeature.heading =
+          relation('generateContentHeading');
+
+        flashesThatFeature.entries =
+          sortedFeatures.map(({flash, track: directlyFeaturedTrack}) =>
+            (directlyFeaturedTrack === track
+              ? {
+                  flashLink: relation('linkFlash', flash),
+                }
+              : {
+                  flashLink: relation('linkFlash', flash),
+                  trackLink: relation('linkTrack', directlyFeaturedTrack),
+                }));
+      }
+    }
+
+    // Section: Lyrics
+
+    if (track.lyrics) {
+      const lyrics = sections.lyrics = {};
+
+      lyrics.heading =
+        relation('generateContentHeading');
+
+      lyrics.content =
+        relation('transformContent', track.lyrics);
+    }
+
+    // Sections: Sheet music files, MIDI/proejct files, additional files
+
+    if (!empty(track.sheetMusicFiles)) {
+      sections.sheetMusicFiles = additionalFilesSection(track.sheetMusicFiles);
+    }
+
+    if (!empty(track.midiProjectFiles)) {
+      sections.midiProjectFiles = additionalFilesSection(track.midiProjectFiles);
+    }
+
+    if (!empty(track.additionalFiles)) {
+      sections.additionalFiles = additionalFilesSection(track.additionalFiles);
+    }
+
+    // Section: Artist commentary
+
+    if (track.commentary) {
+      const artistCommentary = sections.artistCommentary = {};
+
+      artistCommentary.heading =
+        relation('generateContentHeading');
+
+      artistCommentary.content =
+        relation('transformContent', track.commentary);
+    }
+
+    return relations;
   },
 
-  data(track) {
-    return {
-      name: track.name,
+  data(sprawl, track) {
+    const data = {};
+    const {album} = track;
 
-      hasTrackNumbers: track.album.hasTrackNumbers,
-      trackNumber: track.album.tracks.indexOf(track) + 1,
-    };
+    data.name = track.name;
+    data.date = track.date;
+    data.duration = track.duration;
+
+    data.hasUniqueCoverArt = track.hasUniqueCoverArt;
+    data.hasAlbumCoverArt = album.hasCoverArt;
+
+    if (track.hasUniqueCoverArt) {
+      data.albumCoverArtDirectory = album.directory;
+      data.trackCoverArtDirectory = track.directory;
+      data.coverArtFileExtension = track.coverArtFileExtension;
+
+      if (track.coverArtDate && +track.coverArtDate !== +track.date) {
+        data.coverArtDate = track.coverArtDate;
+      }
+    } else if (track.album.hasCoverArt) {
+      data.albumCoverArtDirectory = album.directory;
+      data.coverArtFileExtension = album.coverArtFileExtension;
+    }
+
+    data.hasTrackNumbers = album.hasTrackNumbers;
+    data.trackNumber = album.tracks.indexOf(track) + 1;
+
+    if (!empty(track.additionalFiles)) {
+      data.numAdditionalFiles = track.additionalFiles.length;
+    }
+
+    return data;
   },
 
-  generate(data, relations, {language}) {
+  generate(data, relations, {html, language}) {
+    const {sections: sec} = relations;
+
     return relations.layout
       .slots({
         title: language.$('trackPage.title', {track: data.name}),
@@ -83,9 +345,246 @@ export default {
         colorStyleRules: [relations.colorStyleRules],
         additionalStyleRules: [relations.albumStyleRules],
 
-        cover: relations.content.cover,
-        coverNeedsReveal: relations.content.coverNeedsReveal,
-        mainContent: relations.content.main.content,
+        cover:
+          (relations.cover
+            ? relations.cover.slots({
+                alt: language.$('misc.alt.trackCover'),
+              })
+            : null),
+
+        mainContent: [
+          html.tag('p', {
+            [html.onlyIfContent]: true,
+            [html.joinChildren]: html.tag('br'),
+          }, [
+            sec.releaseInfo.artistContributionLinks
+              .slots({stringKey: 'releaseInfo.by'}),
+
+            sec.releaseInfo.coverArtistContributionsLine
+              ?.slots({stringKey: 'releaseInfo.coverArtBy'}),
+
+            data.date &&
+              language.$('releaseInfo.released', {
+                date: language.formatDate(data.date),
+              }),
+
+            data.coverArtDate &&
+              language.$('releaseInfo.artReleased', {
+                date: language.formatDate(data.coverArtDate),
+              }),
+
+            data.duration &&
+              language.$('releaseInfo.duration', {
+                duration: language.formatDuration(data.duration),
+              }),
+          ]),
+
+          html.tag('p',
+            (sec.listen.externalLinks
+              ? language.$('releaseInfo.listenOn', {
+                  links: language.formatDisjunctionList(sec.listen.externalLinks),
+                })
+              : language.$('releaseInfo.listenOn.noLinks', {
+                  name: html.tag('i', data.name),
+                }))),
+
+          html.tag('p',
+            {
+              [html.onlyIfContent]: true,
+              [html.joinChildren]: '<br>',
+            },
+            [
+              sec.sheetMusicFiles &&
+                language.$('releaseInfo.sheetMusicFiles.shortcut', {
+                  link: html.tag('a',
+                    {href: '#sheet-music-files'},
+                    language.$('releaseInfo.sheetMusicFiles.shortcut.link')),
+                }),
+
+              sec.midiProjectFiles &&
+                language.$('releaseInfo.midiProjectFiles.shortcut', {
+                  link: html.tag('a',
+                    {href: '#midi-project-files'},
+                    language.$('releaseInfo.midiProjectFiles.shortcut.link')),
+                }),
+
+              sec.additionalFiles &&
+                sec.extra.additionalFilesShortcut,
+
+              sec.artistCommentary &&
+                language.$('releaseInfo.readCommentary', {
+                  link: html.tag('a',
+                    {href: '#artist-commentary'},
+                    language.$('releaseInfo.readCommentary.link')),
+                }),
+            ]),
+
+          sec.otherReleases && [
+            sec.otherReleases.heading
+              .slots({
+                id: 'also-released-as',
+                title: language.$('releaseInfo.alsoReleasedAs'),
+              }),
+
+            html.tag('ul',
+              sec.otherReleases.items.map(({trackLink, albumLink}) =>
+                html.tag('li',
+                  language.$('releaseInfo.alsoReleasedAs.item', {
+                    track: trackLink,
+                    album: albumLink,
+                  })))),
+          ],
+
+          sec.contributors && [
+            sec.contributors.heading
+              .slots({
+                id: 'contributors',
+                title: language.$('releaseInfo.contributors'),
+              }),
+
+            html.tag('ul',
+              sec.contributors.contributionLinks.map(contributionLink =>
+                html.tag('li',
+                  contributionLink
+                    .slots({
+                      showIcons: true,
+                      showContribution: true,
+                    })))),
+          ],
+
+          sec.references && [
+            sec.references.heading
+              .slots({
+                id: 'references',
+                title:
+                  language.$('releaseInfo.tracksReferenced', {
+                    track: html.tag('i', data.name),
+                  }),
+              }),
+
+            sec.references.list,
+          ],
+
+          sec.referencedBy && [
+            sec.referencedBy.heading
+              .slots({
+                id: 'referenced-by',
+                title:
+                  language.$('releaseInfo.tracksThatReference', {
+                    track: html.tag('i', data.name),
+                  }),
+              }),
+
+            sec.referencedBy.list,
+          ],
+
+          sec.samples && [
+            sec.samples.heading
+              .slots({
+                id: 'samples',
+                title:
+                  language.$('releaseInfo.tracksSampled', {
+                    track: html.tag('i', data.name),
+                  }),
+              }),
+
+            sec.samples.list,
+          ],
+
+          sec.sampledBy && [
+            sec.sampledBy.heading
+              .slots({
+                id: 'referenced-by',
+                title:
+                  language.$('releaseInfo.tracksThatSample', {
+                    track: html.tag('i', data.name),
+                  }),
+              }),
+
+            sec.sampledBy.list,
+          ],
+
+          sec.flashesThatFeature && [
+            sec.flashesThatFeature.heading
+              .slots({
+                id: 'featured-in',
+                title:
+                  language.$('releaseInfo.flashesThatFeature', {
+                    track: html.tag('i', data.name),
+                  }),
+              }),
+
+            html.tag('ul', sec.flashesThatFeature.entries.map(({flashLink, trackLink}) =>
+              (trackLink
+                ? html.tag('li', {class: 'rerelease'},
+                    language.$('releaseInfo.flashesThatFeature.item.asDifferentRelease', {
+                      flash: flashLink,
+                      track: trackLink,
+                    }))
+                : html.tag('li',
+                    language.$('releaseInfo.flashesThatFeature.item', {
+                      flash: flashLink,
+                    }))))),
+          ],
+
+          sec.lyrics && [
+            sec.lyrics.heading
+              .slots({
+                id: 'lyrics',
+                title: language.$('releaseInfo.lyrics'),
+              }),
+
+            html.tag('blockquote',
+              sec.lyrics.content
+                .slot('mode', 'lyrics')),
+          ],
+
+          sec.sheetMusicFiles && [
+            sec.sheetMusicFiles.heading
+              .slots({
+                id: 'sheet-music-files',
+                title: language.$('releaseInfo.sheetMusicFiles.heading'),
+              }),
+
+            sec.sheetMusicFiles.list,
+          ],
+
+          sec.midiProjectFiles && [
+            sec.midiProjectFiles.heading
+              .slots({
+                id: 'midi-project-files',
+                title: language.$('releaseInfo.midiProjectFiles.heading'),
+              }),
+
+            sec.midiProjectFiles.list,
+          ],
+
+          sec.additionalFiles && [
+            sec.additionalFiles.heading
+              .slots({
+                id: 'additional-files',
+                title:
+                  language.$('releaseInfo.additionalFiles.heading', {
+                    additionalFiles:
+                      language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}),
+                  }),
+              }),
+
+            sec.additionalFiles.list,
+          ],
+
+          sec.artistCommentary && [
+            sec.artistCommentary.heading
+              .slots({
+                id: 'artist-commentary',
+                title: language.$('releaseInfo.artistCommentary')
+              }),
+
+            html.tag('blockquote',
+              sec.artistCommentary.content
+                .slot('mode', 'multiline')),
+          ],
+        ],
 
         navLinkStyle: 'hierarchical',
         navLinks: [
@@ -129,4 +628,109 @@ export default {
         ...relations.sidebar,
       });
   },
-}
+};
+
+/*
+  const data = {
+    type: 'data',
+    path: ['track', track.directory],
+    data: ({
+      serializeContribs,
+      serializeCover,
+      serializeGroupsForTrack,
+      serializeLink,
+    }) => ({
+      name: track.name,
+      directory: track.directory,
+      dates: {
+        released: track.date,
+        originallyReleased: track.originalDate,
+        coverArtAdded: track.coverArtDate,
+      },
+      duration: track.duration,
+      color: track.color,
+      cover: serializeCover(track, getTrackCover),
+      artistsContribs: serializeContribs(track.artistContribs),
+      contributorContribs: serializeContribs(track.contributorContribs),
+      coverArtistContribs: serializeContribs(track.coverArtistContribs || []),
+      album: serializeLink(track.album),
+      groups: serializeGroupsForTrack(track),
+      references: track.references.map(serializeLink),
+      referencedBy: track.referencedBy.map(serializeLink),
+      alsoReleasedAs: otherReleases.map((track) => ({
+        track: serializeLink(track),
+        album: serializeLink(track.album),
+      })),
+    }),
+  };
+
+  const getSocialEmbedDescription = ({
+    getArtistString: _getArtistString,
+    language,
+  }) => {
+    const hasArtists = !empty(track.artistContribs);
+    const hasCoverArtists = !empty(track.coverArtistContribs);
+    const getArtistString = (contribs) =>
+      _getArtistString(contribs, {
+        // We don't want to put actual HTML tags in social embeds (sadly
+        // they don't get parsed and displayed, generally speaking), so
+        // override the link argument so that artist "links" just show
+        // their names.
+        link: {artist: (artist) => artist.name},
+      });
+    if (!hasArtists && !hasCoverArtists) return '';
+    return language.formatString(
+      'trackPage.socialEmbed.body' +
+        [hasArtists && '.withArtists', hasCoverArtists && '.withCoverArtists']
+          .filter(Boolean)
+          .join(''),
+      Object.fromEntries(
+        [
+          hasArtists && ['artists', getArtistString(track.artistContribs)],
+          hasCoverArtists && [
+            'coverArtists',
+            getArtistString(track.coverArtistContribs),
+          ],
+        ].filter(Boolean)
+      )
+    );
+  };
+
+  const page = {
+    page: () => {
+      return {
+        title: language.$('trackPage.title', {track: track.name}),
+        stylesheet: getAlbumStylesheet(album, {to}),
+
+        themeColor: track.color,
+        theme:
+          getThemeString(track.color, {
+            additionalVariables: [
+              `--album-directory: ${album.directory}`,
+              `--track-directory: ${track.directory}`,
+            ]
+          }),
+
+        socialEmbed: {
+          heading: language.$('trackPage.socialEmbed.heading', {
+            album: track.album.name,
+          }),
+          headingLink: absoluteTo('localized.album', album.directory),
+          title: language.$('trackPage.socialEmbed.title', {
+            track: track.name,
+          }),
+          description: getSocialEmbedDescription({getArtistString, language}),
+          image: '/' + getTrackCover(track, {to: urls.from('shared.root').to}),
+          color: track.color,
+        },
+
+        secondaryNav: generateAlbumSecondaryNav(album, track, {
+          getLinkThemeString,
+          html,
+          language,
+          link,
+        }),
+      };
+    },
+  };
+*/
diff --git a/src/content/dependencies/generateTrackInfoPageContent.js b/src/content/dependencies/generateTrackInfoPageContent.js
deleted file mode 100644
index 43f8e689..00000000
--- a/src/content/dependencies/generateTrackInfoPageContent.js
+++ /dev/null
@@ -1,654 +0,0 @@
-import {empty} from '../../util/sugar.js';
-import {sortFlashesChronologically} from '../../util/wiki-data.js';
-
-export default {
-  contentDependencies: [
-    'generateAdditionalFilesShortcut',
-    'generateAlbumAdditionalFilesList',
-    'generateContentHeading',
-    'generateTrackCoverArtwork',
-    'generateTrackList',
-    'generateTrackListDividedByGroups',
-    'linkAlbum',
-    'linkContribution',
-    'linkExternal',
-    'linkFlash',
-    'linkTrack',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
-  sprawl({wikiInfo}) {
-    return {
-      divideTrackListsByGroups: wikiInfo.divideTrackListsByGroups,
-      enableFlashesAndGames: wikiInfo.enableFlashesAndGames,
-    };
-  },
-
-  relations(relation, sprawl, track) {
-    const {album} = track;
-
-    const relations = {};
-    const sections = relations.sections = {};
-
-    const contributionLinksRelation = contribs =>
-      contribs
-        .slice(0, 4)
-        .map(contrib =>
-          relation('linkContribution', contrib.who, contrib.what));
-
-    const additionalFilesSection = additionalFiles => ({
-      heading: relation('generateContentHeading'),
-      list: relation('generateAlbumAdditionalFilesList', album, additionalFiles),
-    });
-
-    if (track.hasUniqueCoverArt || album.hasCoverArt) {
-      relations.cover =
-        relation('generateTrackCoverArtwork', track);
-    }
-
-    // Section: Release info
-
-    const releaseInfo = sections.releaseInfo = {};
-
-    releaseInfo.artistContributionLinks =
-      contributionLinksRelation(track.artistContribs);
-
-    if (track.hasUniqueCoverArt) {
-      releaseInfo.coverArtistContributionLinks =
-        contributionLinksRelation(track.coverArtistContribs);
-    }
-
-    // Section: Listen on
-
-    const listen = sections.listen = {};
-
-    if (!empty(track.urls)) {
-      listen.externalLinks =
-        track.urls.map(url =>
-          relation('linkExternal', url));
-    }
-
-    // Section: Extra links
-
-    const extra = sections.extra = {};
-
-    if (!empty(track.additionalFiles)) {
-      extra.additionalFilesShortcut =
-        relation('generateAdditionalFilesShortcut', track.additionalFiles);
-    }
-
-    // Section: Other releases
-
-    if (!empty(track.otherReleases)) {
-      const otherReleases = sections.otherReleases = {};
-
-      otherReleases.heading =
-        relation('generateContentHeading');
-
-      otherReleases.items =
-        track.otherReleases.map(track => ({
-          trackLink: relation('linkTrack', track),
-          albumLink: relation('linkAlbum', track.album),
-        }));
-    }
-
-    // Section: Contributors
-
-    if (!empty(track.contributorContribs)) {
-      const contributors = sections.contributors = {};
-
-      contributors.heading =
-        relation('generateContentHeading');
-
-      contributors.contributionLinks =
-        contributionLinksRelation(track.contributorContribs);
-    }
-
-    // Section: Referenced tracks
-
-    if (!empty(track.referencedTracks)) {
-      const references = sections.references = {};
-
-      references.heading =
-        relation('generateContentHeading');
-
-      references.list =
-        relation('generateTrackList', track.referencedTracks);
-    }
-
-    // Section: Tracks that reference
-
-    if (!empty(track.referencedByTracks)) {
-      const referencedBy = sections.referencedBy = {};
-
-      referencedBy.heading =
-        relation('generateContentHeading');
-
-      referencedBy.list =
-        relation('generateTrackListDividedByGroups',
-          track.referencedByTracks,
-          sprawl.divideTrackListsByGroups);
-    }
-
-    // Section: Sampled tracks
-
-    if (!empty(track.sampledTracks)) {
-      const samples = sections.samples = {};
-
-      samples.heading =
-        relation('generateContentHeading');
-
-      samples.list =
-        relation('generateTrackList', track.sampledTracks);
-    }
-
-    // Section: Tracks that sample
-
-    if (!empty(track.sampledByTracks)) {
-      const sampledBy = sections.sampledBy = {};
-
-      sampledBy.heading =
-        relation('generateContentHeading');
-
-      sampledBy.list =
-        relation('generateTrackListDividedByGroups',
-          track.sampledByTracks,
-          sprawl.divideTrackListsByGroups);
-    }
-
-    // Section: Flashes that feature
-
-    if (sprawl.enableFlashesAndGames) {
-      const sortedFeatures =
-        sortFlashesChronologically(
-          [track, ...track.otherReleases].flatMap(track =>
-            track.featuredInFlashes.map(flash => ({
-              // These aren't going to be exposed directly, they're processed
-              // into the appropriate relations after this sort.
-              flash, track,
-
-              // These properties are only used for the sort.
-              act: flash.act,
-              date: flash.date,
-            }))));
-
-      if (!empty(sortedFeatures)) {
-        const flashesThatFeature = sections.flashesThatFeature = {};
-
-        flashesThatFeature.heading =
-          relation('generateContentHeading');
-
-        flashesThatFeature.entries =
-          sortedFeatures.map(({flash, track: directlyFeaturedTrack}) =>
-            (directlyFeaturedTrack === track
-              ? {
-                  flashLink: relation('linkFlash', flash),
-                }
-              : {
-                  flashLink: relation('linkFlash', flash),
-                  trackLink: relation('linkTrack', directlyFeaturedTrack),
-                }));
-      }
-    }
-
-    // Section: Lyrics
-
-    if (track.lyrics) {
-      const lyrics = sections.lyrics = {};
-
-      lyrics.heading =
-        relation('generateContentHeading');
-
-      lyrics.content =
-        relation('transformContent', track.lyrics);
-    }
-
-    // Sections: Sheet music files, MIDI/proejct files, additional files
-
-    if (!empty(track.sheetMusicFiles)) {
-      sections.sheetMusicFiles = additionalFilesSection(track.sheetMusicFiles);
-    }
-
-    if (!empty(track.midiProjectFiles)) {
-      sections.midiProjectFiles = additionalFilesSection(track.midiProjectFiles);
-    }
-
-    if (!empty(track.additionalFiles)) {
-      sections.additionalFiles = additionalFilesSection(track.additionalFiles);
-    }
-
-    // Section: Artist commentary
-
-    if (track.commentary) {
-      const artistCommentary = sections.artistCommentary = {};
-
-      artistCommentary.heading =
-        relation('generateContentHeading');
-
-      artistCommentary.content =
-        relation('transformContent', track.commentary);
-    }
-
-    return relations;
-  },
-
-  data(sprawl, track) {
-    const data = {};
-
-    const {album} = track;
-
-    data.name = track.name;
-    data.date = track.date;
-    data.duration = track.duration;
-
-    data.hasUniqueCoverArt = track.hasUniqueCoverArt;
-    data.hasAlbumCoverArt = album.hasCoverArt;
-
-    if (track.hasUniqueCoverArt) {
-      data.albumCoverArtDirectory = album.directory;
-      data.trackCoverArtDirectory = track.directory;
-      data.coverArtFileExtension = track.coverArtFileExtension;
-      data.coverNeedsReveal = track.artTags.some(t => t.isContentWarning);
-
-      if (track.coverArtDate && +track.coverArtDate !== +track.date) {
-        data.coverArtDate = track.coverArtDate;
-      }
-    } else if (track.album.hasCoverArt) {
-      data.albumCoverArtDirectory = album.directory;
-      data.coverArtFileExtension = album.coverArtFileExtension;
-      data.coverNeedsReveal = album.artTags.some(t => t.isContentWarning);
-    }
-
-    if (!empty(track.additionalFiles)) {
-      data.numAdditionalFiles = track.additionalFiles.length;
-    }
-
-    return data;
-  },
-
-  generate(data, relations, {html, language}) {
-    const content = {};
-
-    const {sections: sec} = relations;
-
-    const formatContributions =
-      (stringKey, contributionLinks, {showContribution = true, showIcons = true} = {}) =>
-        contributionLinks &&
-          language.$(stringKey, {
-            artists:
-              language.formatConjunctionList(
-                contributionLinks.map(link =>
-                  link.slots({showContribution, showIcons}))),
-          });
-
-    if (data.hasUniqueCoverArt || data.hasAlbumCoverArt) {
-      content.cover = relations.cover
-        .slots({
-          alt: language.$('misc.alt.trackCover'),
-        });
-      content.coverNeedsReveal = data.coverNeedsReveal;
-    } else {
-      content.cover = null;
-      content.coverNeedsReveal = null;
-    }
-
-    content.main = {
-      headingMode: 'sticky',
-
-      content: html.tags([
-        html.tag('p', {
-          [html.onlyIfContent]: true,
-          [html.joinChildren]: html.tag('br'),
-        }, [
-          formatContributions('releaseInfo.by', sec.releaseInfo.artistContributionLinks),
-          formatContributions('releaseInfo.coverArtBy', sec.releaseInfo.coverArtistContributionLinks),
-
-          data.date &&
-            language.$('releaseInfo.released', {
-              date: language.formatDate(data.date),
-            }),
-
-          data.coverArtDate &&
-            language.$('releaseInfo.artReleased', {
-              date: language.formatDate(data.coverArtDate),
-            }),
-
-          data.duration &&
-            language.$('releaseInfo.duration', {
-              duration: language.formatDuration(data.duration),
-            }),
-        ]),
-
-        html.tag('p',
-          (sec.listen.externalLinks
-            ? language.$('releaseInfo.listenOn', {
-                links: language.formatDisjunctionList(sec.listen.externalLinks),
-              })
-            : language.$('releaseInfo.listenOn.noLinks', {
-                name: html.tag('i', data.name),
-              }))),
-
-        html.tag('p',
-          {
-            [html.onlyIfContent]: true,
-            [html.joinChildren]: '<br>',
-          },
-          [
-            sec.sheetMusicFiles &&
-              language.$('releaseInfo.sheetMusicFiles.shortcut', {
-                link: html.tag('a',
-                  {href: '#sheet-music-files'},
-                  language.$('releaseInfo.sheetMusicFiles.shortcut.link')),
-              }),
-
-            sec.midiProjectFiles &&
-              language.$('releaseInfo.midiProjectFiles.shortcut', {
-                link: html.tag('a',
-                  {href: '#midi-project-files'},
-                  language.$('releaseInfo.midiProjectFiles.shortcut.link')),
-              }),
-
-            sec.additionalFiles &&
-              sec.extra.additionalFilesShortcut,
-
-            sec.artistCommentary &&
-              language.$('releaseInfo.readCommentary', {
-                link: html.tag('a',
-                  {href: '#artist-commentary'},
-                  language.$('releaseInfo.readCommentary.link')),
-              }),
-          ]),
-
-        sec.otherReleases && [
-          sec.otherReleases.heading
-            .slots({
-              id: 'also-released-as',
-              title: language.$('releaseInfo.alsoReleasedAs'),
-            }),
-
-          html.tag('ul',
-            sec.otherReleases.items.map(({trackLink, albumLink}) =>
-              html.tag('li',
-                language.$('releaseInfo.alsoReleasedAs.item', {
-                  track: trackLink,
-                  album: albumLink,
-                })))),
-        ],
-
-        sec.contributors && [
-          sec.contributors.heading
-            .slots({
-              id: 'contributors',
-              title: language.$('releaseInfo.contributors'),
-            }),
-
-          html.tag('ul', sec.contributors.contributionLinks.map(contributionLink =>
-            html.tag('li',
-              contributionLink
-                .slots({
-                  showIcons: true,
-                  showContribution: true,
-                })))),
-        ],
-
-        sec.references && [
-          sec.references.heading
-            .slots({
-              id: 'references',
-              title:
-                language.$('releaseInfo.tracksReferenced', {
-                  track: html.tag('i', data.name),
-                }),
-            }),
-
-          sec.references.list,
-        ],
-
-        sec.referencedBy && [
-          sec.referencedBy.heading
-            .slots({
-              id: 'referenced-by',
-              title:
-                language.$('releaseInfo.tracksThatReference', {
-                  track: html.tag('i', data.name),
-                }),
-            }),
-
-          sec.referencedBy.list,
-        ],
-
-        sec.samples && [
-          sec.samples.heading
-            .slots({
-              id: 'samples',
-              title:
-                language.$('releaseInfo.tracksSampled', {
-                  track: html.tag('i', data.name),
-                }),
-            }),
-
-          sec.samples.list,
-        ],
-
-        sec.sampledBy && [
-          sec.sampledBy.heading
-            .slots({
-              id: 'referenced-by',
-              title:
-                language.$('releaseInfo.tracksThatSample', {
-                  track: html.tag('i', data.name),
-                }),
-            }),
-
-          sec.sampledBy.list,
-        ],
-        sec.flashesThatFeature && [
-          sec.flashesThatFeature.heading
-            .slots({
-              id: 'featured-in',
-              title:
-                language.$('releaseInfo.flashesThatFeature', {
-                  track: html.tag('i', data.name),
-                }),
-            }),
-
-          html.tag('ul', sec.flashesThatFeature.entries.map(({flashLink, trackLink}) =>
-            (trackLink
-              ? html.tag('li', {class: 'rerelease'},
-                  language.$('releaseInfo.flashesThatFeature.item.asDifferentRelease', {
-                    flash: flashLink,
-                    track: trackLink,
-                  }))
-              : html.tag('li',
-                  language.$('releaseInfo.flashesThatFeature.item', {
-                    flash: flashLink,
-                  }))))),
-        ],
-
-        sec.lyrics && [
-          sec.lyrics.heading
-            .slots({
-              id: 'lyrics',
-              title: language.$('releaseInfo.lyrics'),
-            }),
-
-          html.tag('blockquote',
-            sec.lyrics.content
-              .slot('mode', 'lyrics')),
-        ],
-
-        sec.sheetMusicFiles && [
-          sec.sheetMusicFiles.heading
-            .slots({
-              id: 'sheet-music-files',
-              title: language.$('releaseInfo.sheetMusicFiles.heading'),
-            }),
-
-          sec.sheetMusicFiles.list,
-        ],
-
-        sec.midiProjectFiles && [
-          sec.midiProjectFiles.heading
-            .slots({
-              id: 'midi-project-files',
-              title: language.$('releaseInfo.midiProjectFiles.heading'),
-            }),
-
-          sec.midiProjectFiles.list,
-        ],
-
-        sec.additionalFiles && [
-          sec.additionalFiles.heading
-            .slots({
-              id: 'additional-files',
-              title:
-                language.$('releaseInfo.additionalFiles.heading', {
-                  additionalFiles:
-                    language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}),
-                }),
-            }),
-
-          sec.additionalFiles.list,
-        ],
-
-        sec.artistCommentary && [
-          sec.artistCommentary.heading
-            .slots({
-              id: 'artist-commentary',
-              title: language.$('releaseInfo.artistCommentary')
-            }),
-
-          html.tag('blockquote',
-            sec.artistCommentary.content
-              .slot('mode', 'multiline')),
-        ],
-      ]),
-    };
-
-    return content;
-  },
-};
-
-/*
-  const generateCommentary = ({language, link, transformMultiline}) =>
-    transformMultiline([
-      track.commentary,
-      ...otherReleases.map((track) =>
-        track.commentary
-          ?.split('\n')
-          .filter((line) => line.replace(/<\/b>/g, '').includes(':</i>'))
-          .flatMap(line => [
-            line,
-            language.$('releaseInfo.artistCommentary.seeOriginalRelease', {
-              original: link.track(track),
-            }),
-          ])
-          .join('\n')
-      ),
-    ].filter(Boolean).join('\n'));
-
-  const data = {
-    type: 'data',
-    path: ['track', track.directory],
-    data: ({
-      serializeContribs,
-      serializeCover,
-      serializeGroupsForTrack,
-      serializeLink,
-    }) => ({
-      name: track.name,
-      directory: track.directory,
-      dates: {
-        released: track.date,
-        originallyReleased: track.originalDate,
-        coverArtAdded: track.coverArtDate,
-      },
-      duration: track.duration,
-      color: track.color,
-      cover: serializeCover(track, getTrackCover),
-      artistsContribs: serializeContribs(track.artistContribs),
-      contributorContribs: serializeContribs(track.contributorContribs),
-      coverArtistContribs: serializeContribs(track.coverArtistContribs || []),
-      album: serializeLink(track.album),
-      groups: serializeGroupsForTrack(track),
-      references: track.references.map(serializeLink),
-      referencedBy: track.referencedBy.map(serializeLink),
-      alsoReleasedAs: otherReleases.map((track) => ({
-        track: serializeLink(track),
-        album: serializeLink(track.album),
-      })),
-    }),
-  };
-
-  const getSocialEmbedDescription = ({
-    getArtistString: _getArtistString,
-    language,
-  }) => {
-    const hasArtists = !empty(track.artistContribs);
-    const hasCoverArtists = !empty(track.coverArtistContribs);
-    const getArtistString = (contribs) =>
-      _getArtistString(contribs, {
-        // We don't want to put actual HTML tags in social embeds (sadly
-        // they don't get parsed and displayed, generally speaking), so
-        // override the link argument so that artist "links" just show
-        // their names.
-        link: {artist: (artist) => artist.name},
-      });
-    if (!hasArtists && !hasCoverArtists) return '';
-    return language.formatString(
-      'trackPage.socialEmbed.body' +
-        [hasArtists && '.withArtists', hasCoverArtists && '.withCoverArtists']
-          .filter(Boolean)
-          .join(''),
-      Object.fromEntries(
-        [
-          hasArtists && ['artists', getArtistString(track.artistContribs)],
-          hasCoverArtists && [
-            'coverArtists',
-            getArtistString(track.coverArtistContribs),
-          ],
-        ].filter(Boolean)
-      )
-    );
-  };
-
-  const page = {
-    page: () => {
-      return {
-        title: language.$('trackPage.title', {track: track.name}),
-        stylesheet: getAlbumStylesheet(album, {to}),
-
-        themeColor: track.color,
-        theme:
-          getThemeString(track.color, {
-            additionalVariables: [
-              `--album-directory: ${album.directory}`,
-              `--track-directory: ${track.directory}`,
-            ]
-          }),
-
-        socialEmbed: {
-          heading: language.$('trackPage.socialEmbed.heading', {
-            album: track.album.name,
-          }),
-          headingLink: absoluteTo('localized.album', album.directory),
-          title: language.$('trackPage.socialEmbed.title', {
-            track: track.name,
-          }),
-          description: getSocialEmbedDescription({getArtistString, language}),
-          image: '/' + getTrackCover(track, {to: urls.from('shared.root').to}),
-          color: track.color,
-        },
-
-        secondaryNav: generateAlbumSecondaryNav(album, track, {
-          getLinkThemeString,
-          html,
-          language,
-          link,
-        }),
-      };
-    },
-  };
-*/
diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js
index 4ccdf58d..03aa9836 100644
--- a/src/content/dependencies/linkThing.js
+++ b/src/content/dependencies/linkThing.js
@@ -25,70 +25,68 @@ export default {
     };
   },
 
-  generate(data, relations, {html}) {
+  slots: {
+    // content: relations.linkTemplate.getSlotDescription('content'),
+    content: {type: 'html'},
+
+    preferShortName: {type: 'boolean', default: false},
+
+    tooltip: {
+      validate: v => v.oneOf(v.isBoolean, v.isString),
+      default: false,
+    },
+
+    color: {
+      validate: v => v.oneOf(v.isBoolean, v.isColor),
+      default: true,
+    },
+
+    anchor: {type: 'boolean', default: false},
+
+    // attributes: relations.linkTemplate.getSlotDescription('attributes'),
+    // hash: relations.linkTemplate.getSlotDescription('hash'),
+    attributes: {validate: v => v.isAttributes},
+    hash: {type: 'string'},
+  },
+
+  generate(data, relations, slots, {html}) {
     const path = [data.pathKey, data.directory];
 
-    return html.template({
-      annotation: 'linkThing',
-
-      slots: {
-        content: relations.linkTemplate.getSlotDescription('content'),
-        preferShortName: {type: 'boolean', default: false},
-
-        tooltip: {
-          validate: v => v.oneOf(v.isBoolean, v.isString),
-          default: false,
-        },
-
-        color: {
-          validate: v => v.oneOf(v.isBoolean, v.isColor),
-          default: true,
-        },
-
-        anchor: {type: 'boolean', default: false},
-
-        attributes: relations.linkTemplate.getSlotDescription('attributes'),
-        hash: relations.linkTemplate.getSlotDescription('hash'),
-      },
-
-      content(slots) {
-        let content = slots.content;
-
-        const name =
-          (slots.preferShortName
-            ? data.nameShort ?? data.name
-            : data.name);
-
-        if (html.isBlank(content)) {
-          content = name;
-        }
-
-        let color = null;
-        if (slots.color === true) {
-          color = data.color ?? null;
-        } else if (typeof slots.color === 'string') {
-          color = slots.color;
-        }
-
-        let tooltip = null;
-        if (slots.tooltip === true) {
-          tooltip = name;
-        } else if (typeof slots.tooltip === 'string') {
-          tooltip = slots.tooltip;
-        }
-
-        return relations.linkTemplate
-          .slots({
-            path: slots.anchor ? [] : path,
-            href: slots.anchor ? '' : null,
-            content,
-            color,
-            tooltip,
-
-            attributes: slots.attributes,
-            hash: slots.hash,
-          });
-      },
-    });
+    let content = slots.content;
+
+    const name =
+      (slots.preferShortName
+        ? data.nameShort ?? data.name
+        : data.name);
+
+    if (html.isBlank(content)) {
+      content = name;
+    }
+
+    let color = null;
+    if (slots.color === true) {
+      color = data.color ?? null;
+    } else if (typeof slots.color === 'string') {
+      color = slots.color;
+    }
+
+    let tooltip = null;
+    if (slots.tooltip === true) {
+      tooltip = name;
+    } else if (typeof slots.tooltip === 'string') {
+      tooltip = slots.tooltip;
+    }
+
+    return relations.linkTemplate
+      .slots({
+        path: slots.anchor ? [] : path,
+        href: slots.anchor ? '' : null,
+        content,
+        color,
+        tooltip,
+
+        attributes: slots.attributes,
+        hash: slots.hash,
+      });
   },
 }