« get me outta code hell

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:
Diffstat (limited to 'src/content/dependencies')
-rw-r--r--src/content/dependencies/generateAlbumArtInfoBox.js39
-rw-r--r--src/content/dependencies/generateAlbumArtworkColumn.js38
-rw-r--r--src/content/dependencies/generateAlbumCommentaryPage.js7
-rw-r--r--src/content/dependencies/generateAlbumCoverArtwork.js100
-rw-r--r--src/content/dependencies/generateAlbumGalleryAlbumGrid.js90
-rw-r--r--src/content/dependencies/generateAlbumGalleryPage.js242
-rw-r--r--src/content/dependencies/generateAlbumGalleryTrackGrid.js122
-rw-r--r--src/content/dependencies/generateAlbumInfoPage.js16
-rw-r--r--src/content/dependencies/generateAlbumReferencedArtworksPage.js12
-rw-r--r--src/content/dependencies/generateAlbumReferencingArtworksPage.js12
-rw-r--r--src/content/dependencies/generateAlbumReleaseInfo.js23
-rw-r--r--src/content/dependencies/generateAlbumSecondaryNavGroupPart.js2
-rw-r--r--src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js2
-rw-r--r--src/content/dependencies/generateAlbumSidebarTrackSection.js114
-rw-r--r--src/content/dependencies/generateAlbumSocialEmbed.js5
-rw-r--r--src/content/dependencies/generateAlbumTrackList.js12
-rw-r--r--src/content/dependencies/generateArtTagAncestorDescendantMapList.js4
-rw-r--r--src/content/dependencies/generateArtTagGalleryPage.js71
-rw-r--r--src/content/dependencies/generateArtTagInfoPage.js8
-rw-r--r--src/content/dependencies/generateArtTagSidebar.js4
-rw-r--r--src/content/dependencies/generateArtistArtworkColumn.js13
-rw-r--r--src/content/dependencies/generateArtistCredit.js18
-rw-r--r--src/content/dependencies/generateArtistGalleryPage.js150
-rw-r--r--src/content/dependencies/generateArtistInfoPage.js43
-rw-r--r--src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js2
-rw-r--r--src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js7
-rw-r--r--src/content/dependencies/generateCommentaryEntry.js8
-rw-r--r--src/content/dependencies/generateContributionTooltipChronologySection.js24
-rw-r--r--src/content/dependencies/generateCoverArtwork.js159
-rw-r--r--src/content/dependencies/generateCoverArtworkArtTagDetails.js8
-rw-r--r--src/content/dependencies/generateCoverArtworkArtistDetails.js6
-rw-r--r--src/content/dependencies/generateCoverArtworkOriginDetails.js98
-rw-r--r--src/content/dependencies/generateCoverArtworkReferenceDetails.js26
-rw-r--r--src/content/dependencies/generateCoverGrid.js11
-rw-r--r--src/content/dependencies/generateFlashActGalleryPage.js16
-rw-r--r--src/content/dependencies/generateFlashArtworkColumn.js11
-rw-r--r--src/content/dependencies/generateFlashCoverArtwork.js41
-rw-r--r--src/content/dependencies/generateFlashIndexPage.js17
-rw-r--r--src/content/dependencies/generateFlashInfoPage.js8
-rw-r--r--src/content/dependencies/generateGroupGalleryPage.js33
-rw-r--r--src/content/dependencies/generateIntrapageDotSwitcher.js2
-rw-r--r--src/content/dependencies/generateLyricsEntry.js25
-rw-r--r--src/content/dependencies/generateLyricsSection.js81
-rw-r--r--src/content/dependencies/generatePageLayout.js60
-rw-r--r--src/content/dependencies/generateReferencedArtworksPage.js72
-rw-r--r--src/content/dependencies/generateReferencingArtworksPage.js72
-rw-r--r--src/content/dependencies/generateTrackArtworkColumn.js33
-rw-r--r--src/content/dependencies/generateTrackCoverArtwork.js143
-rw-r--r--src/content/dependencies/generateTrackInfoPage.js34
-rw-r--r--src/content/dependencies/generateTrackNavLinks.js2
-rw-r--r--src/content/dependencies/generateTrackReferencedArtworksPage.js12
-rw-r--r--src/content/dependencies/generateTrackReferencingArtworksPage.js12
-rw-r--r--src/content/dependencies/generateTrackReleaseInfo.js16
-rw-r--r--src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js25
-rw-r--r--src/content/dependencies/generateWikiHomepageAlbumGridRow.js17
-rw-r--r--src/content/dependencies/image.js126
-rw-r--r--src/content/dependencies/linkAnythingMan.js3
-rw-r--r--src/content/dependencies/linkArtwork.js20
-rw-r--r--src/content/dependencies/linkReferencedArtworks.js24
-rw-r--r--src/content/dependencies/linkReferencingArtworks.js24
-rw-r--r--src/content/dependencies/listArtTagNetwork.js10
-rw-r--r--src/content/dependencies/listArtTagsByName.js4
-rw-r--r--src/content/dependencies/listArtTagsByUses.js4
-rw-r--r--src/content/dependencies/listArtistsByGroup.js23
-rw-r--r--src/content/dependencies/listArtistsByLatestContribution.js5
-rw-r--r--src/content/dependencies/listTracksWithLyrics.js2
-rw-r--r--src/content/dependencies/transformContent.js35
67 files changed, 1328 insertions, 1180 deletions
diff --git a/src/content/dependencies/generateAlbumArtInfoBox.js b/src/content/dependencies/generateAlbumArtInfoBox.js
new file mode 100644
index 00000000..8c44c930
--- /dev/null
+++ b/src/content/dependencies/generateAlbumArtInfoBox.js
@@ -0,0 +1,39 @@
+export default {
+  contentDependencies: ['generateReleaseInfoContributionsLine'],
+  extraDependencies: ['html', 'language'],
+
+  relations: (relation, album) => ({
+    wallpaperArtistContributionsLine:
+      (album.wallpaperArtwork
+        ? relation('generateReleaseInfoContributionsLine',
+            album.wallpaperArtwork.artistContribs)
+        : null),
+
+    bannerArtistContributionsLine:
+      (album.bannerArtwork
+        ? relation('generateReleaseInfoContributionsLine',
+            album.bannerArtwork.artistContribs)
+        : null),
+  }),
+
+  generate: (relations, {html, language}) =>
+    language.encapsulate('releaseInfo', capsule =>
+      html.tag('div', {class: 'album-art-info'},
+        {[html.onlyIfContent]: true},
+
+        html.tag('p',
+          {[html.onlyIfContent]: true},
+          {[html.joinChildren]: html.tag('br')},
+
+          [
+            relations.wallpaperArtistContributionsLine?.slots({
+              stringKey: capsule + '.wallpaperArtBy',
+              chronologyKind: 'wallpaperArt',
+            }),
+
+            relations.bannerArtistContributionsLine?.slots({
+              stringKey: capsule + '.bannerArtBy',
+              chronologyKind: 'bannerArt',
+            }),
+          ]))),
+};
diff --git a/src/content/dependencies/generateAlbumArtworkColumn.js b/src/content/dependencies/generateAlbumArtworkColumn.js
new file mode 100644
index 00000000..e6762463
--- /dev/null
+++ b/src/content/dependencies/generateAlbumArtworkColumn.js
@@ -0,0 +1,38 @@
+export default {
+  contentDependencies: ['generateAlbumArtInfoBox', 'generateCoverArtwork'],
+  extraDependencies: ['html'],
+
+  relations: (relation, album) => ({
+    firstCover:
+      (album.hasCoverArt
+        ? relation('generateCoverArtwork', album.coverArtworks[0])
+        : null),
+
+    restCovers:
+      (album.hasCoverArt
+        ? album.coverArtworks.slice(1).map(artwork =>
+            relation('generateCoverArtwork', artwork))
+        : []),
+
+    albumArtInfoBox:
+      relation('generateAlbumArtInfoBox', album),
+  }),
+
+  generate: (relations, {html}) =>
+    html.tags([
+      relations.firstCover?.slots({
+        showOriginDetails: true,
+        showArtTagDetails: true,
+        showReferenceDetails: true,
+      }),
+
+      relations.albumArtInfoBox,
+
+      relations.restCovers.map(cover =>
+        cover.slots({
+          showOriginDetails: true,
+          showArtTagDetails: true,
+          showReferenceDetails: true,
+        })),
+    ]),
+};
diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js
index f5df7c3d..1e39b47d 100644
--- a/src/content/dependencies/generateAlbumCommentaryPage.js
+++ b/src/content/dependencies/generateAlbumCommentaryPage.js
@@ -3,13 +3,12 @@ import {empty, stitchArrays} from '#sugar';
 export default {
   contentDependencies: [
     'generateAlbumCommentarySidebar',
-    'generateAlbumCoverArtwork',
     'generateAlbumNavAccent',
     'generateAlbumSecondaryNav',
     'generateAlbumStyleRules',
     'generateCommentaryEntry',
     'generateContentHeading',
-    'generateTrackCoverArtwork',
+    'generateCoverArtwork',
     'generatePageLayout',
     'linkAlbum',
     'linkExternal',
@@ -66,7 +65,7 @@ export default {
 
       if (album.hasCoverArt) {
         relations.albumCommentaryCover =
-          relation('generateAlbumCoverArtwork', album);
+          relation('generateCoverArtwork', album.coverArtworks[0]);
       }
 
       relations.albumCommentaryEntries =
@@ -91,7 +90,7 @@ export default {
       query.tracksWithCommentary
         .map(track =>
           (track.hasUniqueCoverArt
-            ? relation('generateTrackCoverArtwork', track)
+            ? relation('generateCoverArtwork', track.trackArtworks[0])
             : null));
 
     relations.trackCommentaryEntries =
diff --git a/src/content/dependencies/generateAlbumCoverArtwork.js b/src/content/dependencies/generateAlbumCoverArtwork.js
deleted file mode 100644
index ff7d2b85..00000000
--- a/src/content/dependencies/generateAlbumCoverArtwork.js
+++ /dev/null
@@ -1,100 +0,0 @@
-export default {
-  contentDependencies: [
-    'generateCoverArtwork',
-    'generateCoverArtworkArtTagDetails',
-    'generateCoverArtworkArtistDetails',
-    'generateCoverArtworkReferenceDetails',
-    'image',
-    'linkAlbumReferencedArtworks',
-    'linkAlbumReferencingArtworks',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
-  relations: (relation, album) => ({
-    coverArtwork:
-      relation('generateCoverArtwork'),
-
-    image:
-      relation('image'),
-
-    artTagDetails:
-      relation('generateCoverArtworkArtTagDetails', album.artTags),
-
-    artistDetails:
-      relation('generateCoverArtworkArtistDetails', album.coverArtistContribs),
-
-    referenceDetails:
-      relation('generateCoverArtworkReferenceDetails',
-        album.referencedArtworks,
-        album.referencedByArtworks),
-
-    referencedArtworksLink:
-      relation('linkAlbumReferencedArtworks', album),
-
-    referencingArtworksLink:
-      relation('linkAlbumReferencingArtworks', album),
-  }),
-
-  data: (album) => ({
-    path:
-      ['media.albumCover', album.directory, album.coverArtFileExtension],
-
-    color:
-      album.color,
-
-    dimensions:
-      album.coverArtDimensions,
-
-    warnings:
-      album.artTags
-        .filter(tag => tag.isContentWarning)
-        .map(tag => tag.name),
-  }),
-
-  slots: {
-    mode: {type: 'string'},
-
-    details: {
-      validate: v => v.is('tags', 'artists'),
-      default: 'tags',
-    },
-
-    showReferenceLinks: {
-      type: 'boolean',
-      default: false,
-    },
-  },
-
-  generate: (data, relations, slots, {language}) =>
-    relations.coverArtwork.slots({
-      mode: slots.mode,
-
-      image:
-        relations.image.slots({
-          path: data.path,
-          color: data.color,
-          alt: language.$('misc.alt.albumCover'),
-        }),
-
-      dimensions: data.dimensions,
-      warnings: data.warnings,
-
-      details: [
-        slots.details === 'tags' &&
-          relations.artTagDetails,
-
-        slots.details === 'artists' &&
-          relations.artistDetails,
-
-        slots.showReferenceLinks &&
-          relations.referenceDetails.slots({
-            referencedLink:
-              relations.referencedArtworksLink,
-
-            referencingLink:
-              relations.referencingArtworksLink,
-          }),
-      ],
-    }),
-};
diff --git a/src/content/dependencies/generateAlbumGalleryAlbumGrid.js b/src/content/dependencies/generateAlbumGalleryAlbumGrid.js
new file mode 100644
index 00000000..7f152871
--- /dev/null
+++ b/src/content/dependencies/generateAlbumGalleryAlbumGrid.js
@@ -0,0 +1,90 @@
+import {stitchArrays} from '#sugar';
+
+export default {
+  contentDependencies: [
+    'generateCoverGrid',
+    'image',
+    'linkAlbum',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  query: (album) => ({
+    artworks:
+      (album.hasCoverArt
+        ? album.coverArtworks
+        : []),
+  }),
+
+  relations: (relation, query, album) => ({
+    coverGrid:
+      relation('generateCoverGrid'),
+
+    albumLinks:
+      query.artworks.map(_artwork =>
+        relation('linkAlbum', album)),
+
+    images:
+      query.artworks
+        .map(artwork => relation('image', artwork)),
+  }),
+
+  data: (query, album) => ({
+    albumName:
+      album.name,
+
+    artworkLabels:
+      query.artworks
+        .map(artwork => artwork.label),
+
+    artworkArtists:
+      query.artworks
+        .map(artwork => artwork.artistContribs
+          .map(contrib => contrib.artist.name)),
+  }),
+
+  slots: {
+    attributes: {type: 'attributes', mutable: false},
+  },
+
+  generate: (data, relations, slots, {html, language}) =>
+    html.tag('div',
+      {[html.onlyIfContent]: true},
+
+      slots.attributes,
+
+      [
+        relations.coverArtistsLine,
+
+        relations.coverGrid.slots({
+          links:
+            relations.albumLinks,
+
+          names:
+            data.artworkLabels
+              .map(label => label ?? data.albumName),
+
+          images:
+            stitchArrays({
+              image: relations.images,
+              label: data.artworkLabels,
+            }).map(({image, label}) =>
+                image.slots({
+                  missingSourceContent:
+                    language.$('misc.albumGalleryGrid.noCoverArt', {
+                      name:
+                        label ?? data.albumName,
+                    }),
+                })),
+
+          info:
+            data.artworkArtists.map(artists =>
+              language.$('misc.coverGrid.details.coverArtists', {
+                [language.onlyIfOptions]: ['artists'],
+
+                artists:
+                  language.formatUnitList(artists),
+              })),
+        }),
+      ]),
+};
diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js
index b48d92af..2ba3b272 100644
--- a/src/content/dependencies/generateAlbumGalleryPage.js
+++ b/src/content/dependencies/generateAlbumGalleryPage.js
@@ -1,18 +1,18 @@
-import {compareArrays, stitchArrays} from '#sugar';
+import {stitchArrays, unique} from '#sugar';
+import {getKebabCase} from '#wiki-data';
 
 export default {
   contentDependencies: [
-    'generateAlbumGalleryCoverArtistsLine',
+    'generateAlbumGalleryAlbumGrid',
     'generateAlbumGalleryNoTrackArtworksLine',
     'generateAlbumGalleryStatsLine',
+    'generateAlbumGalleryTrackGrid',
     'generateAlbumNavAccent',
     'generateAlbumSecondaryNav',
     'generateAlbumStyleRules',
-    'generateCoverGrid',
+    'generateIntrapageDotSwitcher',
     'generatePageLayout',
-    'image',
     'linkAlbum',
-    'linkTrack',
   ],
 
   extraDependencies: ['html', 'language'],
@@ -20,147 +20,82 @@ export default {
   query(album) {
     const query = {};
 
-    const tracksWithUniqueCoverArt =
+    const trackArtworkLabels =
       album.tracks
-        .filter(track => track.hasUniqueCoverArt);
-
-    // Don't display "all artwork by..." for albums where there's
-    // only one unique artwork in the first place.
-    if (tracksWithUniqueCoverArt.length > 1) {
-      const allCoverArtistArrays =
-        tracksWithUniqueCoverArt
-          .map(track => track.coverArtistContribs)
-          .map(contribs => contribs.map(contrib => contrib.artist));
-
-      const allSameCoverArtists =
-        allCoverArtistArrays
-          .slice(1)
-          .every(artists => compareArrays(artists, allCoverArtistArrays[0]));
-
-      if (allSameCoverArtists) {
-        query.coverArtistsForAllTracks =
-          allCoverArtistArrays[0];
-      }
-    }
+        .map(track => track.trackArtworks
+          .map(artwork => artwork.label));
+
+    const recurranceThreshold = 2;
+
+    // This list may include null, if some artworks are not labelled!
+    // That's expected.
+    query.recurringTrackArtworkLabels =
+      unique(trackArtworkLabels.flat())
+        .filter(label =>
+          trackArtworkLabels
+            .filter(labels => labels.includes(label))
+            .length >=
+          (label === null
+            ? 1
+            : recurranceThreshold));
 
     return query;
   },
 
-  relations(relation, query, album) {
-    const relations = {};
+  relations: (relation, query, album) => ({
+    layout:
+      relation('generatePageLayout'),
 
-    relations.layout =
-      relation('generatePageLayout');
+    albumStyleRules:
+      relation('generateAlbumStyleRules', album, null),
 
-    relations.albumStyleRules =
-      relation('generateAlbumStyleRules', album, null);
-
-    relations.albumLink =
-      relation('linkAlbum', album);
-
-    relations.albumNavAccent =
-      relation('generateAlbumNavAccent', album, null);
-
-    relations.secondaryNav =
-      relation('generateAlbumSecondaryNav', album);
-
-    relations.statsLine =
-      relation('generateAlbumGalleryStatsLine', album);
-
-    if (album.tracks.every(track => !track.hasUniqueCoverArt)) {
-      relations.noTrackArtworksLine =
-        relation('generateAlbumGalleryNoTrackArtworksLine');
-    }
-
-    if (query.coverArtistsForAllTracks) {
-      relations.coverArtistsLine =
-        relation('generateAlbumGalleryCoverArtistsLine', query.coverArtistsForAllTracks);
-    }
-
-    relations.coverGrid =
-      relation('generateCoverGrid');
-
-    relations.links = [
+    albumLink:
       relation('linkAlbum', album),
 
-      ...
-        album.tracks
-          .map(track => relation('linkTrack', track)),
-    ];
-
-    relations.images = [
-      (album.hasCoverArt
-        ? relation('image', album.artTags)
-        : relation('image')),
+    albumNavAccent:
+      relation('generateAlbumNavAccent', album, null),
 
-      ...
-        album.tracks.map(track =>
-          (track.hasUniqueCoverArt
-            ? relation('image', track.artTags)
-            : relation('image'))),
-    ];
-
-    return relations;
-  },
+    secondaryNav:
+      relation('generateAlbumSecondaryNav', album),
 
-  data(query, album) {
-    const data = {};
+    statsLine:
+      relation('generateAlbumGalleryStatsLine', album),
 
-    data.name = album.name;
-    data.color = album.color;
-
-    data.names = [
-      album.name,
-      ...album.tracks.map(track => track.name),
-    ];
-
-    data.coverArtists = [
-      (album.hasCoverArt
-        ? album.coverArtistContribs.map(({artist}) => artist.name)
+    noTrackArtworksLine:
+      (album.tracks.every(track => !track.hasUniqueCoverArt)
+        ? relation('generateAlbumGalleryNoTrackArtworksLine')
         : null),
 
-      ...
-        album.tracks.map(track => {
-          if (query.coverArtistsForAllTracks) {
-            return null;
-          }
+    setSwitcher:
+      relation('generateIntrapageDotSwitcher'),
 
-          if (track.hasUniqueCoverArt) {
-            return track.coverArtistContribs.map(({artist}) => artist.name);
-          }
+    albumGrid:
+      relation('generateAlbumGalleryAlbumGrid', album),
 
-          return null;
-        }),
-    ];
-
-    data.paths = [
-      (album.hasCoverArt
-        ? ['media.albumCover', album.directory, album.coverArtFileExtension]
-        : null),
+    trackGrids:
+      query.recurringTrackArtworkLabels.map(label =>
+        relation('generateAlbumGalleryTrackGrid', album, label)),
+  }),
 
-      ...
-        album.tracks.map(track =>
-          (track.hasUniqueCoverArt
-            ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension]
-            : null)),
-    ];
+  data: (query, album) => ({
+    trackGridLabels:
+      query.recurringTrackArtworkLabels,
 
-    data.dimensions = [
-      (album.hasCoverArt
-        ? album.coverArtDimensions
-        : null),
+    trackGridIDs:
+      query.recurringTrackArtworkLabels.map(label =>
+        'track-grid-' +
+          (label
+            ? getKebabCase(label)
+            : 'no-label')),
 
-      ...
-        album.tracks.map(track =>
-          (track.hasUniqueCoverArt
-            ? track.coverArtDimensions
-            : null)),
-    ];
+    name:
+      album.name,
 
-    return data;
-  },
+    color:
+      album.color,
+  }),
 
-  generate: (data, relations, {language}) =>
+  generate: (data, relations, {html, language}) =>
     language.encapsulate('albumGalleryPage', pageCapsule =>
       relations.layout.slots({
         title:
@@ -176,34 +111,39 @@ export default {
         mainClasses: ['top-index'],
         mainContent: [
           relations.statsLine,
-          relations.coverArtistsLine,
+
+          relations.albumGrid,
+
           relations.noTrackArtworksLine,
 
-          relations.coverGrid
-            .slots({
-              links: relations.links,
-              names: data.names,
-              images:
-                stitchArrays({
-                  image: relations.images,
-                  path: data.paths,
-                  dimensions: data.dimensions,
-                  name: data.names,
-                }).map(({image, path, dimensions, name}) =>
-                    image.slots({
-                      path,
-                      dimensions,
-                      missingSourceContent:
-                        language.$('misc.albumGalleryGrid.noCoverArt', {name}),
-                    })),
-              info:
-                data.coverArtists.map(names =>
-                  (names === null
-                    ? null
-                    : language.$('misc.coverGrid.details.coverArtists', {
-                        artists: language.formatUnitList(names),
-                      }))),
-            }),
+          data.trackGridLabels.some(value => value !== null) &&
+            html.tag('p', {class: 'gallery-set-switcher'},
+              language.encapsulate(pageCapsule, 'setSwitcher', switcherCapsule =>
+                language.$(switcherCapsule, {
+                  sets:
+                    relations.setSwitcher.slots({
+                      initialOptionIndex: 0,
+
+                      titles:
+                        data.trackGridLabels.map(label =>
+                          label ??
+                          language.$(switcherCapsule, 'unlabeledSet')),
+
+                      targetIDs:
+                        data.trackGridIDs,
+                    }),
+                }))),
+
+          stitchArrays({
+            grid: relations.trackGrids,
+            id: data.trackGridIDs,
+          }).map(({grid, id}, index) =>
+              grid.slots({
+                attributes: [
+                  {id},
+                  index >= 1 && {style: 'display: none'},
+                ],
+              })),
         ],
 
         navLinkStyle: 'hierarchical',
diff --git a/src/content/dependencies/generateAlbumGalleryTrackGrid.js b/src/content/dependencies/generateAlbumGalleryTrackGrid.js
new file mode 100644
index 00000000..85e7576c
--- /dev/null
+++ b/src/content/dependencies/generateAlbumGalleryTrackGrid.js
@@ -0,0 +1,122 @@
+import {compareArrays, stitchArrays} from '#sugar';
+
+export default {
+  contentDependencies: [
+    'generateAlbumGalleryCoverArtistsLine',
+    'generateCoverGrid',
+    'image',
+    'linkAlbum',
+    'linkTrack',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  query(album, label) {
+    const query = {};
+
+    query.artworks =
+      album.tracks.map(track =>
+        track.trackArtworks.find(artwork => artwork.label === label) ??
+        null);
+
+    const presentArtworks =
+      query.artworks.filter(Boolean);
+
+    if (presentArtworks.length > 1) {
+      const allArtistArrays =
+        presentArtworks
+          .map(artwork => artwork.artistContribs
+            .map(contrib => contrib.artist));
+
+      const allSameArtists =
+        allArtistArrays
+          .slice(1)
+          .every(artists => compareArrays(artists, allArtistArrays[0]));
+
+      if (allSameArtists) {
+        query.artistsForAllTrackArtworks =
+          allArtistArrays[0];
+      }
+    }
+
+    return query;
+  },
+
+  relations: (relation, query, album, _label) => ({
+    coverArtistsLine:
+      (query.artistsForAllTrackArtworks
+        ? relation('generateAlbumGalleryCoverArtistsLine',
+            query.artistsForAllTrackArtworks)
+        : null),
+
+    coverGrid:
+      relation('generateCoverGrid'),
+
+    albumLink:
+      relation('linkAlbum', album),
+
+    trackLinks:
+      album.tracks
+        .map(track => relation('linkTrack', track)),
+
+    images:
+      query.artworks
+        .map(artwork => relation('image', artwork)),
+  }),
+
+  data: (query, album, _label) => ({
+    trackNames:
+      album.tracks
+        .map(track => track.name),
+
+    trackArtworkArtists:
+      query.artworks.map(artwork =>
+        (query.artistsForAllTrackArtworks
+          ? null
+       : artwork
+          ? artwork.artistContribs
+              .map(contrib => contrib.artist.name)
+          : null)),
+  }),
+
+  slots: {
+    attributes: {type: 'attributes', mutable: false},
+  },
+
+  generate: (data, relations, slots, {html, language}) =>
+    html.tag('div',
+      {[html.onlyIfContent]: true},
+
+      slots.attributes,
+
+      [
+        relations.coverArtistsLine,
+
+        relations.coverGrid.slots({
+          links:
+            relations.trackLinks,
+
+          names:
+            data.trackNames,
+
+          images:
+            stitchArrays({
+              image: relations.images,
+              name: data.trackNames,
+            }).map(({image, name}) =>
+                image.slots({
+                  missingSourceContent:
+                    language.$('misc.albumGalleryGrid.noCoverArt', {name}),
+                })),
+
+          info:
+            data.trackArtworkArtists.map(artists =>
+              language.$('misc.coverGrid.details.coverArtists', {
+                [language.onlyIfOptions]: ['artists'],
+
+                artists:
+                  language.formatUnitList(artists),
+              })),
+        }),
+      ]),
+};
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index aae56637..d0788523 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -4,8 +4,8 @@ export default {
   contentDependencies: [
     'generateAdditionalNamesBox',
     'generateAlbumAdditionalFilesList',
+    'generateAlbumArtworkColumn',
     'generateAlbumBanner',
-    'generateAlbumCoverArtwork',
     'generateAlbumNavAccent',
     'generateAlbumReleaseInfo',
     'generateAlbumSecondaryNav',
@@ -44,10 +44,8 @@ export default {
     additionalNamesBox:
       relation('generateAdditionalNamesBox', album.additionalNames),
 
-    cover:
-      (album.hasCoverArt
-        ? relation('generateAlbumCoverArtwork', album)
-        : null),
+    artworkColumn:
+      relation('generateAlbumArtworkColumn', album),
 
     banner:
       (album.hasBannerArt
@@ -112,12 +110,8 @@ export default {
 
         additionalNames: relations.additionalNamesBox,
 
-        cover:
-          (relations.cover
-            ? relations.cover.slots({
-                showReferenceLinks: true,
-              })
-            : null),
+        artworkColumnContent:
+          relations.artworkColumn,
 
         mainContent: [
           relations.releaseInfo,
diff --git a/src/content/dependencies/generateAlbumReferencedArtworksPage.js b/src/content/dependencies/generateAlbumReferencedArtworksPage.js
index 3f3d77b3..7586393c 100644
--- a/src/content/dependencies/generateAlbumReferencedArtworksPage.js
+++ b/src/content/dependencies/generateAlbumReferencedArtworksPage.js
@@ -1,6 +1,5 @@
 export default {
   contentDependencies: [
-    'generateAlbumCoverArtwork',
     'generateAlbumStyleRules',
     'generateBackToAlbumLink',
     'generateReferencedArtworksPage',
@@ -11,7 +10,7 @@ export default {
 
   relations: (relation, album) => ({
     page:
-      relation('generateReferencedArtworksPage', album.referencedArtworks),
+      relation('generateReferencedArtworksPage', album.coverArtworks[0]),
 
     albumStyleRules:
       relation('generateAlbumStyleRules', album, null),
@@ -21,17 +20,11 @@ export default {
 
     backToAlbumLink:
       relation('generateBackToAlbumLink', album),
-
-    cover:
-      relation('generateAlbumCoverArtwork', album),
   }),
 
   data: (album) => ({
     name:
       album.name,
-
-    color:
-      album.color,
   }),
 
   generate: (data, relations, {html, language}) =>
@@ -42,11 +35,8 @@ export default {
             data.name,
         }),
 
-      color: data.color,
       styleRules: [relations.albumStyleRules],
 
-      cover: relations.cover,
-
       navLinks: [
         {auto: 'home'},
 
diff --git a/src/content/dependencies/generateAlbumReferencingArtworksPage.js b/src/content/dependencies/generateAlbumReferencingArtworksPage.js
index 8f2349f9..d072d2f6 100644
--- a/src/content/dependencies/generateAlbumReferencingArtworksPage.js
+++ b/src/content/dependencies/generateAlbumReferencingArtworksPage.js
@@ -1,6 +1,5 @@
 export default {
   contentDependencies: [
-    'generateAlbumCoverArtwork',
     'generateAlbumStyleRules',
     'generateBackToAlbumLink',
     'generateReferencingArtworksPage',
@@ -11,7 +10,7 @@ export default {
 
   relations: (relation, album) => ({
     page:
-      relation('generateReferencingArtworksPage', album.referencedByArtworks),
+      relation('generateReferencingArtworksPage', album.coverArtworks[0]),
 
     albumStyleRules:
       relation('generateAlbumStyleRules', album, null),
@@ -21,17 +20,11 @@ export default {
 
     backToAlbumLink:
       relation('generateBackToAlbumLink', album),
-
-    cover:
-      relation('generateAlbumCoverArtwork', album),
   }),
 
   data: (album) => ({
     name:
       album.name,
-
-    color:
-      album.color,
   }),
 
   generate: (data, relations, {html, language}) =>
@@ -42,11 +35,8 @@ export default {
             data.name,
         }),
 
-      color: data.color,
       styleRules: [relations.albumStyleRules],
 
-      cover: relations.cover,
-
       navLinks: [
         {auto: 'home'},
 
diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js
index 217282c0..0abb412c 100644
--- a/src/content/dependencies/generateAlbumReleaseInfo.js
+++ b/src/content/dependencies/generateAlbumReleaseInfo.js
@@ -14,9 +14,6 @@ export default {
     relations.artistContributionsLine =
       relation('generateReleaseInfoContributionsLine', album.artistContribs);
 
-    relations.coverArtistContributionsLine =
-      relation('generateReleaseInfoContributionsLine', album.coverArtistContribs);
-
     relations.wallpaperArtistContributionsLine =
       relation('generateReleaseInfoContributionsLine', album.wallpaperArtistContribs);
 
@@ -73,31 +70,11 @@ export default {
               chronologyKind: 'album',
             }),
 
-            relations.coverArtistContributionsLine.slots({
-              stringKey: capsule + '.coverArtBy',
-              chronologyKind: 'coverArt',
-            }),
-
-            relations.wallpaperArtistContributionsLine.slots({
-              stringKey: capsule + '.wallpaperArtBy',
-              chronologyKind: 'wallpaperArt',
-            }),
-
-            relations.bannerArtistContributionsLine.slots({
-              stringKey: capsule + '.bannerArtBy',
-              chronologyKind: 'bannerArt',
-            }),
-
             language.$(capsule, 'released', {
               [language.onlyIfOptions]: ['date'],
               date: language.formatDate(data.date),
             }),
 
-            language.$(capsule, 'artReleased', {
-              [language.onlyIfOptions]: ['date'],
-              date: language.formatDate(data.coverArtDate),
-            }),
-
             language.$(capsule, 'duration', {
               [language.onlyIfOptions]: ['duration'],
               duration:
diff --git a/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js b/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js
index 9f9aaf23..22dfa51c 100644
--- a/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js
+++ b/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js
@@ -67,6 +67,8 @@ export default {
 
   generate: (relations, slots) =>
     relations.parentSiblingsPart.slots({
+      attributes: {class: 'group-nav-links'},
+
       showPreviousNext: slots.mode === 'album',
 
       colorStyle: relations.colorStyle,
diff --git a/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js b/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js
index f579cdc9..16f205e3 100644
--- a/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js
+++ b/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js
@@ -62,7 +62,7 @@ export default {
 
   generate: (data, relations, slots, {language}) =>
     relations.parentSiblingsPart.slots({
-      attributes: {class: 'series-nav-link'},
+      attributes: {class: 'series-nav-links'},
 
       showPreviousNext: slots.mode === 'album',
 
diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js
index 88aea409..dae5fa03 100644
--- a/src/content/dependencies/generateAlbumSidebarTrackSection.js
+++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js
@@ -1,4 +1,4 @@
-import {empty} from '#sugar';
+import {empty, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['linkTrack'],
@@ -17,23 +17,25 @@ export default {
   data(album, track, trackSection) {
     const data = {};
 
-    data.hasTrackNumbers = album.hasTrackNumbers;
+    data.hasTrackNumbers =
+      album.hasTrackNumbers &&
+      !empty(trackSection.tracks);
+
     data.isTrackPage = !!track;
 
     data.name = trackSection.name;
     data.color = trackSection.color;
     data.isDefaultTrackSection = trackSection.isDefaultTrackSection;
 
-    data.firstTrackNumber = trackSection.startIndex + 1;
-    data.lastTrackNumber = trackSection.startIndex + trackSection.tracks.length;
+    data.firstTrackNumber =
+      (data.hasTrackNumbers
+        ? trackSection.tracks.at(0).trackNumber
+        : null);
 
-    if (track) {
-      const index = trackSection.tracks.indexOf(track);
-      if (index !== -1) {
-        data.includesCurrentTrack = true;
-        data.currentTrackIndex = index;
-      }
-    }
+    data.lastTrackNumber =
+      (data.hasTrackNumbers
+        ? trackSection.tracks.at(-1).trackNumber
+        : null);
 
     data.trackDirectories =
       trackSection.tracks
@@ -43,6 +45,13 @@ export default {
       trackSection.tracks
         .map(track => empty(track.commentary));
 
+    data.tracksAreCurrentTrack =
+      trackSection.tracks
+        .map(traaaaaaaack => traaaaaaaack === track);
+
+    data.includesCurrentTrack =
+      data.tracksAreCurrentTrack.includes(true);
+
     return data;
   },
 
@@ -72,29 +81,39 @@ export default {
     }
 
     const trackListItems =
-      relations.trackLinks.map((trackLink, index) =>
-        html.tag('li',
-          data.includesCurrentTrack &&
-          index === data.currentTrackIndex &&
-            {class: 'current'},
-
-          slots.mode === 'commentary' &&
-          data.tracksAreMissingCommentary[index] &&
-            {class: 'no-commentary'},
-
-          language.$(capsule, 'item', {
-            track:
-              (slots.mode === 'commentary' && data.tracksAreMissingCommentary[index]
-                ? trackLink.slots({
-                    linkless: true,
-                  })
-             : slots.anchor
-                ? trackLink.slots({
-                    anchor: true,
-                    hash: data.trackDirectories[index],
-                  })
-                : trackLink),
-          })));
+      stitchArrays({
+        trackLink: relations.trackLinks,
+        directory: data.trackDirectories,
+        isCurrentTrack: data.tracksAreCurrentTrack,
+        missingCommentary: data.tracksAreMissingCommentary,
+      }).map(({
+          trackLink,
+          directory,
+          isCurrentTrack,
+          missingCommentary,
+        }) =>
+          html.tag('li',
+            data.includesCurrentTrack &&
+            isCurrentTrack &&
+              {class: 'current'},
+
+            slots.mode === 'commentary' &&
+            missingCommentary &&
+              {class: 'no-commentary'},
+
+            language.$(capsule, 'item', {
+              track:
+                (slots.mode === 'commentary' && missingCommentary
+                  ? trackLink.slots({
+                      linkless: true,
+                    })
+               : slots.anchor
+                  ? trackLink.slots({
+                      anchor: true,
+                      hash: directory,
+                    })
+                  : trackLink),
+            })));
 
     return html.tag('details',
       data.includesCurrentTrack &&
@@ -121,17 +140,22 @@ export default {
           colorStyle,
 
           html.tag('span',
-            language.encapsulate(capsule, 'group', workingCapsule => {
-              const workingOptions = {group: sectionName};
-
-              if (data.hasTrackNumbers) {
-                workingCapsule += '.withRange';
-                workingOptions.range =
-                  `${data.firstTrackNumber}–${data.lastTrackNumber}`;
-              }
-
-              return language.$(workingCapsule, workingOptions);
-            }))),
+            language.encapsulate(capsule, 'group', groupCapsule =>
+              language.encapsulate(groupCapsule, workingCapsule => {
+                const workingOptions = {group: sectionName};
+
+                if (data.hasTrackNumbers) {
+                  workingCapsule += '.withRange';
+                  workingOptions.rangePart =
+                    html.tag('span', {class: 'track-section-range'},
+                      language.$(groupCapsule, 'withRange.rangePart', {
+                        range:
+                          `${data.firstTrackNumber}–${data.lastTrackNumber}`,
+                      }));
+                }
+
+                return language.$(workingCapsule, workingOptions);
+              })))),
 
         (data.hasTrackNumbers
           ? html.tag('ol',
diff --git a/src/content/dependencies/generateAlbumSocialEmbed.js b/src/content/dependencies/generateAlbumSocialEmbed.js
index ad02e180..e28a3fd0 100644
--- a/src/content/dependencies/generateAlbumSocialEmbed.js
+++ b/src/content/dependencies/generateAlbumSocialEmbed.js
@@ -32,8 +32,7 @@ export default {
     data.hasImage = album.hasCoverArt;
 
     if (data.hasImage) {
-      data.coverArtDirectory = album.directory;
-      data.coverArtFileExtension = album.coverArtFileExtension;
+      data.imagePath = album.coverArtworks[0].path;
     }
 
     data.albumName = album.name;
@@ -65,7 +64,7 @@ export default {
 
         imagePath:
           (data.hasImage
-            ? ['media.albumCover', data.coverArtDirectory, data.coverArtFileExtension]
+            ? data.imagePath
             : null),
       })),
 };
diff --git a/src/content/dependencies/generateAlbumTrackList.js b/src/content/dependencies/generateAlbumTrackList.js
index 9743c750..0a949ded 100644
--- a/src/content/dependencies/generateAlbumTrackList.js
+++ b/src/content/dependencies/generateAlbumTrackList.js
@@ -102,11 +102,11 @@ export default {
             .map(section => section.tracks.length > 1);
 
         if (album.hasTrackNumbers) {
-          data.trackSectionStartIndices =
+          data.trackSectionsStartCountingFrom =
             album.trackSections
-              .map(section => section.startIndex);
+              .map(section => section.startCountingFrom);
         } else {
-          data.trackSectionStartIndices =
+          data.trackSectionsStartCountingFrom =
             album.trackSections
               .map(() => null);
         }
@@ -147,7 +147,7 @@ export default {
             name: data.trackSectionNames,
             duration: data.trackSectionDurations,
             durationApproximate: data.trackSectionDurationsApproximate,
-            startIndex: data.trackSectionStartIndices,
+            startCountingFrom: data.trackSectionsStartCountingFrom,
           }).map(({
               heading,
               description,
@@ -156,7 +156,7 @@ export default {
               name,
               duration,
               durationApproximate,
-              startIndex,
+              startCountingFrom,
             }) => [
               language.encapsulate('trackList.section', capsule =>
                 heading.slots({
@@ -190,7 +190,7 @@ export default {
 
                 html.tag(listTag,
                   data.hasTrackNumbers &&
-                    {start: startIndex + 1},
+                    {start: startCountingFrom},
 
                   slotItems(items)),
               ]),
diff --git a/src/content/dependencies/generateArtTagAncestorDescendantMapList.js b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js
index 89150615..80d19b5a 100644
--- a/src/content/dependencies/generateArtTagAncestorDescendantMapList.js
+++ b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js
@@ -33,8 +33,8 @@ export default {
       const artTagsTimesFeaturedTotal =
         artTags.map(artTag =>
           unique([
-            ...artTag.directlyTaggedInThings,
-            ...artTag.indirectlyTaggedInThings,
+            ...artTag.directlyFeaturedInArtworks,
+            ...artTag.indirectlyFeaturedInArtworks,
           ]).length);
 
       const sublists =
diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js
index b633e58f..344e7bda 100644
--- a/src/content/dependencies/generateArtTagGalleryPage.js
+++ b/src/content/dependencies/generateArtTagGalleryPage.js
@@ -1,5 +1,5 @@
-import {sortAlbumsTracksChronologically} from '#sort';
-import {empty, stitchArrays, unique} from '#sugar';
+import {sortArtworksChronologically} from '#sort';
+import {empty, unique} from '#sugar';
 
 export default {
   contentDependencies: [
@@ -11,10 +11,9 @@ export default {
     'generatePageLayout',
     'generateQuickDescription',
     'image',
-    'linkAlbum',
+    'linkAnythingMan',
     'linkArtTagGallery',
     'linkExternal',
-    'linkTrack',
   ],
 
   extraDependencies: ['html', 'language', 'wikiData'],
@@ -26,16 +25,13 @@ export default {
   },
 
   query(sprawl, artTag) {
-    const directThings = artTag.directlyTaggedInThings;
-    const indirectThings = artTag.indirectlyTaggedInThings;
-    const allThings = unique([...directThings, ...indirectThings]);
+    const directArtworks = artTag.directlyFeaturedInArtworks;
+    const indirectArtworks = artTag.indirectlyFeaturedInArtworks;
+    const allArtworks = unique([...directArtworks, ...indirectArtworks]);
 
-    sortAlbumsTracksChronologically(allThings, {
-      getDate: thing => thing.coverArtDate ?? thing.date,
-      latestFirst: true,
-    });
+    sortArtworksChronologically(allArtworks, {latestFirst: true});
 
-    return {directThings, indirectThings, allThings};
+    return {directArtworks, indirectArtworks, allArtworks};
   },
 
   relations(relation, query, sprawl, artTag) {
@@ -81,15 +77,12 @@ export default {
       relation('generateCoverGrid');
 
     relations.links =
-      query.allThings
-        .map(thing =>
-          (thing.album
-            ? relation('linkTrack', thing)
-            : relation('linkAlbum', thing)));
+      query.allArtworks
+        .map(artwork => relation('linkAnythingMan', artwork.thing));
 
     relations.images =
-      query.allThings
-        .map(thing => relation('image', thing.artTags));
+      query.allArtworks
+        .map(artwork => relation('image', artwork));
 
     return relations;
   },
@@ -102,30 +95,22 @@ export default {
     data.name = artTag.name;
     data.color = artTag.color;
 
-    data.numArtworksIndirectly = query.indirectThings.length;
-    data.numArtworksDirectly = query.directThings.length;
-    data.numArtworksTotal = query.allThings.length;
+    data.numArtworksIndirectly = query.indirectArtworks.length;
+    data.numArtworksDirectly = query.directArtworks.length;
+    data.numArtworksTotal = query.allArtworks.length;
 
     data.names =
-      query.allThings.map(thing => thing.name);
-
-    data.paths =
-      query.allThings.map(thing =>
-        (thing.album
-          ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension]
-          : ['media.albumCover', thing.directory, thing.coverArtFileExtension]));
-
-    data.dimensions =
-      query.allThings.map(thing => thing.coverArtDimensions);
+      query.allArtworks
+        .map(artwork => artwork.thing.name);
 
     data.coverArtists =
-      query.allThings.map(thing =>
-        thing.coverArtistContribs
-          .map(({artist}) => artist.name));
+      query.allArtworks
+        .map(artwork => artwork.artistContribs
+          .map(contrib => contrib.artist.name));
 
     data.onlyFeaturedIndirectly =
-      query.allThings.map(thing =>
-        !query.directThings.includes(thing));
+      query.allArtworks.map(artwork =>
+        !query.directArtworks.includes(artwork));
 
     data.hasMixedDirectIndirect =
       data.onlyFeaturedIndirectly.includes(true) &&
@@ -210,6 +195,7 @@ export default {
           relations.coverGrid
             .slots({
               links: relations.links,
+              images: relations.images,
               names: data.names,
               lazy: 12,
 
@@ -217,17 +203,6 @@ export default {
                 data.onlyFeaturedIndirectly.map(onlyFeaturedIndirectly =>
                   (onlyFeaturedIndirectly ? 'featured-indirectly' : '')),
 
-              images:
-                stitchArrays({
-                  image: relations.images,
-                  path: data.paths,
-                  dimensions: data.dimensions,
-                }).map(({image, path, dimensions}) =>
-                    image.slots({
-                      path,
-                      dimensions,
-                    })),
-
               info:
                 data.coverArtists.map(names =>
                   (names === null
diff --git a/src/content/dependencies/generateArtTagInfoPage.js b/src/content/dependencies/generateArtTagInfoPage.js
index 7765f159..9df51b77 100644
--- a/src/content/dependencies/generateArtTagInfoPage.js
+++ b/src/content/dependencies/generateArtTagInfoPage.js
@@ -23,10 +23,10 @@ export default {
     const query = {};
 
     query.directThings =
-      artTag.directlyTaggedInThings;
+      artTag.directlyFeaturedInArtworks;
 
     query.indirectThings =
-      artTag.indirectlyTaggedInThings;
+      artTag.indirectlyFeaturedInArtworks;
 
     query.allThings =
       unique([...query.directThings, ...query.indirectThings]);
@@ -111,8 +111,8 @@ export default {
     directDescendantTimesFeaturedTotal:
       artTag.directDescendantArtTags.map(artTag =>
         unique([
-          ...artTag.directlyTaggedInThings,
-          ...artTag.indirectlyTaggedInThings,
+          ...artTag.directlyFeaturedInArtworks,
+          ...artTag.indirectlyFeaturedInArtworks,
         ]).length),
   }),
 
diff --git a/src/content/dependencies/generateArtTagSidebar.js b/src/content/dependencies/generateArtTagSidebar.js
index c281b93d..9e2f813c 100644
--- a/src/content/dependencies/generateArtTagSidebar.js
+++ b/src/content/dependencies/generateArtTagSidebar.js
@@ -54,8 +54,8 @@ export default {
     directDescendantTimesFeaturedTotal:
       artTag.directDescendantArtTags.map(artTag =>
         unique([
-          ...artTag.directlyTaggedInThings,
-          ...artTag.indirectlyTaggedInThings,
+          ...artTag.directlyFeaturedInArtworks,
+          ...artTag.indirectlyFeaturedInArtworks,
         ]).length),
 
     furthestAncestorArtTagNames:
diff --git a/src/content/dependencies/generateArtistArtworkColumn.js b/src/content/dependencies/generateArtistArtworkColumn.js
new file mode 100644
index 00000000..a4135489
--- /dev/null
+++ b/src/content/dependencies/generateArtistArtworkColumn.js
@@ -0,0 +1,13 @@
+export default {
+  contentDependencies: ['generateCoverArtwork'],
+
+  relations: (relation, artist) => ({
+    coverArtwork:
+      (artist.hasAvatar
+        ? relation('generateCoverArtwork', artist.avatarArtwork)
+        : null),
+  }),
+
+  generate: (relations) =>
+    relations.coverArtwork,
+};
diff --git a/src/content/dependencies/generateArtistCredit.js b/src/content/dependencies/generateArtistCredit.js
index 72d55854..6bdbeb23 100644
--- a/src/content/dependencies/generateArtistCredit.js
+++ b/src/content/dependencies/generateArtistCredit.js
@@ -80,6 +80,8 @@ export default {
     // It won't be used if contextContributions isn't provided.
     featuringStringKey: {type: 'string'},
 
+    additionalStringOptions: {validate: v => v.isObject},
+
     showAnnotation: {type: 'boolean', default: false},
     showExternalLinks: {type: 'boolean', default: false},
     showChronology: {type: 'boolean', default: false},
@@ -148,7 +150,10 @@ export default {
 
     if (empty(relations.featuringContributionLinks)) {
       if (data.normalContributionsDifferFromContext) {
-        return language.$(slots.normalStringKey, {artists: artistsList});
+        return language.$(slots.normalStringKey, {
+          ...slots.additionalStringOptions,
+          artists: artistsList,
+        });
       } else {
         return html.blank();
       }
@@ -156,13 +161,20 @@ export default {
 
     if (data.normalContributionsDifferFromContext && slots.normalFeaturingStringKey) {
       return language.$(slots.normalFeaturingStringKey, {
+        ...slots.additionalStringOptions,
         artists: artistsList,
         featuring: featuringList,
       });
     } else if (slots.featuringStringKey) {
-      return language.$(slots.featuringStringKey, {artists: featuringList});
+      return language.$(slots.featuringStringKey, {
+        ...slots.additionalStringOptions,
+        artists: featuringList,
+      });
     } else {
-      return language.$(slots.normalStringKey, {artists: everyoneList});
+      return language.$(slots.normalStringKey, {
+        ...slots.additionalStringOptions,
+        artists: everyoneList,
+      });
     }
   },
 };
diff --git a/src/content/dependencies/generateArtistGalleryPage.js b/src/content/dependencies/generateArtistGalleryPage.js
index 7a76188a..6a24275e 100644
--- a/src/content/dependencies/generateArtistGalleryPage.js
+++ b/src/content/dependencies/generateArtistGalleryPage.js
@@ -1,5 +1,4 @@
-import {sortAlbumsTracksChronologically} from '#sort';
-import {stitchArrays} from '#sugar';
+import {sortArtworksChronologically} from '#sort';
 
 export default {
   contentDependencies: [
@@ -7,83 +6,59 @@ export default {
     'generateCoverGrid',
     'generatePageLayout',
     'image',
-    'linkAlbum',
-    'linkTrack',
+    'linkAnythingMan',
   ],
 
   extraDependencies: ['html', 'language'],
 
-  query(artist) {
-    const things =
-      ([
-        artist.albumCoverArtistContributions,
-        artist.trackCoverArtistContributions,
-      ]).flat()
-        .filter(({annotation}) => !annotation?.startsWith(`edits for wiki`))
-        .map(({thing}) => thing);
-
-    sortAlbumsTracksChronologically(things, {
-      latestFirst: true,
-      getDate: thing => thing.coverArtDate ?? thing.date,
-    });
-
-    return {things};
-  },
-
-  relations(relation, query, artist) {
-    const relations = {};
-
-    relations.layout =
-      relation('generatePageLayout');
-
-    relations.artistNavLinks =
-      relation('generateArtistNavLinks', artist);
-
-    relations.coverGrid =
-      relation('generateCoverGrid');
-
-    relations.links =
-      query.things.map(thing =>
-        (thing.album
-          ? relation('linkTrack', thing)
-          : relation('linkAlbum', thing)));
-
-    relations.images =
-      query.things.map(thing =>
-        relation('image', thing.artTags));
-
-    return relations;
-  },
-
-  data(query, artist) {
-    const data = {};
-
-    data.name = artist.name;
-
-    data.numArtworks = query.things.length;
-
-    data.names =
-      query.things.map(thing => thing.name);
-
-    data.paths =
-      query.things.map(thing =>
-        (thing.album
-          ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension]
-          : ['media.albumCover', thing.directory, thing.coverArtFileExtension]));
-
-    data.dimensions =
-      query.things.map(thing => thing.coverArtDimensions);
-
-    data.otherCoverArtists =
-      query.things.map(thing =>
-        (thing.coverArtistContribs.length > 1
-          ? thing.coverArtistContribs
-              .filter(({artist: otherArtist}) => otherArtist !== artist)
-              .map(({artist: otherArtist}) => otherArtist.name)
-          : null));
-
-    return data;
-  },
+  query: (artist) => ({
+    artworks:
+      sortArtworksChronologically(
+        ([
+          artist.albumCoverArtistContributions,
+          artist.trackCoverArtistContributions,
+        ]).flat()
+          .filter(contrib => !contrib.annotation?.startsWith(`edits for wiki`))
+          .map(contrib => contrib.thing),
+        {latestFirst: true}),
+  }),
+
+  relations: (relation, query, artist) => ({
+    layout:
+      relation('generatePageLayout'),
+
+    artistNavLinks:
+      relation('generateArtistNavLinks', artist),
+
+    coverGrid:
+      relation('generateCoverGrid'),
+
+    links:
+      query.artworks
+        .map(artwork => relation('linkAnythingMan', artwork.thing)),
+
+    images:
+      query.artworks
+        .map(artwork => relation('image', artwork)),
+  }),
+
+  data: (query, artist) => ({
+    name:
+      artist.name,
+
+    numArtworks:
+      query.artworks.length,
+
+    names:
+      query.artworks
+        .map(artwork => artwork.thing.name),
+
+    otherCoverArtists:
+      query.artworks
+        .map(artwork => artwork.artistContribs
+          .filter(contrib => contrib.artist !== artist)
+          .map(contrib => contrib.artist.name)),
+  }),
 
   generate: (data, relations, {html, language}) =>
     language.encapsulate('artistGalleryPage', pageCapsule =>
@@ -100,7 +75,7 @@ export default {
           html.tag('p', {class: 'quick-info'},
             language.$(pageCapsule, 'infoLine', {
               coverArts:
-                language.countCoverArts(data.numArtworks, {
+                language.countArtworks(data.numArtworks, {
                   unit: true,
                 }),
             })),
@@ -108,27 +83,16 @@ export default {
           relations.coverGrid
             .slots({
               links: relations.links,
+              images: relations.images,
               names: data.names,
 
-              images:
-                stitchArrays({
-                  image: relations.images,
-                  path: data.paths,
-                  dimensions: data.dimensions,
-                }).map(({image, path, dimensions}) =>
-                    image.slots({
-                      path,
-                      dimensions,
-                    })),
-
-              // TODO: Can this be [language.onlyIfOptions]?
               info:
                 data.otherCoverArtists.map(names =>
-                  (names === null
-                    ? null
-                    : language.$('misc.coverGrid.details.otherCoverArtists', {
-                        artists: language.formatUnitList(names),
-                      }))),
+                  language.$('misc.coverGrid.details.otherCoverArtists', {
+                    [language.onlyIfOptions]: ['artists'],
+
+                    artists: language.formatUnitList(names),
+                  })),
             }),
         ],
 
diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js
index 0c4e4189..3a3cf8b7 100644
--- a/src/content/dependencies/generateArtistInfoPage.js
+++ b/src/content/dependencies/generateArtistInfoPage.js
@@ -2,6 +2,7 @@ import {empty, stitchArrays, unique} from '#sugar';
 
 export default {
   contentDependencies: [
+    'generateArtistArtworkColumn',
     'generateArtistGroupContributionsInfo',
     'generateArtistInfoPageArtworksChunkedList',
     'generateArtistInfoPageCommentaryChunkedList',
@@ -9,9 +10,7 @@ export default {
     'generateArtistInfoPageTracksChunkedList',
     'generateArtistNavLinks',
     'generateContentHeading',
-    'generateCoverArtwork',
     'generatePageLayout',
-    'image',
     'linkArtistGallery',
     'linkExternal',
     'linkGroup',
@@ -35,7 +34,7 @@ export default {
     // Artworks are different, though. We intentionally duplicate album data
     // objects when the artist has contributed some combination of cover art,
     // wallpaper, and banner - these each count as a unique contribution.
-    allArtworks:
+    allArtworkThings:
       ([
         artist.albumCoverArtistContributions,
         artist.albumWallpaperArtistContributions,
@@ -43,7 +42,7 @@ export default {
         artist.trackCoverArtistContributions,
       ]).flat()
         .filter(({annotation}) => !annotation?.startsWith('edits for wiki'))
-        .map(({thing}) => thing),
+        .map(({thing}) => thing.thing),
 
     // Banners and wallpapers don't show up in the artist gallery page, only
     // cover art.
@@ -69,15 +68,8 @@ export default {
     artistNavLinks:
       relation('generateArtistNavLinks', artist),
 
-    cover:
-      (artist.hasAvatar
-        ? relation('generateCoverArtwork', [], [])
-        : null),
-
-    image:
-      (artist.hasAvatar
-        ? relation('image')
-        : null),
+    artworkColumn:
+      relation('generateArtistArtworkColumn', artist),
 
     contentHeading:
       relation('generateContentHeading'),
@@ -110,7 +102,7 @@ export default {
       relation('generateArtistInfoPageArtworksChunkedList', artist, true),
 
     artworksGroupInfo:
-      relation('generateArtistGroupContributionsInfo', query.allArtworks),
+      relation('generateArtistGroupContributionsInfo', query.allArtworkThings),
 
     artistGalleryLink:
       (query.hasGallery
@@ -131,14 +123,6 @@ export default {
     name:
       artist.name,
 
-    directory:
-      artist.directory,
-
-    avatarFileExtension:
-      (artist.hasAvatar
-        ? artist.avatarFileExtension
-        : null),
-
     closeGroupAnnotations:
       query.generalLinkedGroups
         .map(({annotation}) => annotation),
@@ -156,19 +140,8 @@ export default {
         title: data.name,
         headingMode: 'sticky',
 
-        cover:
-          (relations.cover
-            ? relations.cover.slots({
-                image:
-                  relations.image.slots({
-                    path: [
-                      'media.artistAvatar',
-                      data.directory,
-                      data.avatarFileExtension,
-                    ],
-                  }),
-              })
-            : null),
+        artworkColumnContent:
+          relations.artworkColumn,
 
         mainContent: [
           html.tags([
diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
index 089cfb8d..2f2fe0c5 100644
--- a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
@@ -24,7 +24,7 @@ export default {
 
     trackLink:
       (query.kind === 'track-cover'
-        ? relation('linkTrack', contrib.thing)
+        ? relation('linkTrack', contrib.thing.thing)
         : null),
 
     otherArtistLinks:
diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
index 8b024147..75a4aa5a 100644
--- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
@@ -27,20 +27,21 @@ export default {
 
     sortContributionsChronologically(
       filteredContributions,
-      sortAlbumsTracksChronologically);
+      sortAlbumsTracksChronologically,
+      {getThing: contrib => contrib.thing.thing});
 
     query.contribs =
       chunkByConditions(filteredContributions, [
         ({date: date1}, {date: date2}) =>
           +date1 !== +date2,
-        ({thing: thing1}, {thing: thing2}) =>
+        ({thing: {thing: thing1}}, {thing: {thing: thing2}}) =>
           (thing1.album ?? thing1) !==
           (thing2.album ?? thing2),
       ]);
 
     query.albums =
       query.contribs
-        .map(contribs => contribs[0].thing)
+        .map(contribs => contribs[0].thing.thing)
         .map(thing => thing.album ?? thing);
 
     return query;
diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js
index c93020f3..4cb618e3 100644
--- a/src/content/dependencies/generateCommentaryEntry.js
+++ b/src/content/dependencies/generateCommentaryEntry.js
@@ -98,8 +98,6 @@ export default {
 
                 return language.$(workingCapsule, workingOptions);
               })),
-
-            relations.date,
           ])),
 
         html.tag('blockquote', {class: 'commentary-entry-body'},
@@ -107,6 +105,10 @@ export default {
             relations.colorStyle.clone()
               .slot('color', slots.color),
 
-          relations.bodyContent.slot('mode', 'multiline')),
+          [
+            relations.date,
+
+            relations.bodyContent.slot('mode', 'multiline'),
+          ]),
       ])),
 };
diff --git a/src/content/dependencies/generateContributionTooltipChronologySection.js b/src/content/dependencies/generateContributionTooltipChronologySection.js
index 78c9051c..378c0e1c 100644
--- a/src/content/dependencies/generateContributionTooltipChronologySection.js
+++ b/src/content/dependencies/generateContributionTooltipChronologySection.js
@@ -1,3 +1,19 @@
+import Thing from '#thing';
+
+function getName(thing) {
+  if (!thing) {
+    return null;
+  }
+
+  const referenceType = thing.constructor[Thing.referenceType];
+
+  if (referenceType === 'artwork') {
+    return thing.thing.name;
+  }
+
+  return thing.name;
+}
+
 export default {
   contentDependencies: ['linkAnythingMan'],
   extraDependencies: ['html', 'language'],
@@ -30,14 +46,10 @@ export default {
 
   data: (query, _contribution) => ({
     previousName:
-      (query.previous
-        ? query.previous.thing.name
-        : null),
+      getName(query.previous?.thing),
 
     nextName:
-      (query.next
-        ? query.next.thing.name
-        : null),
+      getName(query.next?.thing),
   }),
 
   slots: {
diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js
index 06972d6b..3a10ab20 100644
--- a/src/content/dependencies/generateCoverArtwork.js
+++ b/src/content/dependencies/generateCoverArtwork.js
@@ -1,11 +1,44 @@
 export default {
-  contentDependencies: ['image'],
+  contentDependencies: [
+    'generateCoverArtworkArtTagDetails',
+    'generateCoverArtworkArtistDetails',
+    'generateCoverArtworkOriginDetails',
+    'generateCoverArtworkReferenceDetails',
+    'image',
+  ],
+
   extraDependencies: ['html'],
 
+  relations: (relation, artwork) => ({
+    image:
+      relation('image', artwork),
+
+    originDetails:
+      relation('generateCoverArtworkOriginDetails', artwork),
+
+    artTagDetails:
+      relation('generateCoverArtworkArtTagDetails', artwork),
+
+    artistDetails:
+      relation('generateCoverArtworkArtistDetails', artwork),
+
+    referenceDetails:
+      relation('generateCoverArtworkReferenceDetails', artwork),
+  }),
+
+  data: (artwork) => ({
+    color:
+      artwork.thing.color ?? null,
+
+    dimensions:
+      artwork.dimensions,
+  }),
+
   slots: {
-    image: {
-      type: 'html',
-      mutable: true,
+    alt: {type: 'string'},
+
+    color: {
+      validate: v => v.isColor,
     },
 
     mode: {
@@ -13,13 +46,10 @@ export default {
       default: 'primary',
     },
 
-    dimensions: {
-      validate: v => v.isDimensions,
-    },
-
-    warnings: {
-      validate: v => v.looseArrayOf(v.isString),
-    },
+    showOriginDetails: {type: 'boolean', default: false},
+    showArtTagDetails: {type: 'boolean', default: false},
+    showArtistDetails: {type: 'boolean', default: false},
+    showReferenceDetails: {type: 'boolean', default: false},
 
     details: {
       type: 'html',
@@ -27,60 +57,65 @@ export default {
     },
   },
 
-  generate(slots, {html}) {
+  generate(data, relations, slots, {html}) {
+    const {image} = relations;
+
+    image.setSlots({
+      color: slots.color ?? data.color,
+      alt: slots.alt,
+    });
+
     const square =
-      (slots.dimensions
-        ? slots.dimensions[0] === slots.dimensions[1]
+      (data.dimensions
+        ? data.dimensions[0] === data.dimensions[1]
         : true);
 
-    const sizeSlots =
-      (square
-        ? {square: true}
-        : {dimensions: slots.dimensions});
-
-    switch (slots.mode) {
-      case 'primary':
-        return html.tags([
-          slots.image.slots({
-            thumb: 'medium',
-            reveal: true,
-            link: true,
-
-            warnings: slots.warnings,
-            ...sizeSlots,
-          }),
-
-          slots.details,
-        ]);
-
-      case 'thumbnail':
-        return (
-          slots.image.slots({
-            thumb: 'small',
-            reveal: false,
-            link: false,
-
-            warnings: slots.warnings,
-            ...sizeSlots,
-          }));
-
-      case 'commentary':
-        return (
-          slots.image.slots({
-            thumb: 'medium',
-            reveal: true,
-            link: true,
-            lazy: true,
-
-            warnings: slots.warnings,
-            ...sizeSlots,
-
-            attributes:
-              {class: 'commentary-art'},
-          }));
-
-      default:
-        return html.blank();
+    if (square) {
+      image.setSlot('square', true);
+    } else {
+      image.setSlot('dimensions', data.dimensions);
     }
+
+    return (
+      html.tag('div', {class: 'cover-artwork'},
+        slots.mode === 'commentary' &&
+          {class: 'commentary-art'},
+
+        (slots.mode === 'primary'
+          ? [
+              relations.image.slots({
+                thumb: 'medium',
+                reveal: true,
+                link: true,
+              }),
+
+              slots.showOriginDetails &&
+                relations.originDetails,
+
+              slots.showArtTagDetails &&
+                relations.artTagDetails,
+
+              slots.showArtistDetails &&
+                relations.artistDetails,
+
+              slots.showReferenceDetails &&
+                relations.referenceDetails,
+
+              slots.details,
+            ]
+       : slots.mode === 'thumbnail'
+          ? relations.image.slots({
+              thumb: 'small',
+              reveal: false,
+              link: false,
+            })
+       : slots.mode === 'commentary'
+          ? relations.image.slots({
+              thumb: 'medium',
+              reveal: true,
+              link: true,
+              lazy: true,
+            })
+          : html.blank())));
   },
 };
diff --git a/src/content/dependencies/generateCoverArtworkArtTagDetails.js b/src/content/dependencies/generateCoverArtworkArtTagDetails.js
index b4edbbdd..b20f599b 100644
--- a/src/content/dependencies/generateCoverArtworkArtTagDetails.js
+++ b/src/content/dependencies/generateCoverArtworkArtTagDetails.js
@@ -4,19 +4,19 @@ export default {
   contentDependencies: ['linkArtTagGallery'],
   extraDependencies: ['html'],
 
-  query: (artTags) => ({
+  query: (artwork) => ({
     linkableArtTags:
-      artTags
+      artwork.artTags
         .filter(tag => !tag.isContentWarning),
   }),
 
-  relations: (relation, query, _artTags) => ({
+  relations: (relation, query, _artwork) => ({
     artTagLinks:
       query.linkableArtTags
         .map(tag => relation('linkArtTagGallery', tag)),
   }),
 
-  data: (query, _artTags) => {
+  data: (query, _artwork) => {
     const seenShortNames = new Set();
     const duplicateShortNames = new Set();
 
diff --git a/src/content/dependencies/generateCoverArtworkArtistDetails.js b/src/content/dependencies/generateCoverArtworkArtistDetails.js
index 5b235353..3ead80ab 100644
--- a/src/content/dependencies/generateCoverArtworkArtistDetails.js
+++ b/src/content/dependencies/generateCoverArtworkArtistDetails.js
@@ -2,9 +2,9 @@ export default {
   contentDependencies: ['linkArtistGallery'],
   extraDependencies: ['html', 'language'],
 
-  relations: (relation, contributions) => ({
+  relations: (relation, artwork) => ({
     artistLinks:
-      contributions
+      artwork.artistContribs
         .map(contrib => contrib.artist)
         .map(artist =>
           relation('linkArtistGallery', artist)),
@@ -17,6 +17,8 @@ export default {
       {class: 'illustrator-details'},
 
       language.$('misc.coverGrid.details.coverArtists', {
+        [language.onlyIfOptions]: ['artists'],
+
         artists:
           language.formatConjunctionList(relations.artistLinks),
       })),
diff --git a/src/content/dependencies/generateCoverArtworkOriginDetails.js b/src/content/dependencies/generateCoverArtworkOriginDetails.js
new file mode 100644
index 00000000..08a01cfe
--- /dev/null
+++ b/src/content/dependencies/generateCoverArtworkOriginDetails.js
@@ -0,0 +1,98 @@
+import Thing from '#thing';
+
+export default {
+  contentDependencies: [
+    'generateArtistCredit',
+    'generateAbsoluteDatetimestamp',
+    'linkAlbum',
+    'transformContent',
+  ],
+
+  extraDependencies: ['html', 'language', 'pagePath'],
+
+  query: (artwork) => ({
+    artworkThingType:
+      artwork.thing.constructor[Thing.referenceType],
+  }),
+
+  relations: (relation, query, artwork) => ({
+    credit:
+      relation('generateArtistCredit', artwork.artistContribs, []),
+
+    source:
+      relation('transformContent', artwork.source),
+
+    albumLink:
+      (query.artworkThingType === 'album'
+        ? relation('linkAlbum', artwork.thing)
+        : null),
+
+    datetimestamp:
+      (artwork.date && artwork.date !== artwork.thing.date
+        ? relation('generateAbsoluteDatetimestamp', artwork.date)
+        : null),
+  }),
+
+
+  data: (query, artwork) => ({
+    label:
+      artwork.label,
+
+    artworkThingType:
+      query.artworkThingType,
+  }),
+
+  generate: (data, relations, {html, language, pagePath}) =>
+    language.encapsulate('misc.coverArtwork', capsule =>
+      html.tag('p', {class: 'image-details'},
+        {[html.onlyIfContent]: true},
+        {[html.joinChildren]: html.tag('br')},
+
+        {class: 'origin-details'},
+
+        [
+          language.encapsulate(capsule, 'artworkBy', workingCapsule => {
+            const workingOptions = {};
+
+            if (data.label) {
+              workingCapsule += '.customLabel';
+              workingOptions.label = data.label;
+            }
+
+            if (relations.datetimestamp) {
+              workingCapsule += '.withYear';
+              workingOptions.year =
+                relations.datetimestamp.slots({
+                  style: 'year',
+                  tooltip: true,
+                });
+            }
+
+            return relations.credit.slots({
+              showAnnotation: true,
+              showExternalLinks: true,
+              showChronology: true,
+              showWikiEdits: true,
+
+              trimAnnotation: false,
+
+              chronologyKind: 'coverArt',
+
+              normalStringKey: workingCapsule,
+              additionalStringOptions: workingOptions,
+            });
+          }),
+
+          pagePath[0] === 'track' &&
+          data.artworkThingType === 'album' &&
+            language.$(capsule, 'trackArtFromAlbum', {
+              album:
+                relations.albumLink.slot('color', false),
+            }),
+
+          language.$(capsule, 'source', {
+            [language.onlyIfOptions]: ['source'],
+            source: relations.source.slot('mode', 'inline'),
+          }),
+        ])),
+};
diff --git a/src/content/dependencies/generateCoverArtworkReferenceDetails.js b/src/content/dependencies/generateCoverArtworkReferenceDetails.js
index 006b2b4b..035ab586 100644
--- a/src/content/dependencies/generateCoverArtworkReferenceDetails.js
+++ b/src/content/dependencies/generateCoverArtworkReferenceDetails.js
@@ -1,20 +1,24 @@
 export default {
+  contentDependencies: ['linkReferencedArtworks', 'linkReferencingArtworks'],
   extraDependencies: ['html', 'language'],
 
-  data: (referenced, referencedBy) => ({
+  relations: (relation, artwork) => ({
+    referencedArtworksLink:
+      relation('linkReferencedArtworks', artwork),
+
+    referencingArtworksLink:
+      relation('linkReferencingArtworks', artwork),
+  }),
+
+  data: (artwork) => ({
     referenced:
-      referenced.length,
+      artwork.referencedArtworks.length,
 
     referencedBy:
-      referencedBy.length,
+      artwork.referencedByArtworks.length,
   }),
 
-  slots: {
-    referencedLink: {type: 'html', mutable: true},
-    referencingLink: {type: 'html', mutable: true},
-  },
-
-  generate: (data, slots, {html, language}) =>
+  generate: (data, relations, {html, language}) =>
     language.encapsulate('releaseInfo', capsule => {
       const referencedText =
         language.$(capsule, 'referencesArtworks', {
@@ -47,10 +51,10 @@ export default {
 
           [
             !html.isBlank(referencedText) &&
-              slots.referencedLink.slot('content', referencedText),
+              relations.referencedArtworksLink.slot('content', referencedText),
 
             !html.isBlank(referencingText) &&
-              slots.referencingLink.slot('content', referencingText),
+              relations.referencingArtworksLink.slot('content', referencingText),
           ]));
     }),
 }
diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js
index 1898832f..29ac08b7 100644
--- a/src/content/dependencies/generateCoverGrid.js
+++ b/src/content/dependencies/generateCoverGrid.js
@@ -33,9 +33,11 @@ export default {
     actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)},
   },
 
-  generate(relations, slots, {html, language}) {
-    return (
-      html.tag('div', {class: 'grid-listing'}, [
+  generate: (relations, slots, {html, language}) =>
+    html.tag('div', {class: 'grid-listing'},
+      {[html.onlyIfContent]: true},
+
+      [
         stitchArrays({
           classes: slots.classes,
           image: slots.images,
@@ -84,6 +86,5 @@ export default {
 
         relations.actionLinks
           .slot('actionLinks', slots.actionLinks),
-      ]));
-  },
+      ]),
 };
diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js
index 8f174b21..84ab549d 100644
--- a/src/content/dependencies/generateFlashActGalleryPage.js
+++ b/src/content/dependencies/generateFlashActGalleryPage.js
@@ -1,5 +1,3 @@
-import {stitchArrays} from '#sugar';
-
 import striptags from 'striptags';
 
 export default {
@@ -37,7 +35,7 @@ export default {
 
     coverGridImages:
       act.flashes
-        .map(_flash => relation('image')),
+        .map(flash => relation('image', flash.coverArtwork)),
 
     flashLinks:
       act.flashes
@@ -50,10 +48,6 @@ export default {
 
     flashNames:
       act.flashes.map(flash => flash.name),
-
-    flashCoverPaths:
-      act.flashes.map(flash =>
-        ['media.flashArt', flash.directory, flash.coverArtFileExtension])
   }),
 
   generate: (data, relations, {language}) =>
@@ -71,15 +65,9 @@ export default {
         mainContent: [
           relations.coverGrid.slots({
             links: relations.flashLinks,
+            images: relations.coverGridImages,
             names: data.flashNames,
             lazy: 6,
-
-            images:
-              stitchArrays({
-                image: relations.coverGridImages,
-                path: data.flashCoverPaths,
-              }).map(({image, path}) =>
-                  image.slot('path', path)),
           }),
         ],
 
diff --git a/src/content/dependencies/generateFlashArtworkColumn.js b/src/content/dependencies/generateFlashArtworkColumn.js
new file mode 100644
index 00000000..5987df9e
--- /dev/null
+++ b/src/content/dependencies/generateFlashArtworkColumn.js
@@ -0,0 +1,11 @@
+export default {
+  contentDependencies: ['generateCoverArtwork'],
+
+  relations: (relation, flash) => ({
+    coverArtwork:
+      relation('generateCoverArtwork', flash.coverArtwork),
+  }),
+
+  generate: (relations) =>
+    relations.coverArtwork,
+};
diff --git a/src/content/dependencies/generateFlashCoverArtwork.js b/src/content/dependencies/generateFlashCoverArtwork.js
deleted file mode 100644
index 4b0e5242..00000000
--- a/src/content/dependencies/generateFlashCoverArtwork.js
+++ /dev/null
@@ -1,41 +0,0 @@
-export default {
-  contentDependencies: ['generateCoverArtwork', 'image'],
-  extraDependencies: ['html', 'language'],
-
-  relations: (relation) => ({
-    coverArtwork:
-      relation('generateCoverArtwork'),
-
-    image:
-      relation('image'),
-  }),
-
-  data: (flash) => ({
-    path:
-      ['media.flashArt', flash.directory, flash.coverArtFileExtension],
-
-    color:
-      flash.color,
-
-    dimensions:
-      flash.coverArtDimensions,
-  }),
-
-  slots: {
-    mode: {type: 'string'},
-  },
-
-  generate: (data, relations, slots, {language}) =>
-    relations.coverArtwork.slots({
-      mode: slots.mode,
-
-      image:
-        relations.image.slots({
-          path: data.path,
-          color: data.color,
-          alt: language.$('misc.alt.flashArt'),
-        }),
-
-      dimensions: data.dimensions,
-    }),
-};
diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js
index a21bb49e..2788406c 100644
--- a/src/content/dependencies/generateFlashIndexPage.js
+++ b/src/content/dependencies/generateFlashIndexPage.js
@@ -53,7 +53,7 @@ export default {
     actCoverGridImages:
       query.flashActs
         .map(act => act.flashes
-          .map(() => relation('image'))),
+          .map(flash => relation('image', flash.coverArtwork))),
   }),
 
   data: (query) => ({
@@ -73,11 +73,6 @@ export default {
       query.flashActs
         .map(act => act.flashes
           .map(flash => flash.name)),
-
-    actCoverGridPaths:
-      query.flashActs
-        .map(act => act.flashes
-          .map(flash => ['media.flashArt', flash.directory, flash.coverArtFileExtension])),
   }),
 
   generate: (data, relations, {html, language}) =>
@@ -116,7 +111,6 @@ export default {
             coverGridImages: relations.actCoverGridImages,
             coverGridLinks: relations.actCoverGridLinks,
             coverGridNames: data.actCoverGridNames,
-            coverGridPaths: data.actCoverGridPaths,
           }).map(({
               colorStyle,
               actLink,
@@ -126,7 +120,6 @@ export default {
               coverGridImages,
               coverGridLinks,
               coverGridNames,
-              coverGridPaths,
             }, index) => [
               html.tag('h2',
                 {id: anchor},
@@ -135,15 +128,9 @@ export default {
 
               coverGrid.slots({
                 links: coverGridLinks,
+                images: coverGridImages,
                 names: coverGridNames,
                 lazy: index === 0 ? 4 : true,
-
-                images:
-                  stitchArrays({
-                    image: coverGridImages,
-                    path: coverGridPaths,
-                  }).map(({image, path}) =>
-                      image.slot('path', path)),
               }),
             ]),
         ],
diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js
index 350a0fc5..095e43c4 100644
--- a/src/content/dependencies/generateFlashInfoPage.js
+++ b/src/content/dependencies/generateFlashInfoPage.js
@@ -7,7 +7,7 @@ export default {
     'generateContentHeading',
     'generateContributionList',
     'generateFlashActSidebar',
-    'generateFlashCoverArtwork',
+    'generateFlashArtworkColumn',
     'generateFlashNavAccent',
     'generatePageLayout',
     'generateTrackList',
@@ -47,8 +47,8 @@ export default {
       query.urls
         .map(url => relation('linkExternal', url)),
 
-    cover:
-      relation('generateFlashCoverArtwork', flash),
+    artworkColumn:
+      relation('generateFlashArtworkColumn', flash),
 
     contentHeading:
       relation('generateContentHeading'),
@@ -98,7 +98,7 @@ export default {
 
         additionalNames: relations.additionalNamesBox,
 
-        cover: relations.cover,
+        artworkColumnContent: relations.artworkColumn,
 
         mainContent: [
           html.tag('p',
diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js
index 206c495d..d51366ca 100644
--- a/src/content/dependencies/generateGroupGalleryPage.js
+++ b/src/content/dependencies/generateGroupGalleryPage.js
@@ -53,7 +53,7 @@ export default {
 
       relations.carouselImages =
         carouselAlbums
-          .map(album => relation('image', album.artTags));
+          .map(album => relation('image', album.coverArtworks[0]));
     }
 
     relations.quickDescription =
@@ -69,7 +69,7 @@ export default {
     relations.gridImages =
       albums.map(album =>
         (album.hasCoverArt
-          ? relation('image', album.artTags)
+          ? relation('image', album.coverArtworks[0])
           : relation('image')));
 
     return relations;
@@ -92,22 +92,6 @@ export default {
     data.gridDurations = albums.map(album => getTotalDuration(album.tracks));
     data.gridNumTracks = albums.map(album => album.tracks.length);
 
-    data.gridPaths =
-      albums.map(album =>
-        (album.hasCoverArt
-          ? ['media.albumCover', album.directory, album.coverArtFileExtension]
-          : null));
-
-    const carouselAlbums = filterItemsForCarousel(group.featuredAlbums);
-
-    if (!empty(group.featuredAlbums)) {
-      data.carouselPaths =
-        carouselAlbums.map(album =>
-          (album.hasCoverArt
-            ? ['media.albumCover', album.directory, album.coverArtFileExtension]
-            : null));
-    }
-
     return data;
   },
 
@@ -124,12 +108,7 @@ export default {
           relations.coverCarousel
             ?.slots({
               links: relations.carouselLinks,
-              images:
-                stitchArrays({
-                  image: relations.carouselImages,
-                  path: data.carouselPaths,
-                }).map(({image, path}) =>
-                    image.slot('path', path)),
+              images: relations.carouselImages,
             }),
 
           relations.quickDescription,
@@ -159,19 +138,19 @@ export default {
             .slots({
               links: relations.gridLinks,
               names: data.gridNames,
+
               images:
                 stitchArrays({
                   image: relations.gridImages,
-                  path: data.gridPaths,
                   name: data.gridNames,
-                }).map(({image, path, name}) =>
+                }).map(({image, name}) =>
                     image.slots({
-                      path,
                       missingSourceContent:
                         language.$('misc.coverGrid.noCoverArt', {
                           album: name,
                         }),
                     })),
+
               info:
                 stitchArrays({
                   numTracks: data.gridNumTracks,
diff --git a/src/content/dependencies/generateIntrapageDotSwitcher.js b/src/content/dependencies/generateIntrapageDotSwitcher.js
index 3f300676..1d58367d 100644
--- a/src/content/dependencies/generateIntrapageDotSwitcher.js
+++ b/src/content/dependencies/generateIntrapageDotSwitcher.js
@@ -42,6 +42,8 @@ export default {
         }).map(({title, targetID}) =>
             html.tag('a', {href: '#'},
               {'data-target-id': targetID},
+              {[html.onlyIfContent]: true},
+
               language.sanitize(title))),
     }),
 };
diff --git a/src/content/dependencies/generateLyricsEntry.js b/src/content/dependencies/generateLyricsEntry.js
new file mode 100644
index 00000000..4f9c22f1
--- /dev/null
+++ b/src/content/dependencies/generateLyricsEntry.js
@@ -0,0 +1,25 @@
+export default {
+  contentDependencies: [
+    'transformContent',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  relations: (relation, entry) => ({
+    content:
+      relation('transformContent', entry.body),
+  }),
+
+  slots: {
+    attributes: {
+      type: 'attributes',
+      mutable: false,
+    },
+  },
+
+  generate: (relations, slots, {html}) =>
+    html.tag('div', {class: 'lyrics-entry'},
+      slots.attributes,
+
+      relations.content.slot('mode', 'lyrics')),
+};
diff --git a/src/content/dependencies/generateLyricsSection.js b/src/content/dependencies/generateLyricsSection.js
new file mode 100644
index 00000000..f6b719a9
--- /dev/null
+++ b/src/content/dependencies/generateLyricsSection.js
@@ -0,0 +1,81 @@
+import {stitchArrays} from '#sugar';
+
+export default {
+  contentDependencies: [
+    'generateContentHeading',
+    'generateIntrapageDotSwitcher',
+    'generateLyricsEntry',
+    'transformContent',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  relations: (relation, entries) => ({
+    heading:
+      relation('generateContentHeading'),
+
+    switcher:
+      relation('generateIntrapageDotSwitcher'),
+
+    entries:
+      entries
+        .map(entry => relation('generateLyricsEntry', entry)),
+
+    annotations:
+      entries
+        .map(entry => entry.annotation)
+        .map(annotation => relation('transformContent', annotation)),
+  }),
+
+  data: (entries) => ({
+    ids:
+      Array.from(
+        {length: entries.length},
+        (_, index) => 'lyrics-entry-' + index),
+  }),
+
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('releaseInfo.lyrics', capsule =>
+      html.tags([
+        relations.heading
+          .slots({
+            attributes: {id: 'lyrics'},
+            title: language.$(capsule),
+          }),
+
+        html.tag('p', {class: 'lyrics-switcher'},
+          {[html.onlyIfContent]: true},
+
+          language.$(capsule, 'switcher', {
+            [language.onlyIfOptions]: ['entries'],
+
+            entries:
+              relations.switcher.slots({
+                initialOptionIndex: 0,
+
+                titles:
+                  relations.annotations.map(annotation =>
+                    annotation.slots({
+                      mode: 'inline',
+                      textOnly: true,
+                    })),
+
+                targetIDs:
+                  data.ids,
+              }),
+          })),
+
+        stitchArrays({
+          entry: relations.entries,
+          id: data.ids,
+        }).map(({entry, id}, index) =>
+            entry.slots({
+              attributes: [
+                {id},
+
+                index >= 1 &&
+                  {style: 'display: none'},
+              ],
+            })),
+      ])),
+};
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index d667a6f4..0acf401c 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -1,5 +1,5 @@
 import {openAggregate} from '#aggregate';
-import {empty, repeat} from '#sugar';
+import {atOffset, empty, repeat} from '#sugar';
 
 export default {
   contentDependencies: [
@@ -93,7 +93,7 @@ export default {
       mutable: false,
     },
 
-    cover: {
+    artworkColumnContent: {
       type: 'html',
       mutable: false,
     },
@@ -262,6 +262,17 @@ export default {
         ? data.canonicalBase + pagePathStringFromRoot
         : null);
 
+    const firstItemInArtworkColumn =
+      html.smooth(slots.artworkColumnContent)
+        .content[0];
+
+    const primaryCover =
+      (firstItemInArtworkColumn &&
+       html.resolve(firstItemInArtworkColumn, {normalize: 'tag'})
+         .attributes.has('class', 'cover-artwork')
+        ? firstItemInArtworkColumn
+        : null);
+
     const titleContentsHTML =
       (html.isBlank(slots.title)
         ? null
@@ -279,7 +290,7 @@ export default {
         ? [
             relations.stickyHeadingContainer.slots({
               title: titleContentsHTML,
-              cover: slots.cover,
+              cover: primaryCover,
             }),
 
             relations.stickyHeadingContainer.clone().slots({
@@ -316,9 +327,11 @@ export default {
         [
           titleHTML,
 
-          html.tag('div', {id: 'cover-art-container'},
+          html.tag('div', {id: 'artwork-column'},
             {[html.onlyIfContent]: true},
-            slots.cover),
+            {class: 'isolate-tooltip-z-indexing'},
+
+            slots.artworkColumnContent),
 
           subtitleHTML,
 
@@ -361,7 +374,7 @@ export default {
 
             slots.navLinks
               ?.filter(Boolean)
-              ?.map((cur, i) => {
+              ?.map((cur, i, entries) => {
                 let content;
 
                 if (cur.html) {
@@ -395,20 +408,13 @@ export default {
                   (slots.navLinkStyle === 'hierarchical' &&
                     i === slots.navLinks.length - 1);
 
-                return (
+                const navLink =
                   html.tag('span', {class: 'nav-link'},
                     showAsCurrent &&
                       {class: 'current'},
 
                     [
                       html.tag('span', {class: 'nav-link-content'},
-                        // Use inline-block styling on the content span,
-                        // rather than wrapping the whole nav-link in a proper
-                        // blockwrap, so that if the content spans multiple
-                        // lines, it'll kick the accent down beneath it.
-                        i > 0 &&
-                          {class: 'blockwrap'},
-
                         content),
 
                       html.tag('span', {class: 'nav-link-accent'},
@@ -419,7 +425,25 @@ export default {
                           [language.onlyIfOptions]: ['links'],
                           links: cur.accent,
                         })),
-                    ]));
+                    ]);
+
+                if (slots.navLinkStyle === 'index') {
+                  return navLink;
+                }
+
+                const prev =
+                  atOffset(entries, i, -1);
+
+                if (
+                  prev &&
+                  prev.releaseRestToWrapTogether !== true &&
+                  (prev.releaseRestToWrapTogether === false ||
+                   prev.auto === 'home')
+                ) {
+                  return navLink;
+                } else {
+                  return html.metatag('blockwrap', navLink);
+                }
               })),
 
           html.tag('div', {class: 'nav-bottom-row'},
@@ -559,6 +583,11 @@ export default {
           `    background-image: url("${to('media.path', 'bg.jpg')}");\n` +
           `}`);
 
+    const goshFrigginDarnitStyleRule =
+      `.image-media-link::after {\n` +
+      `    mask-image: url("${to('staticMisc.path', 'image.svg')}");\n` +
+      `}`;
+
     const numWallpaperParts =
       html.resolve(slots.styleRules, {normalize: 'string'})
         .match(/\.wallpaper-part:nth-child/g)
@@ -709,6 +738,7 @@ export default {
                 .slot('color', slots.color ?? data.wikiColor),
 
               fallbackBackgroundStyleRule,
+              goshFrigginDarnitStyleRule,
               slots.styleRules,
             ]),
 
diff --git a/src/content/dependencies/generateReferencedArtworksPage.js b/src/content/dependencies/generateReferencedArtworksPage.js
index 3d21b15d..154b4762 100644
--- a/src/content/dependencies/generateReferencedArtworksPage.js
+++ b/src/content/dependencies/generateReferencedArtworksPage.js
@@ -1,67 +1,55 @@
-import {stitchArrays} from '#sugar';
-
 export default {
   contentDependencies: [
+    'generateCoverArtwork',
     'generateCoverGrid',
     'generatePageLayout',
     'image',
-    'linkAlbum',
-    'linkTrack',
+    'linkAnythingMan',
   ],
 
   extraDependencies: ['html', 'language'],
 
-  relations: (relation, referencedArtworks) => ({
+  relations: (relation, artwork) => ({
     layout:
       relation('generatePageLayout'),
 
+    cover:
+      relation('generateCoverArtwork', artwork),
+
     coverGrid:
       relation('generateCoverGrid'),
 
     links:
-      referencedArtworks.map(({thing}) =>
-        (thing.album
-          ? relation('linkTrack', thing)
-          : relation('linkAlbum', thing))),
+      artwork.referencedArtworks.map(({artwork}) =>
+        relation('linkAnythingMan', artwork.thing)),
 
     images:
-      referencedArtworks.map(({thing}) =>
-        relation('image', thing.artTags)),
+      artwork.referencedArtworks.map(({artwork}) =>
+        relation('image', artwork)),
   }),
 
-  data: (referencedArtworks) => ({
+  data: (artwork) => ({
+    color:
+      artwork.thing.color,
+
     count:
-      referencedArtworks.length,
+      artwork.referencedArtworks.length,
 
     names:
-      referencedArtworks
-        .map(({thing}) => thing.name),
-
-    paths:
-      referencedArtworks
-        .map(({thing}) =>
-          (thing.album
-            ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension]
-            : ['media.albumCover', thing.directory, thing.coverArtFileExtension])),
-
-    dimensions:
-      referencedArtworks
-        .map(({thing}) => thing.coverArtDimensions),
+      artwork.referencedArtworks
+        .map(({artwork}) => artwork.thing.name),
 
     coverArtistNames:
-      referencedArtworks
-        .map(({thing}) =>
-          thing.coverArtistContribs
+      artwork.referencedArtworks
+        .map(({artwork}) =>
+          artwork.artistContribs
             .map(contrib => contrib.artist.name)),
   }),
 
   slots: {
-    color: {validate: v => v.isColor},
-
     styleRules: {type: 'html', mutable: false},
 
     title: {type: 'html', mutable: false},
-    cover: {type: 'html', mutable: true},
 
     navLinks: {validate: v => v.isArray},
     navBottomRowContent: {type: 'html', mutable: false},
@@ -73,11 +61,13 @@ export default {
         title: slots.title,
         subtitle: language.$(pageCapsule, 'subtitle'),
 
-        color: slots.color,
+        color: data.color,
         styleRules: slots.styleRules,
 
-        cover:
-          slots.cover.slot('details', 'artists'),
+        artworkColumnContent:
+          relations.cover.slots({
+            showArtistDetails: true,
+          }),
 
         mainClasses: ['top-index'],
         mainContent: [
@@ -91,19 +81,9 @@ export default {
 
           relations.coverGrid.slots({
             links: relations.links,
+            images: relations.images,
             names: data.names,
 
-            images:
-              stitchArrays({
-                image: relations.images,
-                path: data.paths,
-                dimensions: data.dimensions,
-              }).map(({image, path, dimensions}) =>
-                  image.slots({
-                    path,
-                    dimensions,
-                  })),
-
             info:
               data.coverArtistNames.map(names =>
                 language.$('misc.coverGrid.details.coverArtists', {
diff --git a/src/content/dependencies/generateReferencingArtworksPage.js b/src/content/dependencies/generateReferencingArtworksPage.js
index 2fe2e93d..55977b37 100644
--- a/src/content/dependencies/generateReferencingArtworksPage.js
+++ b/src/content/dependencies/generateReferencingArtworksPage.js
@@ -1,67 +1,55 @@
-import {stitchArrays} from '#sugar';
-
 export default {
   contentDependencies: [
+    'generateCoverArtwork',
     'generateCoverGrid',
     'generatePageLayout',
     'image',
-    'linkAlbum',
-    'linkTrack',
+    'linkAnythingMan',
   ],
 
   extraDependencies: ['html', 'language'],
 
-  relations: (relation, referencingArtworks) => ({
+  relations: (relation, artwork) => ({
     layout:
       relation('generatePageLayout'),
 
+    cover:
+      relation('generateCoverArtwork', artwork),
+
     coverGrid:
       relation('generateCoverGrid'),
 
     links:
-      referencingArtworks.map(({thing}) =>
-        (thing.album
-          ? relation('linkTrack', thing)
-          : relation('linkAlbum', thing))),
+      artwork.referencedByArtworks.map(({artwork}) =>
+        relation('linkAnythingMan', artwork.thing)),
 
     images:
-      referencingArtworks.map(({thing}) =>
-        relation('image', thing.artTags)),
+      artwork.referencedByArtworks.map(({artwork}) =>
+        relation('image', artwork)),
   }),
 
-  data: (referencingArtworks) => ({
+  data: (artwork) => ({
+    color:
+      artwork.thing.color,
+
     count:
-      referencingArtworks.length,
+      artwork.referencedByArtworks.length,
 
     names:
-      referencingArtworks
-        .map(({thing}) => thing.name),
-
-    paths:
-      referencingArtworks
-        .map(({thing}) =>
-          (thing.album
-            ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension]
-            : ['media.albumCover', thing.directory, thing.coverArtFileExtension])),
-
-    dimensions:
-      referencingArtworks
-        .map(({thing}) => thing.coverArtDimensions),
+      artwork.referencedByArtworks
+        .map(({artwork}) => artwork.thing.name),
 
     coverArtistNames:
-      referencingArtworks
-        .map(({thing}) =>
-          thing.coverArtistContribs
+      artwork.referencedByArtworks
+        .map(({artwork}) =>
+          artwork.artistContribs
             .map(contrib => contrib.artist.name)),
   }),
 
   slots: {
-    color: {validate: v => v.isColor},
-
     styleRules: {type: 'html', mutable: false},
 
     title: {type: 'html', mutable: false},
-    cover: {type: 'html', mutable: true},
 
     navLinks: {validate: v => v.isArray},
     navBottomRowContent: {type: 'html', mutable: false},
@@ -73,11 +61,13 @@ export default {
         title: slots.title,
         subtitle: language.$(pageCapsule, 'subtitle'),
 
-        color: slots.color,
+        color: data.color,
         styleRules: slots.styleRules,
 
-        cover:
-          slots.cover.slot('details', 'artists'),
+        artworkColumnContent:
+          relations.cover.slots({
+            showArtistDetails: true,
+          }),
 
         mainClasses: ['top-index'],
         mainContent: [
@@ -91,19 +81,9 @@ export default {
 
           relations.coverGrid.slots({
             links: relations.links,
+            images: relations.images,
             names: data.names,
 
-            images:
-              stitchArrays({
-                image: relations.images,
-                path: data.paths,
-                dimensions: data.dimensions,
-              }).map(({image, path, dimensions}) =>
-                  image.slots({
-                    path,
-                    dimensions,
-                  })),
-
             info:
               data.coverArtistNames.map(names =>
                 language.$('misc.coverGrid.details.coverArtists', {
diff --git a/src/content/dependencies/generateTrackArtworkColumn.js b/src/content/dependencies/generateTrackArtworkColumn.js
new file mode 100644
index 00000000..f06d735b
--- /dev/null
+++ b/src/content/dependencies/generateTrackArtworkColumn.js
@@ -0,0 +1,33 @@
+export default {
+  contentDependencies: ['generateCoverArtwork'],
+  extraDependencies: ['html'],
+
+  relations: (relation, track) => ({
+    albumCover:
+      (!track.hasUniqueCoverArt && track.album.hasCoverArt
+        ? relation('generateCoverArtwork', track.album.coverArtworks[0])
+        : null),
+
+    trackCovers:
+      (track.hasUniqueCoverArt
+        ? track.trackArtworks.map(artwork =>
+            relation('generateCoverArtwork', artwork))
+        : []),
+  }),
+
+  generate: (relations, {html}) =>
+    html.tags([
+      relations.albumCover?.slots({
+        showOriginDetails: true,
+        showArtTagDetails: true,
+        showReferenceDetails: true,
+      }),
+
+      relations.trackCovers.map(cover =>
+        cover.slots({
+          showOriginDetails: true,
+          showArtTagDetails: true,
+          showReferenceDetails: true,
+        })),
+    ]),
+};
diff --git a/src/content/dependencies/generateTrackCoverArtwork.js b/src/content/dependencies/generateTrackCoverArtwork.js
deleted file mode 100644
index 9153e2fc..00000000
--- a/src/content/dependencies/generateTrackCoverArtwork.js
+++ /dev/null
@@ -1,143 +0,0 @@
-export default {
-  contentDependencies: [
-    'generateCoverArtwork',
-    'generateCoverArtworkArtTagDetails',
-    'generateCoverArtworkArtistDetails',
-    'generateCoverArtworkReferenceDetails',
-    'image',
-    'linkAlbum',
-    'linkTrackReferencedArtworks',
-    'linkTrackReferencingArtworks',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
-  query: (track) => ({
-    artTags:
-      (track.hasUniqueCoverArt
-        ? track.artTags
-        : track.album.artTags),
-
-    coverArtistContribs:
-      (track.hasUniqueCoverArt
-        ? track.coverArtistContribs
-        : track.album.coverArtistContribs),
-  }),
-
-  relations: (relation, query, track) => ({
-    coverArtwork:
-      relation('generateCoverArtwork'),
-
-    image:
-      relation('image'),
-
-    artTagDetails:
-      relation('generateCoverArtworkArtTagDetails',
-        query.artTags),
-
-    artistDetails:
-      relation('generateCoverArtworkArtistDetails',
-        query.coverArtistContribs),
-
-    referenceDetails:
-      relation('generateCoverArtworkReferenceDetails',
-        track.referencedArtworks,
-        track.referencedByArtworks),
-
-    referencedArtworksLink:
-      relation('linkTrackReferencedArtworks', track),
-
-    referencingArtworksLink:
-      relation('linkTrackReferencingArtworks', track),
-
-    albumLink:
-      relation('linkAlbum', track.album),
-  }),
-
-  data: (query, track) => ({
-    path:
-      (track.hasUniqueCoverArt
-        ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension]
-        : ['media.albumCover', track.album.directory, track.album.coverArtFileExtension]),
-
-    color:
-      track.color,
-
-    dimensions:
-      (track.hasUniqueCoverArt
-        ? track.coverArtDimensions
-        : track.album.coverArtDimensions),
-
-    nonUnique:
-      !track.hasUniqueCoverArt,
-
-    warnings:
-      query.artTags
-        .filter(tag => tag.isContentWarning)
-        .map(tag => tag.name),
-  }),
-
-  slots: {
-    mode: {type: 'string'},
-
-    details: {
-      validate: v => v.is('tags', 'artists'),
-      default: 'tags',
-    },
-
-    showReferenceLinks: {
-      type: 'boolean',
-      default: false,
-    },
-
-    showNonUniqueLine: {
-      type: 'boolean',
-      default: false,
-    },
-  },
-
-  generate: (data, relations, slots, {html, language}) =>
-    relations.coverArtwork.slots({
-      mode: slots.mode,
-
-      image:
-        relations.image.slots({
-          path: data.path,
-          color: data.color,
-          alt: language.$('misc.alt.trackCover'),
-        }),
-
-      dimensions: data.dimensions,
-      warnings: data.warnings,
-
-      details: [
-        slots.details === 'tags' &&
-          relations.artTagDetails,
-
-        slots.details === 'artists'&&
-          relations.artistDetails,
-
-        slots.showReferenceLinks &&
-          relations.referenceDetails.slots({
-            referencedLink:
-              relations.referencedArtworksLink,
-
-            referencingLink:
-              relations.referencingArtworksLink,
-          }),
-
-        slots.showNonUniqueLine &&
-        data.nonUnique &&
-          html.tag('p', {class: 'image-details'},
-            {class: 'non-unique-details'},
-
-            language.$('misc.trackArtFromAlbum', {
-              album:
-                relations.albumLink.slots({
-                  color: false,
-                }),
-            })),
-      ],
-    }),
-};
-
diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js
index 1c349c2e..11d179ad 100644
--- a/src/content/dependencies/generateTrackInfoPage.js
+++ b/src/content/dependencies/generateTrackInfoPage.js
@@ -9,9 +9,10 @@ export default {
     'generateCommentaryEntry',
     'generateContentHeading',
     'generateContributionList',
+    'generateLyricsSection',
     'generatePageLayout',
     'generateTrackArtistCommentarySection',
-    'generateTrackCoverArtwork',
+    'generateTrackArtworkColumn',
     'generateTrackInfoPageFeaturedByFlashesList',
     'generateTrackInfoPageOtherReleasesList',
     'generateTrackList',
@@ -58,10 +59,8 @@ export default {
     additionalNamesBox:
       relation('generateAdditionalNamesBox', track.additionalNames),
 
-    cover:
-      (track.hasUniqueCoverArt || track.album.hasCoverArt
-        ? relation('generateTrackCoverArtwork', track)
-        : null),
+    artworkColumn:
+      relation('generateTrackArtworkColumn', track),
 
     contentHeading:
       relation('generateContentHeading'),
@@ -92,8 +91,8 @@ export default {
     flashesThatFeatureList:
       relation('generateTrackInfoPageFeaturedByFlashesList', track),
 
-    lyrics:
-      relation('transformContent', track.lyrics),
+    lyricsSection:
+      relation('generateLyricsSection', track.lyrics),
 
     sheetMusicFilesList:
       relation('generateAlbumAdditionalFilesList',
@@ -141,13 +140,8 @@ export default {
         color: data.color,
         styleRules: [relations.albumStyleRules],
 
-        cover:
-          (relations.cover
-            ? relations.cover.slots({
-                showReferenceLinks: true,
-                showNonUniqueLine: true,
-              })
-            : null),
+        artworkColumnContent:
+          relations.artworkColumn,
 
         mainContent: [
           relations.releaseInfo,
@@ -315,17 +309,7 @@ export default {
             relations.flashesThatFeatureList,
           ]),
 
-          html.tags([
-            relations.contentHeading.clone()
-              .slots({
-                attributes: {id: 'lyrics'},
-                title: language.$('releaseInfo.lyrics'),
-              }),
-
-            html.tag('blockquote',
-              {[html.onlyIfContent]: true},
-              relations.lyrics.slot('mode', 'lyrics')),
-          ]),
+          relations.lyricsSection,
 
           html.tags([
             relations.contentHeading.clone()
diff --git a/src/content/dependencies/generateTrackNavLinks.js b/src/content/dependencies/generateTrackNavLinks.js
index e01653f0..6a8b7c64 100644
--- a/src/content/dependencies/generateTrackNavLinks.js
+++ b/src/content/dependencies/generateTrackNavLinks.js
@@ -15,7 +15,7 @@ export default {
       track.album.hasTrackNumbers,
 
     trackNumber:
-      track.album.tracks.indexOf(track) + 1,
+      track.trackNumber,
   }),
 
   slots: {
diff --git a/src/content/dependencies/generateTrackReferencedArtworksPage.js b/src/content/dependencies/generateTrackReferencedArtworksPage.js
index ac81e525..93438c5b 100644
--- a/src/content/dependencies/generateTrackReferencedArtworksPage.js
+++ b/src/content/dependencies/generateTrackReferencedArtworksPage.js
@@ -3,7 +3,6 @@ export default {
     'generateAlbumStyleRules',
     'generateBackToTrackLink',
     'generateReferencedArtworksPage',
-    'generateTrackCoverArtwork',
     'generateTrackNavLinks',
   ],
 
@@ -11,7 +10,7 @@ export default {
 
   relations: (relation, track) => ({
     page:
-      relation('generateReferencedArtworksPage', track.referencedArtworks),
+      relation('generateReferencedArtworksPage', track.trackArtworks[0]),
 
     albumStyleRules:
       relation('generateAlbumStyleRules', track.album, track),
@@ -21,17 +20,11 @@ export default {
 
     backToTrackLink:
       relation('generateBackToTrackLink', track),
-
-    cover:
-      relation('generateTrackCoverArtwork', track),
   }),
 
   data: (track) => ({
     name:
       track.name,
-
-    color:
-      track.color,
   }),
 
   generate: (data, relations, {html, language}) =>
@@ -42,11 +35,8 @@ export default {
             data.name,
         }),
 
-      color: data.color,
       styleRules: [relations.albumStyleRules],
 
-      cover: relations.cover,
-
       navLinks:
         html.resolve(
           relations.navLinks
diff --git a/src/content/dependencies/generateTrackReferencingArtworksPage.js b/src/content/dependencies/generateTrackReferencingArtworksPage.js
index 097ee929..e9818bad 100644
--- a/src/content/dependencies/generateTrackReferencingArtworksPage.js
+++ b/src/content/dependencies/generateTrackReferencingArtworksPage.js
@@ -3,7 +3,6 @@ export default {
     'generateAlbumStyleRules',
     'generateBackToTrackLink',
     'generateReferencingArtworksPage',
-    'generateTrackCoverArtwork',
     'generateTrackNavLinks',
   ],
 
@@ -11,7 +10,7 @@ export default {
 
   relations: (relation, track) => ({
     page:
-      relation('generateReferencingArtworksPage', track.referencedByArtworks),
+      relation('generateReferencingArtworksPage', track.trackArtworks[0]),
 
     albumStyleRules:
       relation('generateAlbumStyleRules', track.album, track),
@@ -21,17 +20,11 @@ export default {
 
     backToTrackLink:
       relation('generateBackToTrackLink', track),
-
-    cover:
-      relation('generateTrackCoverArtwork', track),
   }),
 
   data: (track) => ({
     name:
       track.name,
-
-    color:
-      track.color,
   }),
 
   generate: (data, relations, {html, language}) =>
@@ -42,11 +35,8 @@ export default {
             data.name,
         }),
 
-      color: data.color,
       styleRules: [relations.albumStyleRules],
 
-      cover: relations.cover,
-
       navLinks:
         html.resolve(
           relations.navLinks
diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js
index 38b8383f..54e462c7 100644
--- a/src/content/dependencies/generateTrackReleaseInfo.js
+++ b/src/content/dependencies/generateTrackReleaseInfo.js
@@ -14,11 +14,6 @@ export default {
     relations.artistContributionLinks =
       relation('generateReleaseInfoContributionsLine', track.artistContribs);
 
-    if (track.hasUniqueCoverArt) {
-      relations.coverArtistContributionsLine =
-        relation('generateReleaseInfoContributionsLine', track.coverArtistContribs);
-    }
-
     if (!empty(track.urls)) {
       relations.externalLinks =
         track.urls.map(url =>
@@ -37,7 +32,6 @@ export default {
 
     if (
       track.hasUniqueCoverArt &&
-      track.coverArtDate &&
       +track.coverArtDate !== +track.date
     ) {
       data.coverArtDate = track.coverArtDate;
@@ -60,21 +54,11 @@ export default {
               chronologyKind: 'track',
             }),
 
-            relations.coverArtistContributionsLine?.slots({
-              stringKey: capsule + '.coverArtBy',
-              chronologyKind: 'trackArt',
-            }),
-
             language.$(capsule, 'released', {
               [language.onlyIfOptions]: ['date'],
               date: language.formatDate(data.date),
             }),
 
-            language.$(capsule, 'artReleased', {
-              [language.onlyIfOptions]: ['date'],
-              date: language.formatDate(data.coverArtDate),
-            }),
-
             language.$(capsule, 'duration', {
               [language.onlyIfOptions]: ['duration'],
               duration: language.formatDuration(data.duration),
diff --git a/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js b/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js
index 3068d951..b45bfc19 100644
--- a/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js
+++ b/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js
@@ -1,5 +1,3 @@
-import {stitchArrays} from '#sugar';
-
 export default {
   contentDependencies: ['generateCoverCarousel', 'image', 'linkAlbum'],
 
@@ -13,27 +11,12 @@ export default {
 
     images:
       row.albums
-        .map(album => relation('image', album.artTags)),
-  }),
-
-  data: (row) => ({
-    paths:
-      row.albums.map(album =>
-        (album.hasCoverArt
-          ? ['media.albumCover', album.directory, album.coverArtFileExtension]
-          : null)),
+        .map(album => relation('image', album.coverArtworks[0])),
   }),
 
-  generate: (data, relations) =>
+  generate: (relations) =>
     relations.coverCarousel.slots({
-      links:
-        relations.links,
-
-      images:
-        stitchArrays({
-          image: relations.images,
-          path: data.paths,
-        }).map(({image, path}) =>
-            image.slot('path', path)),
+      links: relations.links,
+      images: relations.images,
     }),
 };
diff --git a/src/content/dependencies/generateWikiHomepageAlbumGridRow.js b/src/content/dependencies/generateWikiHomepageAlbumGridRow.js
index c1d2c79d..a00136ba 100644
--- a/src/content/dependencies/generateWikiHomepageAlbumGridRow.js
+++ b/src/content/dependencies/generateWikiHomepageAlbumGridRow.js
@@ -45,20 +45,17 @@ export default {
 
     images:
       sprawl.albums
-        .map(album => relation('image', album.artTags)),
+        .map(album =>
+          relation('image',
+            (album.hasCoverArt
+              ? album.coverArtworks[0]
+              : null))),
   }),
 
   data: (sprawl, _row) => ({
     names:
       sprawl.albums
         .map(album => album.name),
-
-    paths:
-      sprawl.albums
-        .map(album =>
-          (album.hasCoverArt
-            ? ['media.albumCover', album.directory, album.coverArtFileExtension]
-            : null)),
   }),
 
   generate: (data, relations, {language}) =>
@@ -69,11 +66,9 @@ export default {
       images:
         stitchArrays({
           image: relations.images,
-          path: data.paths,
           name: data.names,
-        }).map(({image, path, name}) =>
+        }).map(({image, name}) =>
             image.slots({
-              path,
               missingSourceContent:
                 language.$('misc.coverGrid.noCoverArt', {
                   album: name,
diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js
index bc268ec1..bf47b14f 100644
--- a/src/content/dependencies/image.js
+++ b/src/content/dependencies/image.js
@@ -16,68 +16,77 @@ export default {
 
   contentDependencies: ['generateColorStyleAttribute'],
 
-  relations: (relation) => ({
+  relations: (relation, _artwork) => ({
     colorStyle:
       relation('generateColorStyleAttribute'),
   }),
 
-  data(artTags) {
-    const data = {};
-
-    if (artTags) {
-      data.contentWarnings =
-        artTags
-          .filter(artTag => artTag.isContentWarning)
-          .map(artTag => artTag.name);
-    } else {
-      data.contentWarnings = null;
-    }
-
-    return data;
-  },
+  data: (artwork) => ({
+    path:
+      (artwork
+        ? artwork.path
+        : null),
+
+    warnings:
+      (artwork
+        ? artwork.artTags
+            .filter(artTag => artTag.isContentWarning)
+            .map(artTag => artTag.name)
+        : null),
+
+    dimensions:
+      (artwork
+        ? artwork.dimensions
+        : null),
+  }),
 
   slots: {
-    src: {type: 'string'},
-
-    path: {
-      validate: v => v.validateArrayItems(v.isString),
-    },
-
     thumb: {type: 'string'},
 
+    reveal: {type: 'boolean', default: true},
+    lazy: {type: 'boolean', default: false},
+    square: {type: 'boolean', default: false},
+
     link: {
       validate: v => v.anyOf(v.isBoolean, v.isString),
       default: false,
     },
 
-    color: {
-      validate: v => v.isColor,
-    },
+    color: {validate: v => v.isColor},
 
-    warnings: {
-      validate: v => v.looseArrayOf(v.isString),
+    // Added to the .image-container.
+    attributes: {
+      type: 'attributes',
+      mutable: false,
     },
 
-    reveal: {type: 'boolean', default: true},
-    lazy: {type: 'boolean', default: false},
-
-    square: {type: 'boolean', default: false},
+    // Added to the <img> itself.
+    alt: {type: 'string'},
 
-    dimensions: {
-      validate: v => v.isDimensions,
-    },
+    // Specify 'src' or 'path', or the path will be used from the artwork.
+    // If none of the above is present, the message in missingSourceContent
+    // will be displayed instead.
 
-    alt: {type: 'string'},
+    src: {type: 'string'},
 
-    attributes: {
-      type: 'attributes',
-      mutable: false,
+    path: {
+      validate: v => v.validateArrayItems(v.isString),
     },
 
     missingSourceContent: {
       type: 'html',
       mutable: false,
     },
+
+    // These will also be used from the artwork if not specified as slots.
+
+    warnings: {
+      validate: v => v.looseArrayOf(v.isString),
+    },
+
+    dimensions: {
+      validate: v => v.isDimensions,
+    },
   },
 
   generate(data, relations, slots, {
@@ -91,15 +100,14 @@ export default {
     missingImagePaths,
     to,
   }) {
-    let originalSrc;
-
-    if (slots.src) {
-      originalSrc = slots.src;
-    } else if (!empty(slots.path)) {
-      originalSrc = to(...slots.path);
-    } else {
-      originalSrc = '';
-    }
+    const originalSrc =
+      (slots.src
+        ? slots.src
+     : slots.path
+        ? to(...slots.path)
+     : data.path
+        ? to(...data.path)
+        : '');
 
     // TODO: This feels janky. It's necessary to deal with static content that
     // includes strings like <img src="media/misc/foo.png">, but processing the
@@ -121,29 +129,27 @@ export default {
       !isMissingImageFile &&
       (typeof slots.link === 'string' || slots.link);
 
-    const contentWarnings =
-      slots.warnings ??
-      data.contentWarnings;
+    const warnings = slots.warnings ?? data.warnings;
+    const dimensions = slots.dimensions ?? data.dimensions;
 
     const willReveal =
       slots.reveal &&
       originalSrc &&
       !isMissingImageFile &&
-      !empty(contentWarnings);
-
-    const willSquare =
-      slots.square;
+      !empty(warnings);
 
     const imgAttributes = html.attributes([
       {class: 'image'},
 
       slots.alt && {alt: slots.alt},
 
-      slots.dimensions?.[0] &&
-        {width: slots.dimensions[0]},
+      dimensions &&
+      dimensions[0] &&
+        {width: dimensions[0]},
 
-      slots.dimensions?.[1] &&
-        {height: slots.dimensions[1]},
+      dimensions &&
+      dimensions[1] &&
+        {height: dimensions[1]},
     ]);
 
     const isPlaceholder =
@@ -169,7 +175,7 @@ export default {
 
         html.tag('span', {class: 'reveal-warnings'},
           language.$('misc.contentWarnings.warnings', {
-            warnings: language.formatUnitList(contentWarnings),
+            warnings: language.formatUnitList(warnings),
           })),
 
         html.tag('br'),
@@ -323,14 +329,14 @@ export default {
 
       wrapped =
         html.tag('div', {class: 'image-outer-area'},
-          willSquare &&
+          slots.square &&
             {class: 'square-content'},
 
           wrapped);
 
       wrapped =
         html.tag('div', {class: 'image-container'},
-          willSquare &&
+          slots.square &&
             {class: 'square'},
 
           typeof slots.link === 'string' &&
diff --git a/src/content/dependencies/linkAnythingMan.js b/src/content/dependencies/linkAnythingMan.js
index d4697403..e408c1b2 100644
--- a/src/content/dependencies/linkAnythingMan.js
+++ b/src/content/dependencies/linkAnythingMan.js
@@ -1,6 +1,7 @@
 export default {
   contentDependencies: [
     'linkAlbum',
+    'linkArtwork',
     'linkFlash',
     'linkTrack',
   ],
@@ -13,6 +14,8 @@ export default {
     link:
       (query.referenceType === 'album'
         ? relation('linkAlbum', thing)
+     : query.referenceType === 'artwork'
+        ? relation('linkArtwork', thing)
      : query.referenceType === 'flash'
         ? relation('linkFlash', thing)
      : query.referenceType === 'track'
diff --git a/src/content/dependencies/linkArtwork.js b/src/content/dependencies/linkArtwork.js
new file mode 100644
index 00000000..8cd6f359
--- /dev/null
+++ b/src/content/dependencies/linkArtwork.js
@@ -0,0 +1,20 @@
+export default {
+  contentDependencies: ['linkAlbum', 'linkTrack'],
+
+  query: (artwork) => ({
+    referenceType:
+      artwork.thing.constructor[Symbol.for('Thing.referenceType')],
+  }),
+
+  relations: (relation, query, artwork) => ({
+    link:
+      (query.referenceType === 'album'
+        ? relation('linkAlbum', artwork.thing)
+     : query.referenceType === 'track'
+        ? relation('linkTrack', artwork.thing)
+        : null),
+  }),
+
+  generate: (relations) =>
+    relations.link,
+};
diff --git a/src/content/dependencies/linkReferencedArtworks.js b/src/content/dependencies/linkReferencedArtworks.js
new file mode 100644
index 00000000..c456b808
--- /dev/null
+++ b/src/content/dependencies/linkReferencedArtworks.js
@@ -0,0 +1,24 @@
+import Thing from '#thing';
+
+export default {
+  contentDependencies: [
+    'linkAlbumReferencedArtworks',
+    'linkTrackReferencedArtworks',
+  ],
+
+  query: (artwork) => ({
+    referenceType:
+      artwork.thing.constructor[Thing.referenceType],
+  }),
+
+  relations: (relation, query, artwork) => ({
+    link:
+      (query.referenceType === 'album'
+        ? relation('linkAlbumReferencedArtworks', artwork.thing)
+     : query.referenceType === 'track'
+        ? relation('linkTrackReferencedArtworks', artwork.thing)
+        : null),
+  }),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/content/dependencies/linkReferencingArtworks.js b/src/content/dependencies/linkReferencingArtworks.js
new file mode 100644
index 00000000..0cfca4db
--- /dev/null
+++ b/src/content/dependencies/linkReferencingArtworks.js
@@ -0,0 +1,24 @@
+import Thing from '#thing';
+
+export default {
+  contentDependencies: [
+    'linkAlbumReferencingArtworks',
+    'linkTrackReferencingArtworks',
+  ],
+
+  query: (artwork) => ({
+    referenceType:
+      artwork.thing.constructor[Thing.referenceType],
+  }),
+
+  relations: (relation, query, artwork) => ({
+    link:
+      (query.referenceType === 'album'
+        ? relation('linkAlbumReferencingArtworks', artwork.thing)
+     : query.referenceType === 'track'
+        ? relation('linkTrackReferencingArtworks', artwork.thing)
+        : null),
+  }),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/content/dependencies/listArtTagNetwork.js b/src/content/dependencies/listArtTagNetwork.js
index 5386dcdc..93dd4ce8 100644
--- a/src/content/dependencies/listArtTagNetwork.js
+++ b/src/content/dependencies/listArtTagNetwork.js
@@ -29,21 +29,21 @@ export default {
 
     const getStats = (artTag) => ({
       directUses:
-        artTag.directlyTaggedInThings.length,
+        artTag.directlyFeaturedInArtworks.length,
 
       // Not currently displayed
       directAndIndirectUses:
         unique([
-          ...artTag.indirectlyTaggedInThings,
-          ...artTag.directlyTaggedInThings,
+          ...artTag.indirectlyFeaturedInArtworks,
+          ...artTag.directlyFeaturedInArtworks,
         ]).length,
 
       totalUses:
         [
-          ...artTag.directlyTaggedInThings,
+          ...artTag.directlyFeaturedInArtworks,
           ...
             artTag.allDescendantArtTags
-              .flatMap(artTag => artTag.directlyTaggedInThings),
+              .flatMap(artTag => artTag.directlyFeaturedInArtworks),
         ].length,
 
       descendants:
diff --git a/src/content/dependencies/listArtTagsByName.js b/src/content/dependencies/listArtTagsByName.js
index 31856478..1df9dfff 100644
--- a/src/content/dependencies/listArtTagsByName.js
+++ b/src/content/dependencies/listArtTagsByName.js
@@ -35,8 +35,8 @@ export default {
       counts:
         query.artTags.map(artTag =>
           unique([
-            ...artTag.indirectlyTaggedInThings,
-            ...artTag.directlyTaggedInThings,
+            ...artTag.indirectlyFeaturedInArtworks,
+            ...artTag.directlyFeaturedInArtworks,
           ]).length),
     };
   },
diff --git a/src/content/dependencies/listArtTagsByUses.js b/src/content/dependencies/listArtTagsByUses.js
index fcd324f7..eca7f1c6 100644
--- a/src/content/dependencies/listArtTagsByUses.js
+++ b/src/content/dependencies/listArtTagsByUses.js
@@ -17,8 +17,8 @@ export default {
     const counts =
       artTags.map(artTag =>
         unique([
-          ...artTag.directlyTaggedInThings,
-          ...artTag.indirectlyTaggedInThings,
+          ...artTag.directlyFeaturedInArtworks,
+          ...artTag.indirectlyFeaturedInArtworks,
         ]).length);
 
     filterByCount(artTags, counts);
diff --git a/src/content/dependencies/listArtistsByGroup.js b/src/content/dependencies/listArtistsByGroup.js
index 0bf9dd2d..17096cfc 100644
--- a/src/content/dependencies/listArtistsByGroup.js
+++ b/src/content/dependencies/listArtistsByGroup.js
@@ -37,20 +37,25 @@ export default {
         ([
           (unique(
             ([
-              artist.albumArtistContributions,
-              artist.albumCoverArtistContributions,
-              artist.albumWallpaperArtistContributions,
-              artist.albumBannerArtistContributions,
+              artist.albumArtistContributions
+                .map(contrib => contrib.thing),
+              artist.albumCoverArtistContributions
+                .map(contrib => contrib.thing.thing),
+              artist.albumWallpaperArtistContributions
+                .map(contrib => contrib.thing.thing),
+              artist.albumBannerArtistContributions
+                .map(contrib => contrib.thing.thing),
             ]).flat()
-              .map(({thing}) => thing)
           )).map(album => album.groups),
           (unique(
             ([
-              artist.trackArtistContributions,
-              artist.trackContributorContributions,
-              artist.trackCoverArtistContributions,
+              artist.trackArtistContributions
+                .map(contrib => contrib.thing),
+              artist.trackContributorContributions
+                .map(contrib => contrib.thing),
+              artist.trackCoverArtistContributions
+                .map(contrib => contrib.thing.thing),
             ]).flat()
-              .map(({thing}) => thing)
           )).map(track => track.album.groups),
         ]).flat()
           .map(groups => groups
diff --git a/src/content/dependencies/listArtistsByLatestContribution.js b/src/content/dependencies/listArtistsByLatestContribution.js
index 27a2faa3..2a8d1b4c 100644
--- a/src/content/dependencies/listArtistsByLatestContribution.js
+++ b/src/content/dependencies/listArtistsByLatestContribution.js
@@ -98,13 +98,16 @@ export default {
       ])) {
         // Might combine later with 'track' of the same album and date.
         considerDate(artist, album.coverArtDate ?? album.date, album, 'artwork');
+        // '?? album.date' is kept here because wallpaper and banner may
+        // technically be present for an album w/o cover art, therefore
+        // also no cover art date.
       }
     }
 
     for (const track of tracksLatestFirst) {
       for (const artist of getArtists(track, 'coverArtistContribs')) {
         // No special effect if artist already has 'artwork' for the same album and date.
-        considerDate(artist, track.coverArtDate ?? track.date, track.album, 'artwork');
+        considerDate(artist, track.coverArtDate, track.album, 'artwork');
       }
 
       for (const artist of new Set([
diff --git a/src/content/dependencies/listTracksWithLyrics.js b/src/content/dependencies/listTracksWithLyrics.js
index a13a76f0..e6ab9d7d 100644
--- a/src/content/dependencies/listTracksWithLyrics.js
+++ b/src/content/dependencies/listTracksWithLyrics.js
@@ -2,7 +2,7 @@ export default {
   contentDependencies: ['listTracksWithExtra'],
 
   relations: (relation, spec) =>
-    ({page: relation('listTracksWithExtra', spec, 'lyrics', 'truthy')}),
+    ({page: relation('listTracksWithExtra', spec, 'lyrics', 'array')}),
 
   generate: (relations) =>
     relations.page,
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js
index f56a1da9..1bbd45e2 100644
--- a/src/content/dependencies/transformContent.js
+++ b/src/content/dependencies/transformContent.js
@@ -2,6 +2,7 @@ import {bindFind} from '#find';
 import {replacerSpec, parseInput} from '#replacer';
 
 import {Marked} from 'marked';
+import striptags from 'striptags';
 
 const commonMarkedOptions = {
   headerIds: false,
@@ -184,6 +185,8 @@ export default {
               link: relation(name, arg),
               label: node.data.label,
               hash: node.data.hash,
+              name: arg?.name,
+              shortName: arg?.shortName ?? arg?.nameShort,
             }
           : getPlaceholder(node, content));
 
@@ -241,6 +244,11 @@ export default {
       default: true,
     },
 
+    textOnly: {
+      type: 'boolean',
+      default: false,
+    },
+
     thumb: {
       validate: v => v.is('small', 'medium', 'large'),
       default: 'large',
@@ -452,7 +460,17 @@ export default {
                 nodeFromRelations.link,
                 {slots: ['content', 'hash']});
 
-            const {label, hash} = nodeFromRelations;
+            const {label, hash, shortName, name} = nodeFromRelations;
+
+            if (slots.textOnly) {
+              if (label) {
+                return {type: 'text', data: label};
+              } else if (slots.preferShortLinkNames) {
+                return {type: 'text', data: shortName ?? name};
+              } else {
+                return {type: 'text', data: name};
+              }
+            }
 
             // These are removed from the typical combined slots({})-style
             // because we don't want to override slots that were already set
@@ -506,6 +524,10 @@ export default {
             const {label} = node.data;
             const externalLink = relations.externalLinks[externalLinkIndex++];
 
+            if (slots.textOnly) {
+              return {type: 'text', data: label};
+            }
+
             externalLink.setSlots({
               content: label,
               fromContent: true,
@@ -542,12 +564,19 @@ export default {
                 ? valueFn(replacerValue)
                 : replacerValue);
 
-            const contents =
+            const content =
               (htmlFn
                 ? htmlFn(value, {html, language})
                 : value);
 
-            return {type: 'text', data: contents.toString()};
+            const contentText =
+              html.resolve(content, {normalize: 'string'});
+
+            if (slots.textOnly) {
+              return {type: 'text', data: striptags(contentText)};
+            } else {
+              return {type: 'text', data: contentText};
+            }
           }
 
           default: