« 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
diff options
context:
space:
mode:
Diffstat (limited to 'src/content')
-rw-r--r--src/content/dependencies/generateAdditionalFilesList.js2
-rw-r--r--src/content/dependencies/generateAdditionalFilesListChunk.js60
-rw-r--r--src/content/dependencies/generateAlbumCommentaryPage.js122
-rw-r--r--src/content/dependencies/generateAlbumGalleryPage.js11
-rw-r--r--src/content/dependencies/generateAlbumInfoPage.js286
-rw-r--r--src/content/dependencies/generateAlbumNavAccent.js11
-rw-r--r--src/content/dependencies/generateAlbumReleaseInfo.js83
-rw-r--r--src/content/dependencies/generateAlbumSidebarGroupBox.js66
-rw-r--r--src/content/dependencies/generateAlbumSidebarTrackSection.js25
-rw-r--r--src/content/dependencies/generateAlbumSocialEmbed.js60
-rw-r--r--src/content/dependencies/generateAlbumTrackList.js35
-rw-r--r--src/content/dependencies/generateAlbumTrackListItem.js97
-rw-r--r--src/content/dependencies/generateAlbumTrackListMissingDuration.js34
-rw-r--r--src/content/dependencies/generateArtTagGalleryPage.js15
-rw-r--r--src/content/dependencies/generateArtistGalleryPage.js31
-rw-r--r--src/content/dependencies/generateArtistGroupContributionsInfo.js188
-rw-r--r--src/content/dependencies/generateArtistInfoPage.js439
-rw-r--r--src/content/dependencies/generateArtistInfoPageArtworksChunk.js34
-rw-r--r--src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js62
-rw-r--r--src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js271
-rw-r--r--src/content/dependencies/generateArtistInfoPageChunk.js45
-rw-r--r--src/content/dependencies/generateArtistInfoPageChunkItem.js70
-rw-r--r--src/content/dependencies/generateArtistInfoPageChunkedList.js11
-rw-r--r--src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js98
-rw-r--r--src/content/dependencies/generateArtistInfoPageFlashesChunk.js34
-rw-r--r--src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js34
-rw-r--r--src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js177
-rw-r--r--src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js36
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunk.js67
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunkItem.js115
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunkedList.js330
-rw-r--r--src/content/dependencies/generateArtistNavLinks.js4
-rw-r--r--src/content/dependencies/generateChronologyLinks.js82
-rw-r--r--src/content/dependencies/generateColorStyleVariables.js2
-rw-r--r--src/content/dependencies/generateCommentaryEntry.js119
-rw-r--r--src/content/dependencies/generateCommentaryIndexPage.js86
-rw-r--r--src/content/dependencies/generateCommentarySection.js25
-rw-r--r--src/content/dependencies/generateContentHeading.js24
-rw-r--r--src/content/dependencies/generateContributionList.js30
-rw-r--r--src/content/dependencies/generateContributionTooltip.js48
-rw-r--r--src/content/dependencies/generateContributionTooltipChronologySection.js117
-rw-r--r--src/content/dependencies/generateContributionTooltipExternalLinkSection.js70
-rw-r--r--src/content/dependencies/generateCoverArtwork.js19
-rw-r--r--src/content/dependencies/generateExternalHandle.js20
-rw-r--r--src/content/dependencies/generateExternalIcon.js26
-rw-r--r--src/content/dependencies/generateExternalPlatform.js20
-rw-r--r--src/content/dependencies/generateFlashActGalleryPage.js78
-rw-r--r--src/content/dependencies/generateFlashCoverArtwork.js24
-rw-r--r--src/content/dependencies/generateFlashIndexPage.js141
-rw-r--r--src/content/dependencies/generateFlashInfoPage.js239
-rw-r--r--src/content/dependencies/generateGroupGalleryPage.js19
-rw-r--r--src/content/dependencies/generateGroupInfoPage.js231
-rw-r--r--src/content/dependencies/generateGroupInfoPageAlbumsSection.js136
-rw-r--r--src/content/dependencies/generateGroupSidebarCategoryDetails.js66
-rw-r--r--src/content/dependencies/generateListAllAdditionalFilesChunk.js79
-rw-r--r--src/content/dependencies/generateListingPage.js72
-rw-r--r--src/content/dependencies/generateNewsEntryPage.js74
-rw-r--r--src/content/dependencies/generateNewsIndexPage.js67
-rw-r--r--src/content/dependencies/generatePageLayout.js183
-rw-r--r--src/content/dependencies/generatePageSidebar.js19
-rw-r--r--src/content/dependencies/generatePageSidebarBox.js2
-rw-r--r--src/content/dependencies/generateQuickDescription.js134
-rw-r--r--src/content/dependencies/generateReleaseInfoContributionsLine.js13
-rw-r--r--src/content/dependencies/generateSearchSidebarBox.js62
-rw-r--r--src/content/dependencies/generateStickyHeadingContainer.js15
-rw-r--r--src/content/dependencies/generateTooltip.js3
-rw-r--r--src/content/dependencies/generateTrackInfoPage.js723
-rw-r--r--src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js62
-rw-r--r--src/content/dependencies/generateTrackInfoPageOtherReleasesList.js80
-rw-r--r--src/content/dependencies/generateTrackList.js89
-rw-r--r--src/content/dependencies/generateTrackListDividedByGroups.js161
-rw-r--r--src/content/dependencies/generateTrackReleaseInfo.js67
-rw-r--r--src/content/dependencies/generateTrackSocialEmbed.js52
-rw-r--r--src/content/dependencies/generateWikiHomeAlbumsRow.js8
-rw-r--r--src/content/dependencies/generateWikiHomeNewsBox.js81
-rw-r--r--src/content/dependencies/image.js15
-rw-r--r--src/content/dependencies/linkAnythingMan.js25
-rw-r--r--src/content/dependencies/linkContribution.js187
-rw-r--r--src/content/dependencies/linkExternalAsIcon.js51
-rw-r--r--src/content/dependencies/listArtistsByContributions.js36
-rw-r--r--src/content/dependencies/listArtistsByDuration.js7
-rw-r--r--src/content/dependencies/listArtistsByGroup.js171
-rw-r--r--src/content/dependencies/listRandomPageLinks.js56
-rw-r--r--src/content/dependencies/listTracksByDate.js3
-rw-r--r--src/content/dependencies/transformContent.js36
-rw-r--r--src/content/util/getChronologyRelations.js55
-rw-r--r--src/content/util/groupTracksByGroup.js23
87 files changed, 3795 insertions, 3491 deletions
diff --git a/src/content/dependencies/generateAdditionalFilesList.js b/src/content/dependencies/generateAdditionalFilesList.js
index f504cf80..68120b23 100644
--- a/src/content/dependencies/generateAdditionalFilesList.js
+++ b/src/content/dependencies/generateAdditionalFilesList.js
@@ -15,6 +15,8 @@ export default {
 
   generate: (slots, {html}) =>
     html.tag('ul', {class: 'additional-files-list'},
+      {[html.onlyIfContent]: true},
+
       stitchArrays({
         chunk: slots.chunks,
         items: slots.chunkItems,
diff --git a/src/content/dependencies/generateAdditionalFilesListChunk.js b/src/content/dependencies/generateAdditionalFilesListChunk.js
index 5804115a..e66560fc 100644
--- a/src/content/dependencies/generateAdditionalFilesListChunk.js
+++ b/src/content/dependencies/generateAdditionalFilesListChunk.js
@@ -17,37 +17,31 @@ export default {
     },
   },
 
-  generate(slots, {html, language}) {
-    const summary =
-      html.tag('summary',
-        html.tag('span',
-          language.$('releaseInfo.additionalFiles.entry', {
-            title:
-              html.tag('span', {class: 'group-name'},
-                slots.title),
-          })));
-
-    const description =
-      html.tag('li', {class: 'entry-description'},
-        {[html.onlyIfContent]: true},
-        slots.description);
-
-    const items =
-      (html.isBlank(slots.items)
-        ? html.tag('li',
-            language.$('releaseInfo.additionalFiles.entry.noFilesAvailable'))
-        : slots.items);
-
-    const content =
-      html.tag('ul', [description, items]);
-
-    const details =
-      html.tag('details',
-        html.isBlank(slots.items) &&
-          {open: true},
-
-        [summary, content]);
-
-    return html.tag('li', details);
-  },
+  generate: (slots, {html, language}) =>
+    language.encapsulate('releaseInfo.additionalFiles.entry', capsule =>
+      html.tag('li',
+        html.tag('details',
+          html.isBlank(slots.items) &&
+            {open: true},
+
+          [
+            html.tag('summary',
+              html.tag('span',
+                language.$(capsule, {
+                  title:
+                    html.tag('span', {class: 'group-name'},
+                      slots.title),
+                }))),
+
+            html.tag('ul', [
+              html.tag('li', {class: 'entry-description'},
+                {[html.onlyIfContent]: true},
+                slots.description),
+
+              (html.isBlank(slots.items)
+                ? html.tag('li',
+                    language.$(capsule, 'noFilesAvailable'))
+                : slots.items),
+            ]),
+          ]))),
 };
diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js
index 7879269f..c14640af 100644
--- a/src/content/dependencies/generateAlbumCommentaryPage.js
+++ b/src/content/dependencies/generateAlbumCommentaryPage.js
@@ -1,4 +1,4 @@
-import {empty, stitchArrays} from '#sugar';
+import {stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
@@ -130,11 +130,11 @@ export default {
     return data;
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout
-      .slots({
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('albumCommentaryPage', pageCapsule =>
+      relations.layout.slots({
         title:
-          language.$('albumCommentaryPage.title', {
+          language.$(pageCapsule, 'title', {
             album: data.name,
           }),
 
@@ -146,7 +146,7 @@ export default {
         mainClasses: ['long-content'],
         mainContent: [
           html.tag('p',
-            language.$('albumCommentaryPage.infoLine', {
+            language.$(pageCapsule, 'infoLine', {
               words:
                 html.tag('b',
                   language.formatWordCount(data.wordCount, {unit: true})),
@@ -156,34 +156,41 @@ export default {
                   language.countCommentaryEntries(data.entryCount, {unit: true})),
             })),
 
-          relations.albumCommentaryEntries && [
-            relations.albumCommentaryHeading.slots({
-              tag: 'h3',
-              color: data.color,
-
-              title:
-                language.$('albumCommentaryPage.entry.title.albumCommentary', {
-                  album: relations.albumCommentaryLink,
-                }),
-
-              accent:
-                !empty(relations.albumCommentaryListeningLinks) &&
-                  language.$('albumCommentaryPage.entry.title.albumCommentary.accent', {
-                    listeningLinks:
-                      language.formatUnitList(
-                        relations.albumCommentaryListeningLinks
-                          .map(link => link.slots({
-                            context: 'album',
-                            tab: 'separate',
-                          }))),
-                  }),
-            }),
-
-            relations.albumCommentaryCover
-              ?.slots({mode: 'commentary'}),
-
-            relations.albumCommentaryEntries,
-          ],
+          relations.albumCommentaryEntries &&
+            language.encapsulate(pageCapsule, 'entry', entryCapsule => [
+              language.encapsulate(entryCapsule, 'title.albumCommentary', titleCapsule =>
+                relations.albumCommentaryHeading.slots({
+                  tag: 'h3',
+                  color: data.color,
+
+                  title:
+                    language.$(titleCapsule, {
+                      album: relations.albumCommentaryLink,
+                    }),
+
+                  stickyTitle:
+                    language.$(titleCapsule, 'sticky', {
+                      album: data.name,
+                    }),
+
+                  accent:
+                    language.$(titleCapsule, 'accent', {
+                      [language.onlyIfOptions]: ['listeningLinks'],
+                      listeningLinks:
+                        language.formatUnitList(
+                          relations.albumCommentaryListeningLinks
+                            .map(link => link.slots({
+                              context: 'album',
+                              tab: 'separate',
+                            }))),
+                    }),
+                })),
+
+              relations.albumCommentaryCover
+                ?.slots({mode: 'commentary'}),
+
+              relations.albumCommentaryEntries,
+            ]),
 
           stitchArrays({
             heading: relations.trackCommentaryHeadings,
@@ -201,31 +208,33 @@ export default {
               cover,
               entries,
               color,
-            }) => [
-              heading.slots({
-                tag: 'h3',
-                id: directory,
-                color,
-
-                title:
-                  language.$('albumCommentaryPage.entry.title.trackCommentary', {
-                    track: link,
-                  }),
-
-                accent:
-                  !empty(listeningLinks) &&
-                    language.$('albumCommentaryPage.entry.title.trackCommentary.accent', {
-                      listeningLinks:
-                        language.formatUnitList(
-                          listeningLinks.map(link =>
-                            link.slot('tab', 'separate'))),
-                    }),
-              }),
+            }) =>
+              language.encapsulate(pageCapsule, 'entry', entryCapsule => [
+                language.encapsulate(entryCapsule, 'title.trackCommentary', titleCapsule =>
+                  heading.slots({
+                    tag: 'h3',
+                    attributes: {id: directory},
+                    color,
+
+                    title:
+                      language.$(titleCapsule, {
+                        track: link,
+                      }),
+
+                    accent:
+                      language.$(titleCapsule, 'accent', {
+                        [language.onlyIfOptions]: ['listeningLinks'],
+                        listeningLinks:
+                          language.formatUnitList(
+                            listeningLinks.map(link =>
+                              link.slot('tab', 'separate'))),
+                      }),
+                  })),
 
               cover?.slots({mode: 'commentary'}),
 
               entries.map(entry => entry.slot('color', color)),
-            ]),
+            ])),
         ],
 
         navLinkStyle: 'hierarchical',
@@ -246,6 +255,5 @@ export default {
         ],
 
         leftSidebar: relations.sidebar,
-      });
-  },
+      })),
 };
diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js
index aa025688..44d49c54 100644
--- a/src/content/dependencies/generateAlbumGalleryPage.js
+++ b/src/content/dependencies/generateAlbumGalleryPage.js
@@ -160,11 +160,11 @@ export default {
     return data;
   },
 
-  generate(data, relations, {language}) {
-    return relations.layout
-      .slots({
+  generate: (data, relations, {language}) =>
+    language.encapsulate('albumGalleryPage', pageCapsule =>
+      relations.layout.slots({
         title:
-          language.$('albumGalleryPage.title', {
+          language.$(pageCapsule, 'title', {
             album: data.name,
           }),
 
@@ -223,6 +223,5 @@ export default {
         ],
 
         secondaryNav: relations.secondaryNav,
-      });
-  },
+      })),
 };
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index 739a6669..1bffe2d0 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -1,8 +1,3 @@
-import {sortAlbumsTracksChronologically} from '#sort';
-import {empty} from '#sugar';
-
-import getChronologyRelations from '../util/getChronologyRelations.js';
-
 export default {
   contentDependencies: [
     'generateAlbumAdditionalFilesList',
@@ -15,148 +10,93 @@ export default {
     'generateAlbumSocialEmbed',
     'generateAlbumStyleRules',
     'generateAlbumTrackList',
-    'generateChronologyLinks',
     'generateCommentarySection',
     'generateContentHeading',
     'generatePageLayout',
-    'linkAlbum',
     'linkAlbumCommentary',
     'linkAlbumGallery',
-    'linkArtist',
-    'linkTrack',
-    'transformContent',
   ],
 
   extraDependencies: ['html', 'language'],
 
-  relations(relation, album) {
-    const relations = {};
-    const sections = relations.sections = {};
-
-    relations.layout =
-      relation('generatePageLayout');
-
-    relations.albumStyleRules =
-      relation('generateAlbumStyleRules', album, null);
-
-    relations.socialEmbed =
-      relation('generateAlbumSocialEmbed', album);
-
-    relations.coverArtistChronologyContributions =
-      getChronologyRelations(album, {
-        contributions: album.coverArtistContribs ?? [],
-
-        linkArtist: artist => relation('linkArtist', artist),
-
-        linkThing: trackOrAlbum =>
-          (trackOrAlbum.album
-            ? relation('linkTrack', trackOrAlbum)
-            : relation('linkAlbum', trackOrAlbum)),
-
-        getThings(artist) {
-          const getDate = thing => thing.coverArtDate ?? thing.date;
-
-          const things = [
-            ...artist.albumsAsCoverArtist,
-            ...artist.tracksAsCoverArtist,
-          ].filter(getDate);
-
-          return sortAlbumsTracksChronologically(things, {getDate});
-        },
-      });
-
-    relations.albumNavAccent =
-      relation('generateAlbumNavAccent', album, null);
-
-    relations.chronologyLinks =
-      relation('generateChronologyLinks');
-
-    relations.secondaryNav =
-      relation('generateAlbumSecondaryNav', album);
-
-    relations.sidebar =
-      relation('generateAlbumSidebar', album, null);
-
-    if (album.hasCoverArt) {
-      relations.cover =
-        relation('generateAlbumCoverArtwork', album);
-    }
-
-    if (album.hasBannerArt) {
-      relations.banner =
-        relation('generateAlbumBanner', album);
-    }
+  relations: (relation, album) => ({
+    layout:
+      relation('generatePageLayout'),
 
-    // Section: Release info
+    albumStyleRules:
+      relation('generateAlbumStyleRules', album, null),
 
-    relations.releaseInfo =
-      relation('generateAlbumReleaseInfo', album);
+    socialEmbed:
+      relation('generateAlbumSocialEmbed', album),
 
-    // Section: Extra links
+    albumNavAccent:
+      relation('generateAlbumNavAccent', album, null),
 
-    const extra = sections.extra = {};
+    secondaryNav:
+      relation('generateAlbumSecondaryNav', album),
 
-    if (album.tracks.some(t => t.hasUniqueCoverArt)) {
-      extra.galleryLink =
-        relation('linkAlbumGallery', album);
-    }
+    sidebar:
+      relation('generateAlbumSidebar', album, null),
 
-    if (album.commentary || album.tracks.some(t => t.commentary)) {
-      extra.commentaryLink =
-        relation('linkAlbumCommentary', album);
-    }
+    cover:
+      (album.hasCoverArt
+        ? relation('generateAlbumCoverArtwork', album)
+        : null),
 
-    // Section: Track list
+    banner:
+      (album.hasBannerArt
+        ? relation('generateAlbumBanner', album)
+        : null),
 
-    relations.trackList =
-      relation('generateAlbumTrackList', album);
+    contentHeading:
+      relation('generateContentHeading'),
 
-    // Section: Additional files
+    releaseInfo:
+      relation('generateAlbumReleaseInfo', album),
 
-    if (!empty(album.additionalFiles)) {
-      const additionalFiles = sections.additionalFiles = {};
+    galleryLink:
+      (album.tracks.some(t => t.hasUniqueCoverArt)
+        ? relation('linkAlbumGallery', album)
+        : null),
 
-      additionalFiles.heading =
-        relation('generateContentHeading');
+    commentaryLink:
+      (album.commentary || album.tracks.some(t => t.commentary)
+        ? relation('linkAlbumCommentary', album)
+        : null),
 
-      additionalFiles.additionalFilesList =
-        relation('generateAlbumAdditionalFilesList', album, album.additionalFiles);
-    }
+    trackList:
+      relation('generateAlbumTrackList', album),
 
-    // Section: Artist commentary
+    additionalFilesList:
+      relation('generateAlbumAdditionalFilesList',
+        album,
+        album.additionalFiles),
 
-    if (album.commentary) {
-      sections.artistCommentary =
-        relation('generateCommentarySection', album.commentary);
-    }
+    artistCommentarySection:
+      relation('generateCommentarySection', album.commentary),
+  }),
 
-    return relations;
-  },
+  data: (album) => ({
+    name:
+      album.name,
 
-  data(album) {
-    const data = {};
+    color:
+      album.color,
 
-    data.name = album.name;
-    data.color = album.color;
+    dateAddedToWiki:
+      album.dateAddedToWiki,
+  }),
 
-    if (!empty(album.additionalFiles)) {
-      data.numAdditionalFiles = album.additionalFiles.length;
-    }
-
-    data.dateAddedToWiki = album.dateAddedToWiki;
-
-    return data;
-  },
-
-  generate(data, relations, {html, language}) {
-    const {sections: sec} = relations;
-
-    return relations.layout
-      .slots({
-        title: language.$('albumPage.title', {album: data.name}),
-        headingMode: 'sticky',
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('albumPage', pageCapsule =>
+      relations.layout.slots({
+        title:
+          language.$(pageCapsule, 'title', {
+            album: data.name,
+          }),
 
         color: data.color,
+        headingMode: 'sticky',
         styleRules: [relations.albumStyleRules],
 
         cover:
@@ -173,38 +113,44 @@ export default {
             {[html.onlyIfContent]: true},
             {[html.joinChildren]: html.tag('br')},
 
-            [
-              sec.additionalFiles &&
-                language.$('releaseInfo.additionalFiles.shortcut', {
+            language.encapsulate('releaseInfo', capsule => [
+              !html.isBlank(relations.additionalFilesList) &&
+                language.$(capsule, 'additionalFiles.shortcut', {
                   link: html.tag('a',
                     {href: '#additional-files'},
-                    language.$('releaseInfo.additionalFiles.shortcut.link')),
-                }),
-
-              sec.extra.galleryLink && sec.extra.commentaryLink &&
-                language.$('releaseInfo.viewGalleryOrCommentary', {
-                  gallery:
-                    sec.extra.galleryLink
-                      .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.gallery')),
-                  commentary:
-                    sec.extra.commentaryLink
-                      .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.commentary')),
-                }),
-
-              sec.extra.galleryLink && !sec.extra.commentaryLink &&
-                language.$('releaseInfo.viewGallery', {
-                  link:
-                    sec.extra.galleryLink
-                      .slot('content', language.$('releaseInfo.viewGallery.link')),
+                    language.$(capsule, 'additionalFiles.shortcut.link')),
                 }),
 
-              !sec.extra.galleryLink && sec.extra.commentaryLink &&
-                language.$('releaseInfo.viewCommentary', {
-                  link:
-                    sec.extra.commentaryLink
-                      .slot('content', language.$('releaseInfo.viewCommentary.link')),
-                }),
-            ]),
+              (relations.galleryLink && relations.commentaryLink
+                ? language.encapsulate(capsule, 'viewGalleryOrCommentary', capsule =>
+                    language.$(capsule, {
+                      gallery:
+                        relations.galleryLink
+                          .slot('content', language.$(capsule, 'gallery')),
+
+                      commentary:
+                        relations.commentaryLink
+                          .slot('content', language.$(capsule, 'commentary')),
+                    }))
+
+             : relations.galleryLink
+                ? language.encapsulate(capsule, 'viewGallery', capsule =>
+                    language.$(capsule, {
+                      link:
+                        relations.galleryLink
+                          .slot('content', language.$(capsule, 'link')),
+                    }))
+
+             : relations.commentaryLink
+                ? language.encapsulate(capsule, 'viewCommentary', capsule =>
+                    language.$(capsule, {
+                      link:
+                        relations.commentaryLink
+                          .slot('content', language.$(capsule, 'link')),
+                    }))
+
+                : html.blank()),
+            ])),
 
           relations.trackList,
 
@@ -212,28 +158,25 @@ export default {
             {[html.onlyIfContent]: true},
             {[html.joinChildren]: html.tag('br')},
 
-            [
-              data.dateAddedToWiki &&
-                language.$('releaseInfo.addedToWiki', {
-                  date: language.formatDate(data.dateAddedToWiki),
-                }),
-            ]),
-
-          sec.additionalFiles && [
-            sec.additionalFiles.heading
-              .slots({
-                id: 'additional-files',
-                title:
-                  language.$('releaseInfo.additionalFiles.heading', {
-                    additionalFiles:
-                      language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}),
-                  }),
+            language.encapsulate('releaseInfo', capsule => [
+              language.$(capsule, 'addedToWiki', {
+                [language.onlyIfOptions]: ['date'],
+                date: language.formatDate(data.dateAddedToWiki),
               }),
+            ])),
+
+          language.encapsulate('releaseInfo.additionalFiles', capsule =>
+            html.tags([
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'additional-files'},
+                  title: language.$(capsule, 'heading'),
+                }),
 
-            sec.additionalFiles.additionalFilesList,
-          ],
+              relations.additionalFilesList,
+            ])),
 
-          sec.artistCommentary,
+          relations.artistCommentarySection,
         ],
 
         navLinkStyle: 'hierarchical',
@@ -249,16 +192,6 @@ export default {
           },
         ],
 
-        navContent:
-          relations.chronologyLinks.slots({
-            chronologyInfoSets: [
-              {
-                headingString: 'misc.chronology.heading.coverArt',
-                contributions: relations.coverArtistChronologyContributions,
-              },
-            ],
-          }),
-
         banner: relations.banner ?? null,
         bannerPosition: 'top',
 
@@ -267,6 +200,5 @@ export default {
         leftSidebar: relations.sidebar,
 
         socialEmbed: relations.socialEmbed,
-      });
-  },
+      })),
 };
diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js
index 121af439..4b6fb062 100644
--- a/src/content/dependencies/generateAlbumNavAccent.js
+++ b/src/content/dependencies/generateAlbumNavAccent.js
@@ -62,18 +62,21 @@ export default {
   },
 
   generate(data, relations, slots, {html, language}) {
+    const albumNavCapsule = language.encapsulate('albumPage.nav');
+    const trackNavCapsule = language.encapsulate('trackPage.nav');
+
     const {content: extraLinks = []} =
       slots.showExtraLinks &&
         {content: [
           (!data.galleryIsStub || slots.currentExtra === 'gallery') &&
             relations.albumGalleryLink?.slots({
               attributes: {class: slots.currentExtra === 'gallery' && 'current'},
-              content: language.$('albumPage.nav.gallery'),
+              content: language.$(albumNavCapsule, 'gallery'),
             }),
 
           relations.albumCommentaryLink?.slots({
             attributes: {class: slots.currentExtra === 'commentary' && 'current'},
-            content: language.$('albumPage.nav.commentary'),
+            content: language.$(albumNavCapsule, 'commentary'),
           }),
         ]};
 
@@ -94,8 +97,8 @@ export default {
           {href: '#', 'data-random': 'track-in-sidebar'},
 
           (data.isTrackPage
-            ? language.$('trackPage.nav.random')
-            : language.$('albumPage.nav.randomTrack')));
+            ? language.$(trackNavCapsule, 'random')
+            : language.$(albumNavCapsule, 'randomTrack')));
 
     const allLinks = [
       ...previousNextLinks,
diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js
index 6fc1375b..28227f45 100644
--- a/src/content/dependencies/generateAlbumReleaseInfo.js
+++ b/src/content/dependencies/generateAlbumReleaseInfo.js
@@ -23,11 +23,9 @@ export default {
     relations.bannerArtistContributionsLine =
       relation('generateReleaseInfoContributionsLine', album.bannerArtistContribs);
 
-    if (!empty(album.urls)) {
-      relations.externalLinks =
-        album.urls.map(url =>
-          relation('linkExternal', url));
-    }
+    relations.externalLinks =
+      album.urls.map(url =>
+        relation('linkExternal', url));
 
     return relations;
   },
@@ -43,55 +41,77 @@ export default {
       data.coverArtDate = album.coverArtDate;
     }
 
-    data.duration = accumulateSum(album.tracks, track => track.duration);
-    data.durationApproximate = album.tracks.length > 1;
+    const durationTerms =
+      album.tracks
+        .map(track => track.duration)
+        .filter(value => value > 0);
+
+    if (empty(durationTerms)) {
+      data.duration = null;
+      data.durationApproximate = null;
+    } else {
+      data.duration = accumulateSum(durationTerms);
+      data.durationApproximate = album.tracks.length > 1;
+    }
 
     data.numTracks = album.tracks.length;
 
     return data;
   },
 
-  generate(data, relations, {html, language}) {
-    return html.tags([
-      html.tag('p',
-        {[html.onlyIfContent]: true},
-        {[html.joinChildren]: html.tag('br')},
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('releaseInfo', capsule =>
+      html.tags([
+        html.tag('p',
+          {[html.onlyIfContent]: true},
+          {[html.joinChildren]: html.tag('br')},
 
-        [
-          relations.artistContributionsLine
-            .slots({stringKey: 'releaseInfo.by'}),
+          [
+            relations.artistContributionsLine.slots({
+              stringKey: capsule + '.by',
+              chronologyKind: 'album',
+            }),
 
-          relations.coverArtistContributionsLine
-            .slots({stringKey: 'releaseInfo.coverArtBy'}),
+            relations.coverArtistContributionsLine.slots({
+              stringKey: capsule + '.coverArtBy',
+              chronologyKind: 'coverArt',
+            }),
 
-          relations.wallpaperArtistContributionsLine
-            .slots({stringKey: 'releaseInfo.wallpaperArtBy'}),
+            relations.wallpaperArtistContributionsLine.slots({
+              stringKey: capsule + '.wallpaperArtBy',
+              chronologyKind: 'wallpaperArt',
+            }),
 
-          relations.bannerArtistContributionsLine
-            .slots({stringKey: 'releaseInfo.bannerArtBy'}),
+            relations.bannerArtistContributionsLine.slots({
+              stringKey: capsule + '.bannerArtBy',
+              chronologyKind: 'bannerArt',
+            }),
 
-          data.date &&
-            language.$('releaseInfo.released', {
+            language.$(capsule, 'released', {
+              [language.onlyIfOptions]: ['date'],
               date: language.formatDate(data.date),
             }),
 
-          data.coverArtDate &&
-            language.$('releaseInfo.artReleased', {
+            language.$(capsule, 'artReleased', {
+              [language.onlyIfOptions]: ['date'],
               date: language.formatDate(data.coverArtDate),
             }),
 
-          data.duration &&
-            language.$('releaseInfo.duration', {
+            language.$(capsule, 'duration', {
+              [language.onlyIfOptions]: ['duration'],
               duration:
                 language.formatDuration(data.duration, {
                   approximate: data.durationApproximate,
                 }),
             }),
-        ]),
+          ]),
 
-      relations.externalLinks &&
         html.tag('p',
-          language.$('releaseInfo.listenOn', {
+          {[html.onlyIfContent]: true},
+
+          language.$(capsule, 'listenOn', {
+            [language.onlyIfOptions]: ['links'],
+
             links:
               language.formatDisjunctionList(
                 relations.externalLinks
@@ -105,6 +125,5 @@ export default {
                         : 'albumMultipleTracks'),
                     ]))),
           })),
-    ]);
-  },
+      ])),
 };
diff --git a/src/content/dependencies/generateAlbumSidebarGroupBox.js b/src/content/dependencies/generateAlbumSidebarGroupBox.js
index 00a96c31..f3be74f7 100644
--- a/src/content/dependencies/generateAlbumSidebarGroupBox.js
+++ b/src/content/dependencies/generateAlbumSidebarGroupBox.js
@@ -1,5 +1,5 @@
 import {sortChronologically} from '#sort';
-import {atOffset, empty} from '#sugar';
+import {atOffset} from '#sugar';
 
 export default {
   contentDependencies: [
@@ -77,40 +77,50 @@ export default {
   },
 
   generate: (relations, slots, {html, language}) =>
-    relations.box.slots({
-      attributes: {class: 'individual-group-sidebar-box'},
-      content: [
-        html.tag('h1',
-          language.$('albumSidebar.groupBox.title', {
-            group: relations.groupLink,
-          })),
-
-        slots.mode === 'album' &&
-          relations.description
-            ?.slot('mode', 'multiline'),
-
-        !empty(relations.externalLinks) &&
+    language.encapsulate('albumSidebar.groupBox', boxCapsule =>
+      relations.box.slots({
+        attributes: {class: 'individual-group-sidebar-box'},
+        content: [
+          html.tag('h1',
+            language.$(boxCapsule, 'title', {
+              group: relations.groupLink,
+            })),
+
+          slots.mode === 'album' &&
+            relations.description
+              ?.slot('mode', 'multiline'),
+
           html.tag('p',
+            {[html.onlyIfContent]: true},
+
             language.$('releaseInfo.visitOn', {
+              [language.onlyIfOptions]: ['links'],
+
               links:
                 language.formatDisjunctionList(
                   relations.externalLinks
                     .map(link => link.slot('context', 'group'))),
             })),
 
-        slots.mode === 'album' &&
-        relations.nextAlbumLink &&
-          html.tag('p', {class: 'group-chronology-link'},
-            language.$('albumSidebar.groupBox.next', {
-              album: relations.nextAlbumLink,
-            })),
+          slots.mode === 'album' &&
+            html.tag('p', {class: 'group-chronology-link'},
+              {[html.onlyIfContent]: true},
 
-        slots.mode === 'album' &&
-        relations.previousAlbumLink &&
-          html.tag('p', {class: 'group-chronology-link'},
-            language.$('albumSidebar.groupBox.previous', {
-              album: relations.previousAlbumLink,
-            })),
-      ],
-    }),
+              language.$(boxCapsule, 'next', {
+                [language.onlyIfOptions]: ['album'],
+
+                album: relations.nextAlbumLink,
+              })),
+
+          slots.mode === 'album' &&
+            html.tag('p', {class: 'group-chronology-link'},
+              {[html.onlyIfContent]: true},
+
+              language.$(boxCapsule, 'previous', {
+                [language.onlyIfOptions]: ['album'],
+
+                album: relations.previousAlbumLink,
+              })),
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js
index aa5c723d..d0c46060 100644
--- a/src/content/dependencies/generateAlbumSidebarTrackSection.js
+++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js
@@ -55,10 +55,12 @@ export default {
   },
 
   generate(data, relations, slots, {getColors, html, language}) {
+    const capsule = language.encapsulate('albumSidebar.trackList');
+
     const sectionName =
       html.tag('span', {class: 'group-name'},
         (data.isDefaultTrackSection
-          ? language.$('albumSidebar.trackList.fallbackSectionName')
+          ? language.$(capsule, 'fallbackSectionName')
           : data.name));
 
     let colorStyle;
@@ -78,7 +80,7 @@ export default {
           data.tracksAreMissingCommentary[index] &&
             {class: 'no-commentary'},
 
-          language.$('albumSidebar.trackList.item', {
+          language.$(capsule, 'item', {
             track:
               (slots.mode === 'commentary' && data.tracksAreMissingCommentary[index]
                 ? trackLink.slots({
@@ -117,14 +119,17 @@ export default {
           colorStyle,
 
           html.tag('span',
-            (data.hasTrackNumbers
-              ? language.$('albumSidebar.trackList.group.withRange', {
-                  group: sectionName,
-                  range: `${data.firstTrackNumber}–${data.lastTrackNumber}`
-                })
-              : language.$('albumSidebar.trackList.group', {
-                  group: sectionName,
-                })))),
+            language.encapsulate(capsule, 'group', workingCapsule => {
+              const workingOptions = {group: sectionName};
+
+              if (data.hasTrackNumbers) {
+                workingCapsule += '.withRange';
+                workingOptions.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 c8b123fe..7500109e 100644
--- a/src/content/dependencies/generateAlbumSocialEmbed.js
+++ b/src/content/dependencies/generateAlbumSocialEmbed.js
@@ -41,34 +41,34 @@ export default {
     return data;
   },
 
-  generate(data, relations, {absoluteTo, language, urls}) {
-    return relations.socialEmbed.slots({
-      title:
-        language.$('albumPage.socialEmbed.title', {
-          album: data.albumName,
-        }),
-
-      description: relations.description,
-
-      headingContent:
-        (data.hasHeading
-          ? language.$('albumPage.socialEmbed.heading', {
-              group: data.headingGroupName,
-            })
-          : null),
-
-      headingLink:
-        (data.hasHeading
-          ? absoluteTo('localized.groupGallery', data.headingGroupDirectory)
-          : null),
-
-      imagePath:
-        (data.hasImage
-          ? '/' +
-            urls
-              .from('shared.root')
-              .to('media.albumCover', data.coverArtDirectory, data.coverArtFileExtension)
-          : null),
-    });
-  },
+  generate: (data, relations, {absoluteTo, language, urls}) =>
+    language.encapsulate('albumPage.socialEmbed', embedCapsule =>
+      relations.socialEmbed.slots({
+        title:
+          language.$(embedCapsule, 'title', {
+            album: data.albumName,
+          }),
+
+        description: relations.description,
+
+        headingContent:
+          (data.hasHeading
+            ? language.$(embedCapsule, 'heading', {
+                group: data.headingGroupName,
+              })
+            : null),
+
+        headingLink:
+          (data.hasHeading
+            ? absoluteTo('localized.groupGallery', data.headingGroupDirectory)
+            : null),
+
+        imagePath:
+          (data.hasImage
+            ? '/' +
+              urls
+                .from('shared.root')
+                .to('media.albumCover', data.coverArtDirectory, data.coverArtFileExtension)
+            : null),
+      })),
 };
diff --git a/src/content/dependencies/generateAlbumTrackList.js b/src/content/dependencies/generateAlbumTrackList.js
index ee06b9e6..a3435bea 100644
--- a/src/content/dependencies/generateAlbumTrackList.js
+++ b/src/content/dependencies/generateAlbumTrackList.js
@@ -147,21 +147,30 @@ export default {
               durationApproximate,
               startIndex,
             }) => [
-              heading.slots({
-                tag: 'dt',
-                title:
-                  (duration === 0
-                    ? language.$('trackList.section', {
-                        section: name,
-                      })
-                    : language.$('trackList.section.withDuration', {
-                        section: name,
-                        duration:
+              language.encapsulate('trackList.section', capsule =>
+                heading.slots({
+                  tag: 'dt',
+
+                  title:
+                    language.encapsulate(capsule, capsule => {
+                      const options = {section: name};
+
+                      if (duration !== 0) {
+                        capsule += '.withDuration';
+                        options.duration =
                           language.formatDuration(duration, {
                             approximate: durationApproximate,
-                          }),
-                      })),
-              }),
+                          });
+                      }
+
+                      return language.$(capsule, options);
+                    }),
+
+                  stickyTitle:
+                    language.$(capsule, 'sticky', {
+                      section: name,
+                    }),
+                })),
 
               html.tag('dd',
                 html.tag(listTag,
diff --git a/src/content/dependencies/generateAlbumTrackListItem.js b/src/content/dependencies/generateAlbumTrackListItem.js
index 7190fb4c..7d5d2c6e 100644
--- a/src/content/dependencies/generateAlbumTrackListItem.js
+++ b/src/content/dependencies/generateAlbumTrackListItem.js
@@ -80,54 +80,51 @@ export default {
     },
   },
 
-  generate(data, relations, slots, {getColors, html, language}) {
-    let colorStyle;
-    if (data.color) {
-      const {primary} = getColors(data.color);
-      colorStyle = {style: `--primary-color: ${primary}`};
-    }
-
-    const parts = ['trackList.item'];
-    const options = {};
-
-    options.track =
-      relations.trackLink
-        .slot('color', false);
-
-    const collapseDuration =
-      (slots.collapseDurationScope === 'track'
-        ? !data.trackHasDuration
-     : slots.collapseDurationScope === 'section'
-        ? !data.sectionHasDuration
-     : slots.collapseDurationScope === 'album'
-        ? !data.albumHasDuration
-        : false);
-
-    if (!collapseDuration) {
-      parts.push('withDuration');
-
-      options.duration =
-        (data.trackHasDuration
-          ? language.$('trackList.item.withDuration.duration', {
-              duration:
-                language.formatDuration(data.duration),
-            })
-          : relations.missingDuration);
-    }
-
-    if (data.showArtists) {
-      parts.push('withArtists');
-      options.by =
-        html.tag('span', {class: 'by'},
-          html.metatag('chunkwrap', {split: ','},
-            html.resolve(
-              language.$('trackList.item.withArtists.by', {
-                artists: language.formatConjunctionList(relations.contributionLinks),
-              }))));
-    }
-
-    return html.tag('li',
-      colorStyle,
-      language.formatString(...parts, options));
-  },
+  generate: (data, relations, slots, {getColors, html, language}) =>
+    language.encapsulate('trackList.item', itemCapsule =>
+      html.tag('li',
+        data.color &&
+          {style: `--primary-color: ${getColors(data.color).primary}`},
+
+        language.encapsulate(itemCapsule, workingCapsule => {
+          const workingOptions = {};
+
+          workingOptions.track =
+            relations.trackLink
+              .slot('color', false);
+
+          const collapseDuration =
+            (slots.collapseDurationScope === 'track'
+              ? !data.trackHasDuration
+           : slots.collapseDurationScope === 'section'
+              ? !data.sectionHasDuration
+           : slots.collapseDurationScope === 'album'
+              ? !data.albumHasDuration
+              : false);
+
+          if (!collapseDuration) {
+            workingCapsule += '.withDuration';
+            workingOptions.duration =
+              (data.trackHasDuration
+                ? language.$(itemCapsule, 'withDuration.duration', {
+                    duration:
+                      language.formatDuration(data.duration),
+                  })
+                : relations.missingDuration);
+          }
+
+          if (data.showArtists) {
+            workingCapsule += '.withArtists';
+            workingOptions.by =
+              html.tag('span', {class: 'by'},
+                html.metatag('chunkwrap', {split: ','},
+                  html.resolve(
+                    language.$(itemCapsule, 'withArtists.by', {
+                      artists:
+                        language.formatConjunctionList(relations.contributionLinks),
+                    }))));
+          }
+
+          return language.$(workingCapsule, workingOptions);
+        }))),
 };
diff --git a/src/content/dependencies/generateAlbumTrackListMissingDuration.js b/src/content/dependencies/generateAlbumTrackListMissingDuration.js
index 6d4a6ec8..b5917982 100644
--- a/src/content/dependencies/generateAlbumTrackListMissingDuration.js
+++ b/src/content/dependencies/generateAlbumTrackListMissingDuration.js
@@ -11,23 +11,25 @@ export default {
   }),
 
   generate: (relations, {html, language}) =>
-    relations.textWithTooltip.slots({
-      attributes: {class: 'missing-duration'},
-      customInteractionCue: true,
+    language.encapsulate('trackList.item.withDuration', itemCapsule =>
+      language.encapsulate(itemCapsule, 'duration', durationCapsule =>
+        relations.textWithTooltip.slots({
+          attributes: {class: 'missing-duration'},
+          customInteractionCue: true,
 
-      text:
-        language.$('trackList.item.withDuration.duration', {
-          duration:
-            html.tag('span', {class: 'text-with-tooltip-interaction-cue'},
-              language.$('trackList.item.withDuration.duration.missing')),
-        }),
+          text:
+            language.$(durationCapsule, {
+              duration:
+                html.tag('span', {class: 'text-with-tooltip-interaction-cue'},
+                  language.$(durationCapsule, 'missing')),
+            }),
 
-      tooltip:
-        relations.tooltip.slots({
-          attributes: {class: 'missing-duration-tooltip'},
+          tooltip:
+            relations.tooltip.slots({
+              attributes: {class: 'missing-duration-tooltip'},
 
-          content:
-            language.$('trackList.item.withDuration.duration.missing.info'),
-        }),
-    }),
+              content:
+                language.$(durationCapsule, 'missing.info'),
+            }),
+        }))),
 };
diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js
index eae48f05..c51faeba 100644
--- a/src/content/dependencies/generateArtTagGalleryPage.js
+++ b/src/content/dependencies/generateArtTagGalleryPage.js
@@ -85,11 +85,11 @@ export default {
     return data;
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout
-      .slots({
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('tagPage', pageCapsule =>
+      relations.layout.slots({
         title:
-          language.$('tagPage.title', {
+          language.$(pageCapsule, 'title', {
             tag: data.name,
           }),
 
@@ -100,7 +100,7 @@ export default {
         mainClasses: ['top-index'],
         mainContent: [
           html.tag('p', {class: 'quick-info'},
-            language.$('tagPage.infoLine', {
+            language.$(pageCapsule, 'infoLine', {
               coverArts: language.countCoverArts(data.numArtworks, {
                 unit: true,
               }),
@@ -143,11 +143,10 @@ export default {
 
           {
             html:
-              language.$('tagPage.nav.tag', {
+              language.$(pageCapsule, 'nav.tag', {
                 tag: relations.artTagMainLink,
               }),
           },
         ],
-      });
-  },
+      })),
 };
diff --git a/src/content/dependencies/generateArtistGalleryPage.js b/src/content/dependencies/generateArtistGalleryPage.js
index db8f123f..28f06a21 100644
--- a/src/content/dependencies/generateArtistGalleryPage.js
+++ b/src/content/dependencies/generateArtistGalleryPage.js
@@ -14,10 +14,12 @@ export default {
   extraDependencies: ['html', 'language'],
 
   query(artist) {
-    const things = [
-      ...artist.albumsAsCoverArtist,
-      ...artist.tracksAsCoverArtist,
-    ];
+    const things =
+      ([
+        artist.albumCoverArtistContributions,
+        artist.trackCoverArtistContributions,
+      ]).flat()
+        .map(({thing}) => thing);
 
     sortAlbumsTracksChronologically(things, {
       latestFirst: true,
@@ -82,11 +84,11 @@ export default {
     return data;
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout
-      .slots({
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('artistGalleryPage', pageCapsule =>
+      relations.layout.slots({
         title:
-          language.$('artistGalleryPage.title', {
+          language.$(pageCapsule, 'title', {
             artist: data.name,
           }),
 
@@ -95,10 +97,11 @@ export default {
         mainClasses: ['top-index'],
         mainContent: [
           html.tag('p', {class: 'quick-info'},
-            language.$('artistGalleryPage.infoLine', {
-              coverArts: language.countCoverArts(data.numArtworks, {
-                unit: true,
-              }),
+            language.$(pageCapsule, 'infoLine', {
+              coverArts:
+                language.countCoverArts(data.numArtworks, {
+                  unit: true,
+                }),
             })),
 
           relations.coverGrid
@@ -117,6 +120,7 @@ export default {
                       dimensions,
                     })),
 
+              // TODO: Can this be [language.onlyIfOptions]?
               info:
                 data.otherCoverArtists.map(names =>
                   (names === null
@@ -135,6 +139,5 @@ export default {
               currentExtra: 'gallery',
             })
             .content,
-      })
-  },
+      })),
 }
diff --git a/src/content/dependencies/generateArtistGroupContributionsInfo.js b/src/content/dependencies/generateArtistGroupContributionsInfo.js
index 1725d4b9..f84d00de 100644
--- a/src/content/dependencies/generateArtistGroupContributionsInfo.js
+++ b/src/content/dependencies/generateArtistGroupContributionsInfo.js
@@ -131,94 +131,104 @@ export default {
     countUnit: {validate: v => v.is('tracks', 'artworks')},
   },
 
-  generate(data, relations, slots, {html, language}) {
-    if (slots.sort === 'count' && empty(relations.groupLinksSortedByCount)) {
-      return html.blank();
-    } else if (slots.sort === 'duration' && empty(relations.groupLinksSortedByDuration)) {
-      return html.blank();
-    }
+  generate: (data, relations, slots, {html, language}) =>
+    language.encapsulate('artistPage.groupContributions', capsule => {
+      if (slots.sort === 'count' && empty(relations.groupLinksSortedByCount)) {
+        return html.blank();
+      } else if (slots.sort === 'duration' && empty(relations.groupLinksSortedByDuration)) {
+        return html.blank();
+      }
 
-    const getCounts = counts =>
-      counts.map(count => {
-        switch (slots.countUnit) {
-          case 'tracks': return language.countTracks(count, {unit: true});
-          case 'artworks': return language.countArtworks(count, {unit: true});
-        }
-      });
-
-    // We aren't displaying the "~" approximate symbol here for now.
-    // The general notion that these sums aren't going to be 100% accurate
-    // is made clear by the "XYZ has contributed ~1:23:45 hours of music..."
-    // line that's always displayed above this table.
-    const getDurations = (durations, approximate) =>
-      stitchArrays({
-        duration: durations,
-        approximate: approximate,
-      }).map(({duration}) => language.formatDuration(duration));
-
-    const topLevelClasses = [
-      'group-contributions-sorted-by-' + slots.sort,
-      slots.visible && 'visible',
-    ];
-
-    return html.tags([
-      html.tag('dt', {class: topLevelClasses},
-        (slots.showSortButton
-          ? language.$('artistPage.groupContributions.title.withSortButton', {
-              title: slots.title,
-              sort:
-                html.tag('a', {class: 'group-contributions-sort-button'},
-                  {href: '#'},
-
-                  (slots.sort === 'count'
-                    ? language.$('artistPage.groupContributions.title.sorting.count')
-                    : language.$('artistPage.groupContributions.title.sorting.duration'))),
-            })
-          : slots.title)),
-
-      html.tag('dd', {class: topLevelClasses},
-        html.tag('ul', {class: 'group-contributions-table'},
-          {role: 'list'},
-
-          (slots.sort === 'count'
-            ? stitchArrays({
-                group: relations.groupLinksSortedByCount,
-                count: getCounts(data.groupCountsSortedByCount),
-                duration:
-                  getDurations(
-                    data.groupDurationsSortedByCount,
-                    data.groupDurationsApproximateSortedByCount),
-              }).map(({group, count, duration}) =>
-                  html.tag('li',
-                    html.tag('div', {class: 'group-contributions-row'}, [
-                      group,
-                      html.tag('span', {class: 'group-contributions-metrics'},
-                        // When sorting by count, duration details aren't necessarily
-                        // available for all items.
-                        (slots.showBothColumns && duration
-                          ? language.$('artistPage.groupContributions.item.countDurationAccent', {count, duration})
-                          : language.$('artistPage.groupContributions.item.countAccent', {count}))),
-                    ])))
-
-            : stitchArrays({
-                group: relations.groupLinksSortedByDuration,
-                count: getCounts(data.groupCountsSortedByDuration),
-                duration:
-                  getDurations(
-                    data.groupDurationsSortedByDuration,
-                    data.groupDurationsApproximateSortedByDuration),
-              }).map(({group, count, duration}) =>
-                  html.tag('li',
-                    html.tag('div', {class: 'group-contributions-row'}, [
-                      group,
-                      html.tag('span', {class: 'group-contributions-metrics'},
-                        // Count details are always available, since they're just the
-                        // number of contributions directly. And duration details are
-                        // guaranteed for every item when sorting by duration.
-                        (slots.showBothColumns
-                          ? language.$('artistPage.groupContributions.item.durationCountAccent', {duration, count})
-                          : language.$('artistPage.groupContributions.item.durationAccent', {duration}))),
-                    ])))))),
-    ]);
-  },
+      const getCounts = counts =>
+        counts.map(count => {
+          switch (slots.countUnit) {
+            case 'tracks': return language.countTracks(count, {unit: true});
+            case 'artworks': return language.countArtworks(count, {unit: true});
+          }
+        });
+
+      // We aren't displaying the "~" approximate symbol here for now.
+      // The general notion that these sums aren't going to be 100% accurate
+      // is made clear by the "XYZ has contributed ~1:23:45 hours of music..."
+      // line that's always displayed above this table.
+      const getDurations = (durations, approximate) =>
+        stitchArrays({
+          duration: durations,
+          approximate: approximate,
+        }).map(({duration}) => language.formatDuration(duration));
+
+      const topLevelClasses = [
+        'group-contributions-sorted-by-' + slots.sort,
+        slots.visible && 'visible',
+      ];
+
+      // TODO: It feels pretty awkward that this component is the only one that
+      // has enough knowledge to decide if the sort button is even applicable...
+      const switchingSortPossible =
+        !empty(relations.groupLinksSortedByCount) &&
+        !empty(relations.groupLinksSortedByDuration);
+
+      return html.tags([
+        html.tag('dt', {class: topLevelClasses},
+          language.encapsulate(capsule, 'title', capsule =>
+            (switchingSortPossible && slots.showSortButton
+              ? language.$(capsule, 'withSortButton', {
+                  title: slots.title,
+                  sort:
+                    html.tag('a', {class: 'group-contributions-sort-button'},
+                      {href: '#'},
+
+                      (slots.sort === 'count'
+                        ? language.$(capsule, 'sorting.count')
+                        : language.$(capsule, 'sorting.duration'))),
+                })
+              : slots.title))),
+
+        html.tag('dd', {class: topLevelClasses},
+          html.tag('ul', {class: 'group-contributions-table'},
+            {role: 'list'},
+
+            (slots.sort === 'count'
+              ? stitchArrays({
+                  group: relations.groupLinksSortedByCount,
+                  count: getCounts(data.groupCountsSortedByCount),
+                  duration:
+                    getDurations(
+                      data.groupDurationsSortedByCount,
+                      data.groupDurationsApproximateSortedByCount),
+                }).map(({group, count, duration}) =>
+                    language.encapsulate(capsule, 'item', capsule =>
+                      html.tag('li',
+                        html.tag('div', {class: 'group-contributions-row'}, [
+                          group,
+                          html.tag('span', {class: 'group-contributions-metrics'},
+                            // When sorting by count, duration details aren't necessarily
+                            // available for all items.
+                            (slots.showBothColumns && duration
+                              ? language.$(capsule, 'countDurationAccent', {count, duration})
+                              : language.$(capsule, 'countAccent', {count}))),
+                        ]))))
+
+              : stitchArrays({
+                  group: relations.groupLinksSortedByDuration,
+                  count: getCounts(data.groupCountsSortedByDuration),
+                  duration:
+                    getDurations(
+                      data.groupDurationsSortedByDuration,
+                      data.groupDurationsApproximateSortedByDuration),
+                }).map(({group, count, duration}) =>
+                    language.encapsulate(capsule, 'item', capsule =>
+                      html.tag('li',
+                        html.tag('div', {class: 'group-contributions-row'}, [
+                          group,
+                          html.tag('span', {class: 'group-contributions-metrics'},
+                            // Count details are always available, since they're just the
+                            // number of contributions directly. And duration details are
+                            // guaranteed for every item when sorting by duration.
+                            (slots.showBothColumns
+                              ? language.$(capsule, 'durationCountAccent', {duration, count})
+                              : language.$(capsule, 'durationAccent', {duration}))),
+                        ]))))))),
+      ]);
+    }),
 };
diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js
index ac9209a7..f9ce7e3b 100644
--- a/src/content/dependencies/generateArtistInfoPage.js
+++ b/src/content/dependencies/generateArtistInfoPage.js
@@ -1,5 +1,4 @@
 import {empty, unique} from '#sugar';
-import {getTotalDuration} from '#wiki-data';
 
 export default {
   contentDependencies: [
@@ -12,131 +11,112 @@ export default {
     'generateContentHeading',
     'generateCoverArtwork',
     'generatePageLayout',
-    'linkAlbum',
     'linkArtistGallery',
     'linkExternal',
-    'linkGroup',
-    'linkTrack',
     'transformContent',
   ],
 
-  extraDependencies: ['html', 'language', 'wikiData'],
-
-  sprawl({wikiInfo}) {
-    return {
-      enableFlashesAndGames: wikiInfo.enableFlashesAndGames,
-    };
-  },
-
-  query(sprawl, artist) {
-    return {
-      // Even if an artist has served as both "artist" (compositional) and
-      // "contributor" (instruments, production, etc) on the same track, that
-      // track only counts as one unique contribution.
-      allTracks:
-        unique([...artist.tracksAsArtist, ...artist.tracksAsContributor]),
-
-      // 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: [
-        ...artist.albumsAsCoverArtist,
-        ...artist.albumsAsWallpaperArtist,
-        ...artist.albumsAsBannerArtist,
-        ...artist.tracksAsCoverArtist,
-      ],
-
-      // Banners and wallpapers don't show up in the artist gallery page, only
-      // cover art.
-      hasGallery:
-        !empty(artist.albumsAsCoverArtist) ||
-        !empty(artist.tracksAsCoverArtist),
-    };
-  },
-
-  relations(relation, query, sprawl, artist) {
-    const relations = {};
-    const sections = relations.sections = {};
-
-    relations.layout =
-      relation('generatePageLayout');
-
-    relations.artistNavLinks =
-      relation('generateArtistNavLinks', artist);
-
-    if (artist.hasAvatar) {
-      relations.cover =
-        relation('generateCoverArtwork', []);
-    }
-
-    if (artist.contextNotes) {
-      const contextNotes = sections.contextNotes = {};
-      contextNotes.content = relation('transformContent', artist.contextNotes);
-    }
-
-    if (!empty(artist.urls)) {
-      const visit = sections.visit = {};
-      visit.externalLinks =
-        artist.urls.map(url =>
-          relation('linkExternal', url));
-    }
-
-    if (!empty(query.allTracks)) {
-      const tracks = sections.tracks = {};
-      tracks.heading = relation('generateContentHeading');
-      tracks.list = relation('generateArtistInfoPageTracksChunkedList', artist);
-      tracks.groupInfo = relation('generateArtistGroupContributionsInfo', query.allTracks);
-    }
-
-    if (!empty(query.allArtworks)) {
-      const artworks = sections.artworks = {};
-      artworks.heading = relation('generateContentHeading');
-      artworks.list = relation('generateArtistInfoPageArtworksChunkedList', artist);
-      artworks.groupInfo =
-        relation('generateArtistGroupContributionsInfo', query.allArtworks);
-
-      if (query.hasGallery) {
-        artworks.artistGalleryLink =
-          relation('linkArtistGallery', artist);
-      }
-    }
-
-    if (sprawl.enableFlashesAndGames && !empty(artist.flashesAsContributor)) {
-      const flashes = sections.flashes = {};
-      flashes.heading = relation('generateContentHeading');
-      flashes.list = relation('generateArtistInfoPageFlashesChunkedList', artist);
-    }
-
-    if (!empty(artist.albumsAsCommentator) || !empty(artist.tracksAsCommentator)) {
-      const commentary = sections.commentary = {};
-      commentary.heading = relation('generateContentHeading');
-      commentary.list = relation('generateArtistInfoPageCommentaryChunkedList', artist);
-    }
-
-    return relations;
-  },
-
-  data(query, sprawl, artist) {
-    const data = {};
-
-    data.name = artist.name;
-    data.directory = artist.directory;
-
-    if (artist.hasAvatar) {
-      data.avatarFileExtension = artist.avatarFileExtension;
-    }
-
-    data.totalTrackCount = query.allTracks.length;
-    data.totalDuration = getTotalDuration(query.allTracks, {originalReleasesOnly: true});
-
-    return data;
-  },
-
-  generate(data, relations, {html, language}) {
-    const {sections: sec} = relations;
-
-    return relations.layout
-      .slots({
+  extraDependencies: ['html', 'language'],
+
+  query: (artist) => ({
+    // Even if an artist has served as both "artist" (compositional) and
+    // "contributor" (instruments, production, etc) on the same track, that
+    // track only counts as one unique contribution in the list.
+    allTracks:
+      unique(
+        ([
+          artist.trackArtistContributions,
+          artist.trackContributorContributions,
+        ]).flat()
+          .map(({thing}) => thing)),
+
+    // 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:
+      ([
+        artist.albumCoverArtistContributions,
+        artist.albumWallpaperArtistContributions,
+        artist.albumBannerArtistContributions,
+        artist.trackCoverArtistContributions,
+      ]).flat()
+        .map(({thing}) => thing),
+
+    // Banners and wallpapers don't show up in the artist gallery page, only
+    // cover art.
+    hasGallery:
+      !empty(artist.albumCoverArtistContributions) ||
+      !empty(artist.trackCoverArtistContributions),
+  }),
+
+  relations: (relation, query, artist) => ({
+    layout:
+      relation('generatePageLayout'),
+
+    artistNavLinks:
+      relation('generateArtistNavLinks', artist),
+
+    cover:
+      (artist.hasAvatar
+        ? relation('generateCoverArtwork', [])
+        : null),
+
+    contentHeading:
+      relation('generateContentHeading'),
+
+    contextNotes:
+      relation('transformContent', artist.contextNotes),
+
+    visitLinks:
+      artist.urls
+        .map(url => relation('linkExternal', url)),
+
+    tracksChunkedList:
+      relation('generateArtistInfoPageTracksChunkedList', artist),
+
+    tracksGroupInfo:
+      relation('generateArtistGroupContributionsInfo', query.allTracks),
+
+    artworksChunkedList:
+      relation('generateArtistInfoPageArtworksChunkedList', artist),
+
+    artworksGroupInfo:
+      relation('generateArtistGroupContributionsInfo', query.allArtworks),
+
+    artistGalleryLink:
+      (query.hasGallery
+        ? relation('linkArtistGallery', artist)
+        : null),
+
+    flashesChunkedList:
+      relation('generateArtistInfoPageFlashesChunkedList', artist),
+
+    commentaryChunkedList:
+      relation('generateArtistInfoPageCommentaryChunkedList', artist),
+  }),
+
+  data: (query, artist) => ({
+    name:
+      artist.name,
+
+    directory:
+      artist.directory,
+
+    avatarFileExtension:
+      (artist.hasAvatar
+        ? artist.avatarFileExtension
+        : null),
+
+    totalTrackCount:
+      query.allTracks.length,
+
+    totalDuration:
+      artist.totalDuration,
+  }),
+
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('artistPage', pageCapsule =>
+      relations.layout.slots({
         title: data.name,
         headingMode: 'sticky',
 
@@ -152,67 +132,85 @@ export default {
             : null),
 
         mainContent: [
-          sec.contextNotes && [
-            html.tag('p', language.$('releaseInfo.note')),
+          html.tags([
+            html.tag('p',
+              {[html.onlyIfSiblings]: true},
+              language.$('releaseInfo.note')),
+
             html.tag('blockquote',
-              sec.contextNotes.content),
-          ],
+              {[html.onlyIfContent]: true},
+              relations.contextNotes),
+          ]),
 
-          sec.visit &&
-            html.tag('p',
-              language.$('releaseInfo.visitOn', {
-                links:
-                  language.formatDisjunctionList(
-                    sec.visit.externalLinks
-                      .map(link => link.slot('context', 'artist'))),
-              })),
-
-          sec.artworks?.artistGalleryLink &&
-            html.tag('p',
-              language.$('artistPage.viewArtGallery', {
-                link: sec.artworks.artistGalleryLink.slots({
-                  content: language.$('artistPage.viewArtGallery.link'),
-                }),
-              })),
+          html.tag('p',
+            {[html.onlyIfContent]: true},
 
-          (sec.tracks || sec.artworsk || sec.flashes || sec.commentary) &&
-            html.tag('p',
-              language.$('misc.jumpTo.withLinks', {
-                links: language.formatUnitList(
-                  [
-                    sec.tracks &&
-                      html.tag('a',
-                        {href: '#tracks'},
-                        language.$('artistPage.trackList.title')),
-
-                    sec.artworks &&
-                      html.tag('a',
-                        {href: '#art'},
-                        language.$('artistPage.artList.title')),
-
-                    sec.flashes &&
-                      html.tag('a',
-                        {href: '#flashes'},
-                        language.$('artistPage.flashList.title')),
-
-                    sec.commentary &&
-                      html.tag('a',
-                        {href: '#commentary'},
-                        language.$('artistPage.commentaryList.title')),
-                  ].filter(Boolean)),
-              })),
-
-          sec.tracks && [
-            sec.tracks.heading
+            language.$('releaseInfo.visitOn', {
+              [language.onlyIfOptions]: ['links'],
+
+              links:
+                language.formatDisjunctionList(
+                  relations.visitLinks
+                    .map(link => link.slot('context', 'artist'))),
+            })),
+
+          html.tag('p',
+            {[html.onlyIfContent]: true},
+
+            language.encapsulate(pageCapsule, 'viewArtGallery', capsule =>
+              language.$(capsule, {
+                [language.onlyIfOptions]: ['link'],
+
+                link:
+                  relations.artistGalleryLink?.slots({
+                    content:
+                      language.$(capsule, 'link'),
+                  }),
+              }))),
+
+          html.tag('p',
+            {[html.onlyIfContent]: true},
+
+            language.$('misc.jumpTo.withLinks', {
+              [language.onlyIfOptions]: ['links'],
+
+              links:
+                language.formatUnitList([
+                  !html.isBlank(relations.tracksChunkedList) &&
+                    html.tag('a',
+                      {href: '#tracks'},
+                      language.$(pageCapsule, 'trackList.title')),
+
+                  !html.isBlank(relations.artworksChunkedList) &&
+                    html.tag('a',
+                      {href: '#art'},
+                      language.$(pageCapsule, 'artList.title')),
+
+                  !html.isBlank(relations.flashesChunkedList) &&
+                    html.tag('a',
+                      {href: '#flashes'},
+                      language.$(pageCapsule, 'flashList.title')),
+
+                  !html.isBlank(relations.commentaryChunkedList) &&
+                    html.tag('a',
+                      {href: '#commentary'},
+                      language.$(pageCapsule, 'commentaryList.title')),
+                ].filter(Boolean)),
+            })),
+
+          html.tags([
+            relations.contentHeading.clone()
               .slots({
                 tag: 'h2',
-                id: 'tracks',
-                title: language.$('artistPage.trackList.title'),
+                attributes: {id: 'tracks'},
+                title: language.$(pageCapsule, 'trackList.title'),
               }),
 
             data.totalDuration > 0 &&
               html.tag('p',
-                language.$('artistPage.contributedDurationLine', {
+                {[html.onlyIfSiblings]: true},
+
+                language.$(pageCapsule, 'contributedDurationLine', {
                   artist: data.name,
                   duration:
                     language.formatDuration(data.totalDuration, {
@@ -221,82 +219,86 @@ export default {
                     }),
                 })),
 
-            sec.tracks.list
-              .slots({
-                groupInfo: [
-                  sec.tracks.groupInfo
-                    .clone()
+            relations.tracksChunkedList.slots({
+              groupInfo:
+                language.encapsulate(pageCapsule, 'groupContributions', capsule => [
+                  relations.tracksGroupInfo.clone()
                     .slots({
-                      title: language.$('artistPage.groupContributions.title.music'),
+                      title: language.$(capsule, 'title.music'),
                       showSortButton: true,
                       sort: 'count',
                       countUnit: 'tracks',
                       visible: true,
                     }),
 
-                  sec.tracks.groupInfo
-                    .clone()
+                  relations.tracksGroupInfo.clone()
                     .slots({
-                      title: language.$('artistPage.groupContributions.title.music'),
+                      title: language.$(capsule, 'title.music'),
                       showSortButton: true,
                       sort: 'duration',
                       countUnit: 'tracks',
                       visible: false,
                     }),
-                ],
-              }),
-          ],
+                ]),
+            }),
+          ]),
 
-          sec.artworks && [
-            sec.artworks.heading
+          html.tags([
+            relations.contentHeading.clone()
               .slots({
                 tag: 'h2',
-                id: 'art',
-                title: language.$('artistPage.artList.title'),
+                attributes: {id: 'art'},
+                title: language.$(pageCapsule, 'artList.title'),
               }),
 
-            sec.artworks.artistGalleryLink &&
-              html.tag('p',
-                language.$('artistPage.viewArtGallery.orBrowseList', {
-                  link: sec.artworks.artistGalleryLink.slots({
-                    content: language.$('artistPage.viewArtGallery.link'),
-                  }),
-                })),
+            html.tag('p',
+              {[html.onlyIfContent]: true},
 
-            sec.artworks.list
+              language.encapsulate(pageCapsule, 'viewArtGallery', capsule =>
+                language.$(capsule, 'orBrowseList', {
+                  [language.onlyIfOptions]: ['link'],
+
+                  link:
+                    relations.artistGalleryLink?.slots({
+                      content: language.$(capsule, 'link'),
+                    }),
+                }))),
+
+            relations.artworksChunkedList
               .slots({
                 groupInfo:
-                  sec.artworks.groupInfo
-                    .slots({
-                      title: language.$('artistPage.groupContributions.title.artworks'),
-                      showBothColumns: false,
-                      sort: 'count',
-                      countUnit: 'artworks',
-                    }),
+                  language.encapsulate(pageCapsule, 'groupContributions', capsule =>
+                    relations.artworksGroupInfo
+                      .slots({
+                        title: language.$(capsule, 'title.artworks'),
+                        showBothColumns: false,
+                        sort: 'count',
+                        countUnit: 'artworks',
+                      })),
               }),
-          ],
+          ]),
 
-          sec.flashes && [
-            sec.flashes.heading
+          html.tags([
+            relations.contentHeading.clone()
               .slots({
                 tag: 'h2',
-                id: 'flashes',
-                title: language.$('artistPage.flashList.title'),
+                attributes: {id: 'flashes'},
+                title: language.$(pageCapsule, 'flashList.title'),
               }),
 
-            sec.flashes.list,
-          ],
+            relations.flashesChunkedList,
+          ]),
 
-          sec.commentary && [
-            sec.commentary.heading
+          html.tags([
+            relations.contentHeading.clone()
               .slots({
                 tag: 'h2',
-                id: 'commentary',
-                title: language.$('artistPage.commentaryList.title'),
+                attributes: {id: 'commentary'},
+                title: language.$(pageCapsule, 'commentaryList.title'),
               }),
 
-            sec.commentary.list,
-          ],
+            relations.commentaryChunkedList,
+          ]),
         ],
 
         navLinkStyle: 'hierarchical',
@@ -306,6 +308,5 @@ export default {
               showExtraLinks: true,
             })
             .content,
-      });
-  },
+      })),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunk.js b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js
new file mode 100644
index 00000000..2b10df3e
--- /dev/null
+++ b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js
@@ -0,0 +1,34 @@
+export default {
+  contentDependencies: [
+    'generateArtistInfoPageChunk',
+    'generateArtistInfoPageArtworksChunkItem',
+    'linkAlbum',
+  ],
+
+  relations: (relation, album, contribs) => ({
+    template:
+      relation('generateArtistInfoPageChunk'),
+
+    albumLink:
+      relation('linkAlbum', album),
+
+    items:
+      contribs
+        .map(contrib =>
+          relation('generateArtistInfoPageArtworksChunkItem', contrib)),
+  }),
+
+  data: (_album, contribs) => ({
+    dates:
+      contribs
+        .map(contrib => contrib.date),
+  }),
+
+  generate: (data, relations) =>
+    relations.template.slots({
+      mode: 'album',
+      albumLink: relations.albumLink,
+      dates: data.dates,
+      items: relations.items,
+    }),
+};
diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
new file mode 100644
index 00000000..e8d887b1
--- /dev/null
+++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
@@ -0,0 +1,62 @@
+export default {
+  contentDependencies: [
+    'generateArtistInfoPageChunkItem',
+    'generateArtistInfoPageOtherArtistLinks',
+    'linkTrack',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  query: (contrib) => ({
+    kind:
+      (contrib.isBannerArtistContribution
+        ? 'banner'
+     : contrib.isWallpaperArtistContribution
+        ? 'wallpaper'
+     : contrib.isForAlbum
+        ? 'album-cover'
+        : 'track-cover'),
+  }),
+
+  relations: (relation, query, contrib) => ({
+    template:
+      relation('generateArtistInfoPageChunkItem'),
+
+    trackLink:
+      (query.kind === 'track-cover'
+        ? relation('linkTrack', contrib.thing)
+        : null),
+
+    otherArtistLinks:
+      relation('generateArtistInfoPageOtherArtistLinks', [contrib]),
+  }),
+
+  data: (query, contrib) => ({
+    kind:
+      query.kind,
+
+    annotation:
+      contrib.annotation,
+  }),
+
+  generate: (data, relations, {html, language}) =>
+    relations.template.slots({
+      otherArtistLinks: relations.otherArtistLinks,
+
+      annotation: data.annotation,
+
+      content:
+        language.encapsulate('artistPage.creditList.entry', capsule =>
+          (data.kind === 'track-cover'
+            ? language.$(capsule, 'track', {
+                track: relations.trackLink,
+              })
+            : html.tag('i',
+                language.encapsulate(capsule, 'album', capsule =>
+                  (data.kind === 'wallpaper'
+                    ? language.$(capsule, 'wallpaperArt')
+                 : data.kind === 'banner'
+                    ? language.$(capsule, 'bannerArt')
+                    : language.$(capsule, 'coverArt')))))),
+    }),
+};
diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
index 44fb42f2..caefb7a3 100644
--- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
@@ -1,241 +1,58 @@
-import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort';
-import {chunkByProperties, stitchArrays} from '#sugar';
+import {sortAlbumsTracksChronologically, sortContributionsChronologically}
+  from '#sort';
+import {chunkByConditions, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
-    'generateArtistInfoPageChunk',
     'generateArtistInfoPageChunkedList',
-    'generateArtistInfoPageChunkItem',
-    'generateArtistInfoPageOtherArtistLinks',
-    'linkAlbum',
-    'linkTrack',
+    'generateArtistInfoPageArtworksChunk',
   ],
 
-  extraDependencies: ['html', 'language'],
-
   query(artist) {
-    // TODO: Add and integrate wallpaper and banner date fields (#90)
-    // This will probably only happen once all artworks follow a standard
-    // shape (#70) and get their own sorting function. Read for more info:
-    // https://github.com/hsmusic/hsmusic-wiki/issues/90#issuecomment-1607422961
-
-    const processEntry = ({thing, type, track, album, contribs}) => ({
-      thing: thing,
-      entry: {
-        type: type,
-        track: track,
-        album: album,
-        contribs: contribs,
-        date: thing.coverArtDate ?? thing.date,
-      },
-    });
-
-    const processAlbumEntry = ({type, album, contribs}) =>
-      processEntry({
-        thing: album,
-        type: type,
-        track: null,
-        album: album,
-        contribs: contribs,
-      });
-
-    const processTrackEntry = ({type, track, contribs}) =>
-      processEntry({
-        thing: track,
-        type: type,
-        track: track,
-        album: track.album,
-        contribs: contribs,
-      });
-
-    const processAlbumEntries = ({type, albums, contribs}) =>
-      stitchArrays({
-        album: albums,
-        contribs: contribs,
-      }).map(entry =>
-          processAlbumEntry({type, ...entry}));
-
-    const processTrackEntries = ({type, tracks, contribs}) =>
-      stitchArrays({
-        track: tracks,
-        contribs: contribs,
-      }).map(entry =>
-          processTrackEntry({type, ...entry}));
-
-    const {
-      albumsAsCoverArtist,
-      albumsAsWallpaperArtist,
-      albumsAsBannerArtist,
-      tracksAsCoverArtist,
-    } = artist;
+    const query = {};
 
-    const albumsAsCoverArtistContribs =
-      albumsAsCoverArtist
-        .map(album => album.coverArtistContribs);
-
-    const albumsAsWallpaperArtistContribs =
-      albumsAsWallpaperArtist
-        .map(album => album.wallpaperArtistContribs);
-
-    const albumsAsBannerArtistContribs =
-      albumsAsBannerArtist
-        .map(album => album.bannerArtistContribs);
-
-    const tracksAsCoverArtistContribs =
-      tracksAsCoverArtist
-        .map(track => track.coverArtistContribs);
-
-    const albumsAsCoverArtistEntries =
-      processAlbumEntries({
-        type: 'albumCover',
-        albums: albumsAsCoverArtist,
-        contribs: albumsAsCoverArtistContribs,
-      });
-
-    const albumsAsWallpaperArtistEntries =
-      processAlbumEntries({
-        type: 'albumWallpaper',
-        albums: albumsAsWallpaperArtist,
-        contribs: albumsAsWallpaperArtistContribs,
-      });
-
-    const albumsAsBannerArtistEntries =
-      processAlbumEntries({
-        type: 'albumBanner',
-        albums: albumsAsBannerArtist,
-        contribs: albumsAsBannerArtistContribs,
-      });
-
-    const tracksAsCoverArtistEntries =
-      processTrackEntries({
-        type: 'trackCover',
-        tracks: tracksAsCoverArtist,
-        contribs: tracksAsCoverArtistContribs,
-      });
-
-    const entries = [
-      ...albumsAsCoverArtistEntries,
-      ...albumsAsWallpaperArtistEntries,
-      ...albumsAsBannerArtistEntries,
-      ...tracksAsCoverArtistEntries,
+    const allContributions = [
+      ...artist.albumCoverArtistContributions,
+      ...artist.albumWallpaperArtistContributions,
+      ...artist.albumBannerArtistContributions,
+      ...artist.trackCoverArtistContributions,
     ];
 
-    sortEntryThingPairs(entries,
-      things => sortAlbumsTracksChronologically(things, {
-        getDate: thing => thing.coverArtDate ?? thing.date,
-      }));
-
-    const chunks =
-      chunkByProperties(
-        entries.map(({entry}) => entry),
-        ['album', 'date']);
-
-    return {chunks};
+    sortContributionsChronologically(
+      allContributions,
+      sortAlbumsTracksChronologically);
+
+    query.contribs =
+      chunkByConditions(allContributions, [
+        ({date: date1}, {date: date2}) =>
+          +date1 !== +date2,
+        ({thing: thing1}, {thing: thing2}) =>
+          (thing1.album ?? thing1) !==
+          (thing2.album ?? thing2),
+      ]);
+
+    query.albums =
+      query.contribs
+        .map(contribs => contribs[0].thing)
+        .map(thing => thing.album ?? thing);
+
+    return query;
   },
 
-  relations(relation, query, artist) {
-    return {
-      chunkedList:
-        relation('generateArtistInfoPageChunkedList'),
-
-      chunks:
-        query.chunks.map(() => relation('generateArtistInfoPageChunk')),
-
-      albumLinks:
-        query.chunks.map(({album}) => relation('linkAlbum', album)),
-
-      items:
-        query.chunks.map(({chunk}) =>
-          chunk.map(() => relation('generateArtistInfoPageChunkItem'))),
-
-      itemTrackLinks:
-        query.chunks.map(({chunk}) =>
-          chunk.map(({track}) => track ? relation('linkTrack', track) : null)),
-
-      itemOtherArtistLinks:
-        query.chunks.map(({chunk}) =>
-          chunk.map(({contribs}) => relation('generateArtistInfoPageOtherArtistLinks', contribs, artist))),
-    };
-  },
-
-  data(query, artist) {
-    return {
-      chunkDates:
-        query.chunks.map(({date}) => date),
-
-      itemTypes:
-        query.chunks.map(({chunk}) =>
-          chunk.map(({type}) => type)),
+  relations: (relation, query, _artist) => ({
+    chunkedList:
+      relation('generateArtistInfoPageChunkedList'),
 
-      itemContributions:
-        query.chunks.map(({chunk}) =>
-          chunk.map(({contribs}) =>
-            contribs
-              .find(contrib => contrib.artist === artist)
-              .annotation)),
-    };
-  },
-
-  generate(data, relations, {html, language}) {
-    return relations.chunkedList.slots({
-      chunks:
-        stitchArrays({
-          chunk: relations.chunks,
-          albumLink: relations.albumLinks,
-          date: data.chunkDates,
-
-          items: relations.items,
-          itemTrackLinks: relations.itemTrackLinks,
-          itemOtherArtistLinks: relations.itemOtherArtistLinks,
-          itemTypes: data.itemTypes,
-          itemContributions: data.itemContributions,
-        }).map(({
-            chunk,
-            albumLink,
-            date,
-
-            items,
-            itemTrackLinks,
-            itemOtherArtistLinks,
-            itemTypes,
-            itemContributions,
-          }) =>
-            chunk.slots({
-              mode: 'album',
-              albumLink,
-              date,
-
-              items:
-                stitchArrays({
-                  item: items,
-                  trackLink: itemTrackLinks,
-                  otherArtistLinks: itemOtherArtistLinks,
-                  type: itemTypes,
-                  contribution: itemContributions,
-                }).map(({
-                    item,
-                    trackLink,
-                    otherArtistLinks,
-                    type,
-                    contribution,
-                  }) =>
-                    item.slots({
-                      otherArtistLinks,
-                      annotation: contribution,
-
-                      content:
-                        (type === 'trackCover'
-                          ? language.$('artistPage.creditList.entry.track', {
-                              track: trackLink,
-                            })
-                          : html.tag('i',
-                              language.$('artistPage.creditList.entry.album.' + {
-                                albumWallpaper: 'wallpaperArt',
-                                albumBanner: 'bannerArt',
-                                albumCover: 'coverArt',
-                              }[type]))),
-                    })),
-            })),
-    });
-  },
+    chunks:
+      stitchArrays({
+        album: query.albums,
+        contribs: query.contribs,
+      }).map(({album, contribs}) =>
+          relation('generateArtistInfoPageArtworksChunk', album, contribs)),
+  }),
+
+  generate: (relations) =>
+    relations.chunkedList.slots({
+      chunks: relations.chunks,
+    }),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageChunk.js b/src/content/dependencies/generateArtistInfoPageChunk.js
index 40943914..c16d50f3 100644
--- a/src/content/dependencies/generateArtistInfoPageChunk.js
+++ b/src/content/dependencies/generateArtistInfoPageChunk.js
@@ -1,3 +1,5 @@
+import {empty} from '#sugar';
+
 export default {
   extraDependencies: ['html', 'language'],
 
@@ -21,15 +23,33 @@ export default {
       mutable: false,
     },
 
-    date: {validate: v => v.isDate},
-    dateRangeStart: {validate: v => v.isDate},
-    dateRangeEnd: {validate: v => v.isDate},
+    dates: {
+      validate: v => v.sparseArrayOf(v.isDate),
+    },
 
     duration: {validate: v => v.isDuration},
     durationApproximate: {type: 'boolean'},
   },
 
   generate(slots, {html, language}) {
+    let earliestDate = null;
+    let latestDate = null;
+    let onlyDate = null;
+
+    if (!empty(slots.dates)) {
+      earliestDate =
+        slots.dates
+          .reduce((a, b) => a <= b ? a : b);
+
+      latestDate =
+        slots.dates
+          .reduce((a, b) => a <= b ? b : a);
+
+      if (+earliestDate === +latestDate) {
+        onlyDate = earliestDate;
+      }
+    }
+
     let accentedLink;
 
     accent: {
@@ -40,9 +60,9 @@ export default {
           const options = {album: accentedLink};
           const parts = ['artistPage.creditList.album'];
 
-          if (slots.date) {
+          if (onlyDate) {
             parts.push('withDate');
-            options.date = language.formatDate(slots.date);
+            options.date = language.formatDate(onlyDate);
           }
 
           if (slots.duration) {
@@ -63,16 +83,13 @@ export default {
           const options = {act: accentedLink};
           const parts = ['artistPage.creditList.flashAct'];
 
-          if (
-            slots.dateRangeStart &&
-            slots.dateRangeEnd &&
-            slots.dateRangeStart !== slots.dateRangeEnd
-          ) {
-            parts.push('withDateRange');
-            options.dateRange = language.formatDateRange(slots.dateRangeStart, slots.dateRangeEnd);
-          } else if (slots.dateRangeStart || slots.date) {
+          if (onlyDate) {
             parts.push('withDate');
-            options.date = language.formatDate(slots.dateRangeStart ?? slots.date);
+            options.date = language.formatDate(onlyDate);
+          } else if (earliestDate && latestDate) {
+            parts.push('withDateRange');
+            options.dateRange =
+              language.formatDateRange(earliestDate, latestDate);
           }
 
           accentedLink = language.formatString(...parts, options);
diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js
index b6f40727..9d406c67 100644
--- a/src/content/dependencies/generateArtistInfoPageChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js
@@ -1,3 +1,5 @@
+import {empty} from '#sugar';
+
 export default {
   extraDependencies: ['html', 'language'],
 
@@ -19,42 +21,38 @@ export default {
     rerelease: {type: 'boolean'},
   },
 
-  generate(slots, {html, language}) {
-    let accentedContent = slots.content;
-
-    accent: {
-      if (slots.rerelease) {
-        accentedContent =
-          language.$('artistPage.creditList.entry.rerelease', {
-            entry: accentedContent,
-          });
-
-        break accent;
-      }
-
-      const parts = ['artistPage.creditList.entry'];
-      const options = {entry: accentedContent};
-
-      if (slots.otherArtistLinks) {
-        parts.push('withArtists');
-        options.artists = language.formatConjunctionList(slots.otherArtistLinks);
-      }
-
-      if (!html.isBlank(slots.annotation)) {
-        parts.push('withAnnotation');
-        options.annotation = slots.annotation;
-      }
-
-      if (parts.length === 1) {
-        break accent;
-      }
-
-      accentedContent = language.formatString(...parts, options);
-    }
-
-    return (
+  generate: (slots, {html, language}) =>
+    language.encapsulate('artistPage.creditList.entry', entryCapsule =>
       html.tag('li',
         slots.rerelease && {class: 'rerelease'},
-        accentedContent));
-  },
+
+        language.encapsulate(entryCapsule, workingCapsule => {
+          const workingOptions = {entry: slots.content};
+
+          if (slots.rerelease) {
+            workingCapsule += '.rerelease';
+            return language.$(workingCapsule, workingOptions);
+          }
+
+          let anyAccent = false;
+
+          if (!empty(slots.otherArtistLinks)) {
+            anyAccent = true;
+            workingCapsule += '.withArtists';
+            workingOptions.artists =
+              language.formatConjunctionList(slots.otherArtistLinks);
+          }
+
+          if (!html.isBlank(slots.annotation)) {
+            anyAccent = true;
+            workingCapsule += '.withAnnotation';
+            workingOptions.annotation = slots.annotation;
+          }
+
+          if (anyAccent) {
+            return language.$(workingCapsule, workingOptions);
+          } else {
+            return slots.content;
+          }
+        }))),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageChunkedList.js b/src/content/dependencies/generateArtistInfoPageChunkedList.js
index 8503d014..e7915ab7 100644
--- a/src/content/dependencies/generateArtistInfoPageChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageChunkedList.js
@@ -13,11 +13,8 @@ export default {
     },
   },
 
-  generate(slots, {html}) {
-    return (
-      html.tag('dl', [
-        slots.groupInfo,
-        slots.chunks,
-      ]));
-  },
+  generate: (slots, {html}) =>
+    html.tag('dl',
+      {[html.onlyIfContent]: true},
+      [slots.groupInfo, slots.chunks]),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
index 133095ea..72bbf1b6 100644
--- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
@@ -10,7 +10,6 @@ export default {
   contentDependencies: [
     'generateArtistInfoPageChunk',
     'generateArtistInfoPageChunkItem',
-    'generateArtistInfoPageOtherArtistLinks',
     'linkAlbum',
     'linkFlash',
     'linkFlashAct',
@@ -217,53 +216,52 @@ export default {
           itemAnnotations,
           itemTypes,
         }) =>
-          (chunkType === 'album'
-            ? chunk.slots({
-                mode: 'album',
-                albumLink: chunkLink,
-                items:
-                  stitchArrays({
-                    item: items,
-                    link: itemLinks,
-                    annotation: itemAnnotations,
-                    type: itemTypes,
-                  }).map(({item, link, annotation, type}) =>
-                    item.slots({
-                      annotation:
-                        (annotation
-                          ? annotation.slot('mode', 'inline')
-                          : null),
-
-                      content:
-                        (type === 'album'
-                          ? html.tag('i',
-                              language.$('artistPage.creditList.entry.album.commentary'))
-                          : language.$('artistPage.creditList.entry.track', {
-                              track: link,
-                            })),
-                    })),
-              })
-         : chunkType === 'flash-act'
-            ? chunk.slots({
-                mode: 'flash',
-                flashActLink: chunkLink,
-                items:
-                  stitchArrays({
-                    item: items,
-                    link: itemLinks,
-                    annotation: itemAnnotations,
-                  }).map(({item, link, annotation}) =>
-                    item.slots({
-                      annotation:
-                        (annotation
-                          ? annotation.slot('mode', 'inline')
-                          : null),
-
-                      content:
-                        language.$('artistPage.creditList.entry.flash', {
-                          flash: link,
-                        }),
-                    })),
-              })
-            : null))),
+          language.encapsulate('artistPage.creditList.entry', capsule =>
+            (chunkType === 'album'
+              ? chunk.slots({
+                  mode: 'album',
+                  albumLink: chunkLink,
+                  items:
+                    stitchArrays({
+                      item: items,
+                      link: itemLinks,
+                      annotation: itemAnnotations,
+                      type: itemTypes,
+                    }).map(({item, link, annotation, type}) =>
+                      item.slots({
+                        annotation:
+                          (annotation
+                            ? annotation.slot('mode', 'inline')
+                            : null),
+
+                        content:
+                          (type === 'album'
+                            ? html.tag('i',
+                                language.$(capsule, 'album.commentary'))
+                            : language.$(capsule, 'track', {track: link})),
+                      })),
+                })
+           : chunkType === 'flash-act'
+              ? chunk.slots({
+                  mode: 'flash',
+                  flashActLink: chunkLink,
+                  items:
+                    stitchArrays({
+                      item: items,
+                      link: itemLinks,
+                      annotation: itemAnnotations,
+                    }).map(({item, link, annotation}) =>
+                      item.slots({
+                        annotation:
+                          (annotation
+                            ? annotation.slot('mode', 'inline')
+                            : null),
+
+                        content:
+                          language.$(capsule, 'flash', {
+                            flash: link,
+                          }),
+                      })),
+                })
+              : null)))),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunk.js b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js
new file mode 100644
index 00000000..8aa7223a
--- /dev/null
+++ b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js
@@ -0,0 +1,34 @@
+export default {
+  contentDependencies: [
+    'generateArtistInfoPageChunk',
+    'generateArtistInfoPageFlashesChunkItem',
+    'linkFlashAct',
+  ],
+
+  relations: (relation, flashAct, contribs) => ({
+    template:
+      relation('generateArtistInfoPageChunk'),
+
+    flashActLink:
+      relation('linkFlashAct', flashAct),
+
+    items:
+      contribs
+        .map(contrib =>
+          relation('generateArtistInfoPageFlashesChunkItem', contrib)),
+  }),
+
+  data: (_flashAct, contribs) => ({
+    dates:
+      contribs
+        .map(contrib => contrib.date),
+  }),
+
+  generate: (data, relations) =>
+    relations.template.slots({
+      mode: 'flash',
+      flashActLink: relations.flashActLink,
+      dates: data.dates,
+      items: relations.items,
+    }),
+};
diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js
new file mode 100644
index 00000000..e4908bf9
--- /dev/null
+++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js
@@ -0,0 +1,34 @@
+export default {
+  contentDependencies: ['generateArtistInfoPageChunkItem', 'linkFlash'],
+
+  extraDependencies: ['language'],
+
+  relations: (relation, contrib) => ({
+    // Flashes and games can list multiple contributors as collaborative
+    // credits, but we don't display these on the artist page, since they
+    // usually involve many artists crediting a larger team where collaboration
+    // isn't as relevant (without more particular details that aren't tracked
+    // on the wiki).
+
+    template:
+      relation('generateArtistInfoPageChunkItem'),
+
+    flashLink:
+      relation('linkFlash', contrib.thing),
+  }),
+
+  data: (contrib) => ({
+    annotation:
+      contrib.annotation,
+  }),
+
+  generate: (data, relations, {language}) =>
+    relations.template.slots({
+      annotation: data.annotation,
+
+      content:
+        language.$('artistPage.creditList.entry.flash', {
+          flash: relations.flashLink,
+        }),
+    }),
+};
diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js
index 447e697e..b347faf5 100644
--- a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js
@@ -1,149 +1,62 @@
-import {sortEntryThingPairs, sortFlashesChronologically} from '#sort';
-import {chunkByProperties, stitchArrays} from '#sugar';
+import {sortContributionsChronologically, sortFlashesChronologically}
+  from '#sort';
+import {chunkByConditions, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
-    'generateArtistInfoPageChunk',
-    'generateArtistInfoPageChunkItem',
-    'linkFlash',
+    'generateArtistInfoPageChunkedList',
+    'generateArtistInfoPageFlashesChunk',
   ],
 
-  extraDependencies: ['html', 'language'],
+  extraDependencies: ['wikiData'],
 
-  query(artist) {
-    const processFlashEntry = ({flash, contribs}) => ({
-      thing: flash,
-      entry: {
-        flash: flash,
-        act: flash.act,
-        contribs: contribs,
-      },
-    });
+  sprawl: ({wikiInfo}) => ({
+    enableFlashesAndGames:
+      wikiInfo.enableFlashesAndGames,
+  }),
 
-    const processFlashEntries = ({flashes, contribs}) =>
-      stitchArrays({
-        flash: flashes,
-        contribs: contribs,
-      }).map(processFlashEntry);
-
-    const {flashesAsContributor} = artist;
-
-    const flashesAsContributorContribs =
-      flashesAsContributor
-        .map(flash => flash.contributorContribs);
-
-    const flashesAsContributorEntries =
-      processFlashEntries({
-        flashes: flashesAsContributor,
-        contribs: flashesAsContributorContribs,
-      });
-
-    const entries = [
-      ...flashesAsContributorEntries,
-    ];
-
-    sortEntryThingPairs(entries, sortFlashesChronologically);
-
-    const chunks =
-      chunkByProperties(
-        entries.map(({entry}) => entry),
-        ['act']);
-
-    return {chunks};
-  },
+  query(sprawl, artist) {
+    const query = {};
 
-  relations(relation, query) {
-    // Flashes and games can list multiple contributors as collaborative
-    // credits, but we don't display these on the artist page, since they
-    // usually involve many artists crediting a larger team where collaboration
-    // isn't as relevant (without more particular details that aren't tracked
-    // on the wiki).
+    const allContributions =
+      (sprawl.enableFlashesAndGames
+        ? [
+            ...artist.flashContributorContributions,
+          ]
+      : []);
 
-    return {
-      chunks:
-        query.chunks.map(() => relation('generateArtistInfoPageChunk')),
+    sortContributionsChronologically(
+      allContributions,
+      sortFlashesChronologically);
 
-      actLinks:
-        query.chunks.map(({chunk}) =>
-          relation('linkFlash', chunk[0].flash)),
+    query.contribs =
+      chunkByConditions(allContributions, [
+        ({thing: flash1}, {thing: flash2}) =>
+          flash1.act !== flash2.act,
+      ]);
 
-      items:
-        query.chunks.map(({chunk}) =>
-          chunk.map(() => relation('generateArtistInfoPageChunkItem'))),
+    query.flashActs =
+      query.contribs
+        .map(contribs => contribs[0].thing)
+        .map(thing => thing.act);
 
-      itemFlashLinks:
-        query.chunks.map(({chunk}) =>
-          chunk.map(({flash}) => relation('linkFlash', flash))),
-    };
+    return query;
   },
 
-  data(query, artist) {
-    return {
-      actNames:
-        query.chunks.map(({act}) => act.name),
+  relations: (relation, query, _sprawl, _artist) => ({
+    chunkedList:
+      relation('generateArtistInfoPageChunkedList'),
 
-      firstDates:
-        query.chunks.map(({chunk}) => chunk[0].flash.date ?? null),
-
-      lastDates:
-        query.chunks.map(({chunk}) => chunk.at(-1).flash.date ?? null),
-
-      itemContributions:
-        query.chunks.map(({chunk}) =>
-          chunk.map(({contribs}) =>
-            contribs
-              .find(contrib => contrib.artist === artist)
-              .annotation)),
-    };
-  },
-
-  generate(data, relations, {html, language}) {
-    return html.tag('dl',
+    chunks:
       stitchArrays({
-        chunk: relations.chunks,
-        actLink: relations.actLinks,
-        actName: data.actNames,
-        firstDate: data.firstDates,
-        lastDate: data.lastDates,
-
-        items: relations.items,
-        itemFlashLinks: relations.itemFlashLinks,
-        itemContributions: data.itemContributions,
-      }).map(({
-          chunk,
-          actLink,
-          actName,
-          firstDate,
-          lastDate,
-
-          items,
-          itemFlashLinks,
-          itemContributions,
-        }) =>
-          chunk.slots({
-            mode: 'flash',
-            flashActLink: actLink.slot('content', actName),
-            dateRangeStart: firstDate,
-            dateRangeEnd: lastDate,
-
-            items:
-              stitchArrays({
-                item: items,
-                flashLink: itemFlashLinks,
-                contribution: itemContributions,
-              }).map(({
-                  item,
-                  flashLink,
-                  contribution,
-                }) =>
-                  item.slots({
-                    annotation: contribution,
-
-                    content:
-                      language.$('artistPage.creditList.entry.flash', {
-                        flash: flashLink,
-                      }),
-                  })),
-          })));
-  },
+        flashAct: query.flashActs,
+        contribs: query.contribs,
+      }).map(({flashAct, contribs}) =>
+          relation('generateArtistInfoPageFlashesChunk', flashAct, contribs)),
+  }),
+
+  generate: (relations) =>
+    relations.chunkedList.slots({
+      chunks: relations.chunks,
+    }),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js
index 471ee26c..dcee9c00 100644
--- a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js
+++ b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js
@@ -1,24 +1,30 @@
-import {empty} from '#sugar';
+import {unique} from '#sugar';
 
 export default {
   contentDependencies: ['linkArtist'],
 
-  relations(relation, contribs, artist) {
-    const otherArtistContribs =
-      contribs.filter(contrib => contrib.artist !== artist);
+  query(contribs) {
+    const associatedContributionsByOtherArtists =
+      contribs
+        .flatMap(ownContrib =>
+          ownContrib.associatedContributions
+            .filter(associatedContrib =>
+              associatedContrib.artist !== ownContrib.artist));
 
-    if (empty(otherArtistContribs)) {
-      return {};
-    }
+    const otherArtists =
+      unique(
+        associatedContributionsByOtherArtists
+          .map(contrib => contrib.artist));
 
-    const otherArtistLinks =
-      otherArtistContribs
-        .map(contrib => relation('linkArtist', contrib.artist));
-
-    return {otherArtistLinks};
+    return {otherArtists};
   },
 
-  generate(relations) {
-    return relations.otherArtistLinks ?? null;
-  },
+  relations: (relation, query) => ({
+    artistLinks:
+      query.otherArtists
+        .map(artist => relation('linkArtist', artist)),
+  }),
+
+  generate: (relations) =>
+    relations.artistLinks,
 };
diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunk.js b/src/content/dependencies/generateArtistInfoPageTracksChunk.js
new file mode 100644
index 00000000..b42e4165
--- /dev/null
+++ b/src/content/dependencies/generateArtistInfoPageTracksChunk.js
@@ -0,0 +1,67 @@
+import {unique} from '#sugar';
+import {getTotalDuration} from '#wiki-data';
+
+export default {
+  contentDependencies: [
+    'generateArtistInfoPageChunk',
+    'generateArtistInfoPageTracksChunkItem',
+    'linkAlbum',
+  ],
+
+  relations: (relation, artist, album, trackContribLists) => ({
+    template:
+      relation('generateArtistInfoPageChunk'),
+
+    albumLink:
+      relation('linkAlbum', album),
+
+    // Intentional mapping here: each item may be associated with
+    // more than one contribution.
+    items:
+      trackContribLists.map(trackContribs =>
+        relation('generateArtistInfoPageTracksChunkItem',
+          artist,
+          trackContribs)),
+  }),
+
+  data(_artist, album, trackContribLists) {
+    const data = {};
+
+    const contribs =
+      trackContribLists.flat();
+
+    data.dates =
+      contribs
+        .map(contrib => contrib.date);
+
+    // TODO: Duration stuff should *maybe* be in proper data logic? Maaaybe?
+    const durationTerms =
+      unique(
+        contribs
+          .filter(contrib => contrib.countInDurationTotals)
+          .map(contrib => contrib.thing)
+          .filter(track => track.isOriginalRelease)
+          .filter(track => track.duration > 0));
+
+    data.duration =
+      getTotalDuration(durationTerms);
+
+    data.durationApproximate =
+      durationTerms.length > 1;
+
+    return data;
+  },
+
+  generate: (data, relations) =>
+    relations.template.slots({
+      mode: 'album',
+
+      albumLink: relations.albumLink,
+
+      dates: data.dates,
+      duration: data.duration,
+      durationApproximate: data.durationApproximate,
+
+      items: relations.items,
+    }),
+};
diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js
new file mode 100644
index 00000000..96976826
--- /dev/null
+++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js
@@ -0,0 +1,115 @@
+import {empty} from '#sugar';
+
+export default {
+  contentDependencies: [
+    'generateArtistInfoPageChunkItem',
+    'generateArtistInfoPageOtherArtistLinks',
+    'linkTrack',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  query (_artist, contribs) {
+    const query = {};
+
+    // TODO: Very mysterious what to do if the set of contributions is,
+    // in total, associated with more than one thing. No design yet.
+    query.track =
+      contribs[0].thing;
+
+    const creditedAsArtist =
+      contribs
+        .some(contrib => contrib.isArtistContribution);
+
+    const creditedAsContributor =
+      contribs
+        .some(contrib => contrib.isContributorContribution);
+
+    const annotatedContribs =
+      contribs
+        .filter(contrib => contrib.annotation);
+
+    const annotatedArtistContribs =
+      annotatedContribs
+        .filter(contrib => contrib.isArtistContribution);
+
+    const annotatedContributorContribs =
+      annotatedContribs
+        .filter(contrib => contrib.isContributorContribution);
+
+    // Don't display annotations associated with crediting in the
+    // Contributors field if the artist is also credited as an Artist
+    // *and* the Artist-field contribution is non-annotated. This is
+    // so that we don't misrepresent the artist - the contributor
+    // annotation tends to be for "secondary" and performance roles.
+    // For example, this avoids crediting Marcy Nabors on Renewed
+    // Return seemingly only for "bass clarinet" when they're also
+    // the one who composed and arranged Renewed Return!
+    if (
+      creditedAsArtist &&
+      creditedAsContributor &&
+      empty(annotatedArtistContribs)
+    ) {
+      query.displayedContributions = null;
+    } else if (
+      !empty(annotatedArtistContribs) ||
+      !empty(annotatedContributorContribs)
+    ) {
+      query.displayedContributions = [
+        ...annotatedArtistContribs,
+        ...annotatedContributorContribs,
+      ];
+    }
+
+    return query;
+  },
+
+  relations: (relation, query, artist, contribs) => ({
+    template:
+      relation('generateArtistInfoPageChunkItem'),
+
+    trackLink:
+      relation('linkTrack', query.track),
+
+    otherArtistLinks:
+      relation('generateArtistInfoPageOtherArtistLinks', contribs),
+  }),
+
+  data: (query) => ({
+    duration:
+      query.track.duration,
+
+    rerelease:
+      query.track.isRerelease,
+
+    contribAnnotations:
+      (query.displayedContributions
+        ? query.displayedContributions
+            .map(contrib => contrib.annotation)
+        : null),
+  }),
+
+  generate: (data, relations, {html, language}) =>
+    relations.template.slots({
+      otherArtistLinks: relations.otherArtistLinks,
+      rerelease: data.rerelease,
+
+      annotation:
+        (data.contribAnnotations
+          ? language.formatUnitList(data.contribAnnotations)
+          : html.blank()),
+
+      content:
+        language.encapsulate('artistPage.creditList.entry.track', workingCapsule => {
+          const workingOptions = {track: relations.trackLink};
+
+          if (data.duration) {
+            workingCapsule += '.withDuration';
+            workingOptions.duration =
+              language.formatDuration(data.duration);
+          }
+
+          return language.$(workingCapsule, workingOptions);
+        }),
+    }),
+};
diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
index bce6cedf..7c01accb 100644
--- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
@@ -1,293 +1,65 @@
-import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort';
-import {accumulateSum, chunkByProperties, empty, stitchArrays} from '#sugar';
+import {sortAlbumsTracksChronologically, sortContributionsChronologically}
+  from '#sort';
+import {chunkByConditions, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
-    'generateArtistInfoPageChunk',
     'generateArtistInfoPageChunkedList',
-    'generateArtistInfoPageChunkItem',
-    'generateArtistInfoPageOtherArtistLinks',
-    'linkAlbum',
-    'linkTrack',
+    'generateArtistInfoPageTracksChunk',
   ],
 
-  extraDependencies: ['html', 'language'],
-
   query(artist) {
-    const processTrackEntry = ({track, contribs}) => ({
-      thing: track,
-      entry: {
-        track: track,
-        album: track.album,
-        date: track.date,
-        contribs: contribs,
-      },
-    });
-
-    const processTrackEntries = ({tracks, contribs}) =>
-      stitchArrays({
-        track: tracks,
-        contribs: contribs,
-      }).map(processTrackEntry);
-
-    const {tracksAsArtist, tracksAsContributor} = artist;
-
-    const tracksAsArtistAndContributor =
-      tracksAsArtist
-        .filter(track => tracksAsContributor.includes(track));
-
-    const tracksAsArtistOnly =
-      tracksAsArtist
-        .filter(track => !tracksAsContributor.includes(track));
-
-    const tracksAsContributorOnly =
-      tracksAsContributor
-        .filter(track => !tracksAsArtist.includes(track));
-
-    const tracksAsArtistAndContributorContribs =
-      tracksAsArtistAndContributor
-        .map(track => [
-          ...
-            track.artistContribs
-              .map(contrib => ({...contrib, kind: 'artist'})),
-          ...
-            track.contributorContribs
-              .map(contrib => ({...contrib, kind: 'contributor'})),
-        ]);
-
-    const tracksAsArtistOnlyContribs =
-      tracksAsArtistOnly
-        .map(track => track.artistContribs
-          .map(contrib => ({...contrib, kind: 'artist'})));
-
-    const tracksAsContributorOnlyContribs =
-      tracksAsContributorOnly
-        .map(track => track.contributorContribs
-          .map(contrib => ({...contrib, kind: 'contributor'})));
+    const query = {};
 
-    const tracksAsArtistAndContributorEntries =
-      processTrackEntries({
-        tracks: tracksAsArtistAndContributor,
-        contribs: tracksAsArtistAndContributorContribs,
-      });
-
-    const tracksAsArtistOnlyEntries =
-      processTrackEntries({
-        tracks: tracksAsArtistOnly,
-        contribs: tracksAsArtistOnlyContribs,
-      });
-
-    const tracksAsContributorOnlyEntries =
-      processTrackEntries({
-        tracks: tracksAsContributorOnly,
-        contribs: tracksAsContributorOnlyContribs,
-      });
-
-    const entries = [
-      ...tracksAsArtistAndContributorEntries,
-      ...tracksAsArtistOnlyEntries,
-      ...tracksAsContributorOnlyEntries,
+    const allContributions = [
+      ...artist.trackArtistContributions,
+      ...artist.trackContributorContributions,
     ];
 
-    sortEntryThingPairs(entries, sortAlbumsTracksChronologically);
-
-    const chunks =
-      chunkByProperties(
-        entries.map(({entry}) => entry),
-        ['album', 'date']);
-
-    return {chunks};
-  },
-
-  relations(relation, query, artist) {
-    return {
-      chunkedList:
-        relation('generateArtistInfoPageChunkedList'),
-
-      chunks:
-        query.chunks.map(() => relation('generateArtistInfoPageChunk')),
-
-      albumLinks:
-        query.chunks.map(({album}) => relation('linkAlbum', album)),
-
-      items:
-        query.chunks.map(({chunk}) =>
-          chunk.map(() => relation('generateArtistInfoPageChunkItem'))),
-
-      trackLinks:
-        query.chunks.map(({chunk}) =>
-          chunk.map(({track}) => relation('linkTrack', track))),
-
-      trackOtherArtistLinks:
-        query.chunks.map(({chunk}) =>
-          chunk.map(({contribs}) => relation('generateArtistInfoPageOtherArtistLinks', contribs, artist))),
-    };
-  },
-
-  data(query, artist) {
-    return {
-      chunkDates:
-        query.chunks.map(({date}) => date),
-
-      chunkDurations:
-        query.chunks.map(({chunk}) =>
-          accumulateSum(
-            chunk
-              .filter(({track}) => track.duration && track.originalReleaseTrack === null)
-              .map(({track}) => track.duration))),
-
-      chunkDurationsApproximate:
-        query.chunks.map(({chunk}) =>
-          chunk
-            .filter(({track}) => track.duration && track.originalReleaseTrack === null)
-            .length > 1),
-
-      trackDurations:
-        query.chunks.map(({chunk}) =>
-          chunk.map(({track}) => track.duration)),
-
-      trackContributions:
-        query.chunks.map(({chunk}) =>
-          chunk
-            .map(({contribs}) =>
-              contribs.filter(contrib => contrib.artist === artist))
-            .map(ownContribs => ({
-              creditedAsArtist:
-                ownContribs
-                  .some(({kind}) => kind === 'artist'),
-
-              creditedAsContributor:
-                ownContribs
-                  .some(({kind}) => kind === 'contributor'),
-
-              annotatedContribs:
-                ownContribs
-                  .filter(({annotation}) => annotation),
-            }))
-            .map(({annotatedContribs, ...rest}) => ({
-              ...rest,
-
-              annotatedArtistContribs:
-                annotatedContribs
-                  .filter(({kind}) => kind === 'artist'),
-
-              annotatedContributorContribs:
-                annotatedContribs
-                  .filter(({kind}) => kind === 'contributor'),
-            }))
-            .map(({
-              creditedAsArtist,
-              creditedAsContributor,
-              annotatedArtistContribs,
-              annotatedContributorContribs,
-            }) => {
-              // Don't display annotations associated with crediting in the
-              // Contributors field if the artist is also credited as an Artist
-              // *and* the Artist-field contribution is non-annotated. This is
-              // so that we don't misrepresent the artist - the contributor
-              // annotation tends to be for "secondary" and performance roles.
-              // For example, this avoids crediting Marcy Nabors on Renewed
-              // Return seemingly only for "bass clarinet" when they're also
-              // the one who composed and arranged Renewed Return!
-              if (
-                creditedAsArtist &&
-                creditedAsContributor &&
-                empty(annotatedArtistContribs)
-              ) {
-                return [];
-              }
-
-              return [
-                ...annotatedArtistContribs,
-                ...annotatedContributorContribs,
-              ];
-            })
-            .map(contribs =>
-              contribs.map(({annotation}) => annotation))
-            .map(contributions =>
-              (empty(contributions)
-                ? null
-                : contributions))),
-
-      trackRereleases:
-        query.chunks.map(({chunk}) =>
-          chunk.map(({track}) => track.originalReleaseTrack !== null)),
-    };
+    sortContributionsChronologically(
+      allContributions,
+      sortAlbumsTracksChronologically);
+
+    query.contribs =
+      // First chunk by (contribution) date and album.
+      chunkByConditions(allContributions, [
+        ({date: date1}, {date: date2}) =>
+          +date1 !== +date2,
+        ({thing: track1}, {thing: track2}) =>
+          track1.album !== track2.album,
+      ]).map(contribs =>
+          // Then, *within* the boundaries of the existing chunks,
+          // chunk contributions to the same thing together.
+          chunkByConditions(contribs, [
+            ({thing: thing1}, {thing: thing2}) =>
+              thing1 !== thing2,
+          ]));
+
+    query.albums =
+      query.contribs
+        .map(contribs =>
+          contribs[0][0].thing.album);
+
+    return query;
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.chunkedList.slots({
-      chunks:
-        stitchArrays({
-          chunk: relations.chunks,
-          albumLink: relations.albumLinks,
-          date: data.chunkDates,
-          duration: data.chunkDurations,
-          durationApproximate: data.chunkDurationsApproximate,
-
-          items: relations.items,
-          trackLinks: relations.trackLinks,
-          trackOtherArtistLinks: relations.trackOtherArtistLinks,
-          trackDurations: data.trackDurations,
-          trackContributions: data.trackContributions,
-          trackRereleases: data.trackRereleases,
-        }).map(({
-            chunk,
-            albumLink,
-            date,
-            duration,
-            durationApproximate,
+  relations: (relation, query, artist) => ({
+    chunkedList:
+      relation('generateArtistInfoPageChunkedList'),
 
-            items,
-            trackLinks,
-            trackOtherArtistLinks,
-            trackDurations,
-            trackContributions,
-            trackRereleases,
-          }) =>
-            chunk.slots({
-              mode: 'album',
-              albumLink,
-              date,
-              duration,
-              durationApproximate,
-
-              items:
-                stitchArrays({
-                  item: items,
-                  trackLink: trackLinks,
-                  otherArtistLinks: trackOtherArtistLinks,
-                  duration: trackDurations,
-                  contribution: trackContributions,
-                  rerelease: trackRereleases,
-                }).map(({
-                    item,
-                    trackLink,
-                    otherArtistLinks,
-                    duration,
-                    contribution,
-                    rerelease,
-                  }) =>
-                    item.slots({
-                      otherArtistLinks,
-                      rerelease,
-
-                      annotation:
-                        (contribution
-                          ? language.formatUnitList(contribution)
-                          : html.blank()),
-
-                      content:
-                        (duration
-                          ? language.$('artistPage.creditList.entry.track.withDuration', {
-                              track: trackLink,
-                              duration: language.formatDuration(duration),
-                            })
-                          : language.$('artistPage.creditList.entry.track', {
-                              track: trackLink,
-                            })),
-                    })),
-            })),
-    });
-  },
+    chunks:
+      stitchArrays({
+        album: query.albums,
+        contribs: query.contribs,
+      }).map(({album, contribs}) =>
+          relation('generateArtistInfoPageTracksChunk',
+            artist,
+            album,
+            contribs)),
+  }),
+
+  generate: (relations) =>
+    relations.chunkedList.slots({
+      chunks: relations.chunks,
+    }),
 };
diff --git a/src/content/dependencies/generateArtistNavLinks.js b/src/content/dependencies/generateArtistNavLinks.js
index aa95dba2..527e4741 100644
--- a/src/content/dependencies/generateArtistNavLinks.js
+++ b/src/content/dependencies/generateArtistNavLinks.js
@@ -24,8 +24,8 @@ export default {
       relation('linkArtist', artist);
 
     if (
-      !empty(artist.albumsAsCoverArtist) ||
-      !empty(artist.tracksAsCoverArtist)
+      !empty(artist.albumCoverArtistContributions) ||
+      !empty(artist.trackCoverArtistContributions)
     ) {
       relations.artistGalleryLink =
         relation('linkArtistGallery', artist);
diff --git a/src/content/dependencies/generateChronologyLinks.js b/src/content/dependencies/generateChronologyLinks.js
deleted file mode 100644
index 8ec6ee0a..00000000
--- a/src/content/dependencies/generateChronologyLinks.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import {accumulateSum, empty} from '#sugar';
-
-export default {
-  extraDependencies: ['html', 'language'],
-
-  slots: {
-    chronologyInfoSets: {
-      validate: v =>
-        v.strictArrayOf(
-          v.validateProperties({
-            headingString: v.isString,
-            contributions: v.strictArrayOf(v.validateProperties({
-              index: v.isCountingNumber,
-              artistLink: v.isHTML,
-              previousLink: v.isHTML,
-              nextLink: v.isHTML,
-            })),
-          })),
-    }
-  },
-
-  generate(slots, {html, language}) {
-    if (empty(slots.chronologyInfoSets)) {
-      return html.blank();
-    }
-
-    const totalContributionCount =
-      accumulateSum(
-        slots.chronologyInfoSets,
-        ({contributions}) => contributions.length);
-
-    if (totalContributionCount === 0) {
-      return html.blank();
-    }
-
-    if (totalContributionCount > 8) {
-      return html.tag('div', {class: 'chronology'},
-        language.$('misc.chronology.seeArtistPages'));
-    }
-
-    return html.tags(
-      slots.chronologyInfoSets.map(({
-        headingString,
-        contributions,
-      }) =>
-        contributions.map(({
-          index,
-          artistLink,
-          previousLink,
-          nextLink,
-        }) => {
-          const heading =
-            html.tag('span', {class: 'heading'},
-              language.$(headingString, {
-                index: language.formatIndex(index),
-                artist: artistLink,
-              }));
-
-          const navigation =
-            (previousLink || nextLink) &&
-              html.tag('span', {class: 'buttons'},
-                language.formatUnitList([
-                  previousLink?.slots({
-                    tooltipStyle: 'browser',
-                    color: false,
-                    content: language.$('misc.nav.previous'),
-                  }),
-
-                  nextLink?.slots({
-                    tooltipStyle: 'browser',
-                    color: false,
-                    content: language.$('misc.nav.next'),
-                  }),
-                ].filter(Boolean)));
-
-          return html.tag('div', {class: 'chronology'},
-            (navigation
-              ? language.$('misc.chronology.withNavigation', {heading, navigation})
-              : heading));
-        })));
-  },
-};
diff --git a/src/content/dependencies/generateColorStyleVariables.js b/src/content/dependencies/generateColorStyleVariables.js
index 069d85dd..5270dbe4 100644
--- a/src/content/dependencies/generateColorStyleVariables.js
+++ b/src/content/dependencies/generateColorStyleVariables.js
@@ -32,6 +32,7 @@ export default {
       dim,
       deep,
       deepGhost,
+      lightGhost,
       bg,
       bgBlack,
       shadow,
@@ -43,6 +44,7 @@ export default {
       `--dim-color: ${dim}`,
       `--deep-color: ${deep}`,
       `--deep-ghost-color: ${deepGhost}`,
+      `--light-ghost-color: ${lightGhost}`,
       `--bg-color: ${bg}`,
       `--bg-black-color: ${bgBlack}`,
       `--shadow-color: ${shadow}`,
diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js
index 522a0284..7c4aed80 100644
--- a/src/content/dependencies/generateCommentaryEntry.js
+++ b/src/content/dependencies/generateCommentaryEntry.js
@@ -43,56 +43,71 @@ export default {
     color: {validate: v => v.isColor},
   },
 
-  generate(data, relations, slots, {html, language}) {
-    const artistsSpan =
-      html.tag('span', {class: 'commentary-entry-artists'},
-        (relations.artistsContent
-          ? relations.artistsContent.slot('mode', 'inline')
-       : relations.artistLinks
-          ? language.formatConjunctionList(relations.artistLinks)
-          : language.$('misc.artistCommentary.entry.title.noArtists')));
-
-    const accentParts = ['misc.artistCommentary.entry.title.accent'];
-    const accentOptions = {};
-
-    if (relations.annotationContent) {
-      accentParts.push('withAnnotation');
-      accentOptions.annotation =
-        relations.annotationContent.slot('mode', 'inline');
-    }
-
-    if (data.date) {
-      accentParts.push('withDate');
-      accentOptions.date =
-        language.formatDate(data.date);
-    }
-
-    const accent =
-      (accentParts.length > 1
-        ? html.tag('span', {class: 'commentary-entry-accent'},
-            language.$(...accentParts, accentOptions))
-        : null);
-
-    const titleParts = ['misc.artistCommentary.entry.title'];
-    const titleOptions = {artists: artistsSpan};
-
-    if (accent) {
-      titleParts.push('withAccent');
-      titleOptions.accent = accent;
-    }
-
-    const style =
-      slots.color &&
-        relations.colorStyle.slot('color', slots.color);
-
-    return html.tags([
-      html.tag('p', {class: 'commentary-entry-heading'},
-        style,
-        language.$(...titleParts, titleOptions)),
-
-      html.tag('blockquote', {class: 'commentary-entry-body'},
-        style,
-        relations.bodyContent.slot('mode', 'multiline')),
-    ]);
-  },
+  generate: (data, relations, slots, {html, language}) =>
+    language.encapsulate('misc.artistCommentary.entry', entryCapsule =>
+      html.tags([
+        html.tag('p', {class: 'commentary-entry-heading'},
+          slots.color &&
+            relations.colorStyle.clone()
+              .slot('color', slots.color),
+
+          language.encapsulate(entryCapsule, 'title', titleCapsule => [
+            html.tag('time',
+              {[html.onlyIfContent]: true},
+
+              language.$(titleCapsule, 'date', {
+                [language.onlyIfOptions]: ['date'],
+
+                date:
+                  language.formatDate(data.date),
+              })),
+
+            language.encapsulate(titleCapsule, workingCapsule => {
+              const workingOptions = {};
+
+              workingOptions.artists =
+                html.tag('span', {class: 'commentary-entry-artists'},
+                  (relations.artistsContent
+                    ? relations.artistsContent.slot('mode', 'inline')
+                 : relations.artistLinks
+                    ? language.formatConjunctionList(relations.artistLinks)
+                    : language.$(titleCapsule, 'noArtists')));
+
+              const accent =
+                html.tag('span', {class: 'commentary-entry-accent'},
+                  {[html.onlyIfContent]: true},
+
+                  language.encapsulate(titleCapsule, 'accent', accentCapsule =>
+                    language.encapsulate(accentCapsule, workingCapsule => {
+                      const workingOptions = {};
+
+                      if (relations.annotationContent) {
+                        workingCapsule += '.withAnnotation';
+                        workingOptions.annotation =
+                          relations.annotationContent.slot('mode', 'inline');
+                      }
+
+                      if (workingCapsule === accentCapsule) {
+                        return html.blank();
+                      } else {
+                        return language.$(workingCapsule, workingOptions);
+                      }
+                    })));
+
+              if (!html.isBlank(accent)) {
+                workingCapsule += '.withAccent';
+                workingOptions.accent = accent;
+              }
+
+              return language.$(workingCapsule, workingOptions);
+            }),
+          ])),
+
+        html.tag('blockquote', {class: 'commentary-entry-body'},
+          slots.color &&
+            relations.colorStyle.clone()
+              .slot('color', slots.color),
+
+          relations.bodyContent.slot('mode', 'multiline')),
+      ])),
 };
diff --git a/src/content/dependencies/generateCommentaryIndexPage.js b/src/content/dependencies/generateCommentaryIndexPage.js
index 3c3504d2..d68ba42e 100644
--- a/src/content/dependencies/generateCommentaryIndexPage.js
+++ b/src/content/dependencies/generateCommentaryIndexPage.js
@@ -57,46 +57,48 @@ export default {
     };
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout.slots({
-      title: language.$('commentaryIndex.title'),
-
-      headingMode: 'static',
-
-      mainClasses: ['long-content'],
-      mainContent: [
-        html.tag('p', language.$('commentaryIndex.infoLine', {
-          words:
-            html.tag('b',
-              language.formatWordCount(data.totalWordCount, {unit: true})),
-
-          entries:
-            html.tag('b',
-                language.countCommentaryEntries(data.totalEntryCount, {unit: true})),
-        })),
-
-        html.tag('p',
-          language.$('commentaryIndex.albumList.title')),
-
-        html.tag('ul',
-          stitchArrays({
-            albumLink: relations.albumLinks,
-            wordCount: data.wordCounts,
-            entryCount: data.entryCounts,
-          }).map(({albumLink, wordCount, entryCount}) =>
-            html.tag('li',
-              language.$('commentaryIndex.albumList.item', {
-                album: albumLink,
-                words: language.formatWordCount(wordCount, {unit: true}),
-                entries: language.countCommentaryEntries(entryCount, {unit: true}),
-              })))),
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {auto: 'current'},
-      ],
-    });
-  },
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('commentaryIndex', pageCapsule =>
+      relations.layout.slots({
+        title: language.$(pageCapsule, 'title'),
+
+        headingMode: 'static',
+
+        mainClasses: ['long-content'],
+        mainContent: [
+          html.tag('p', language.$(pageCapsule, 'infoLine', {
+            words:
+              html.tag('b',
+                language.formatWordCount(data.totalWordCount, {unit: true})),
+
+            entries:
+              html.tag('b',
+                  language.countCommentaryEntries(data.totalEntryCount, {unit: true})),
+          })),
+
+          language.encapsulate(pageCapsule, 'albumList', listCapsule => [
+            html.tag('p',
+              language.$(listCapsule, 'title')),
+
+            html.tag('ul',
+              stitchArrays({
+                albumLink: relations.albumLinks,
+                wordCount: data.wordCounts,
+                entryCount: data.entryCounts,
+              }).map(({albumLink, wordCount, entryCount}) =>
+                html.tag('li',
+                  language.$(listCapsule, 'item', {
+                    album: albumLink,
+                    words: language.formatWordCount(wordCount, {unit: true}),
+                    entries: language.countCommentaryEntries(entryCount, {unit: true}),
+                  })))),
+          ]),
+        ],
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {auto: 'current'},
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generateCommentarySection.js b/src/content/dependencies/generateCommentarySection.js
index 8ae1b2d0..c5090660 100644
--- a/src/content/dependencies/generateCommentarySection.js
+++ b/src/content/dependencies/generateCommentarySection.js
@@ -1,3 +1,5 @@
+import {empty} from '#sugar';
+
 export default {
   contentDependencies: [
     'transformContent',
@@ -12,16 +14,29 @@ export default {
       relation('generateContentHeading'),
 
     entries:
-      entries.map(entry =>
-        relation('generateCommentaryEntry', entry)),
+      (entries
+        ? entries.map(entry =>
+            relation('generateCommentaryEntry', entry))
+        : []),
+  }),
+
+  data: (entries) => ({
+    firstEntryIsDated:
+      (empty(entries)
+        ? null
+        : !!entries[0].date),
   }),
 
-  generate: (relations, {html, language}) =>
+  generate: (data, relations, {html, language}) =>
     html.tags([
       relations.heading
         .slots({
-          id: 'artist-commentary',
-          title: language.$('misc.artistCommentary')
+          title: language.$('misc.artistCommentary'),
+          attributes: [
+            {id: 'artist-commentary'},
+            data.firstEntryIsDated &&
+              {class: 'first-entry-is-dated'},
+          ],
         }),
 
       relations.entries,
diff --git a/src/content/dependencies/generateContentHeading.js b/src/content/dependencies/generateContentHeading.js
index 469db876..f52bc043 100644
--- a/src/content/dependencies/generateContentHeading.js
+++ b/src/content/dependencies/generateContentHeading.js
@@ -12,23 +12,35 @@ export default {
       mutable: false,
     },
 
+    stickyTitle: {
+      type: 'html',
+      mutable: false,
+    },
+
     accent: {
       type: 'html',
       mutable: false,
     },
 
+    attributes: {
+      type: 'attributes',
+      mutable: false,
+    },
+
     color: {validate: v => v.isColor},
 
-    id: {type: 'string'},
-    tag: {type: 'string', default: 'p'},
+    tag: {
+      type: 'string',
+      default: 'p',
+    },
   },
 
   generate: (relations, slots, {html}) =>
     html.tag(slots.tag, {class: 'content-heading'},
       {tabindex: '0'},
+      {[html.onlyIfSiblings]: true},
 
-      slots.id &&
-        {id: slots.id},
+      slots.attributes,
 
       slots.color &&
         relations.colorStyle.slot('color', slots.color),
@@ -38,6 +50,10 @@ export default {
           {[html.onlyIfContent]: true},
           slots.title),
 
+        html.tag('template', {class: 'content-heading-sticky-title'},
+          {[html.onlyIfContent]: true},
+          slots.stickyTitle),
+
         html.tag('span', {class: 'content-heading-accent'},
           {[html.onlyIfContent]: true},
           slots.accent),
diff --git a/src/content/dependencies/generateContributionList.js b/src/content/dependencies/generateContributionList.js
index 6401e65e..8e8c5020 100644
--- a/src/content/dependencies/generateContributionList.js
+++ b/src/content/dependencies/generateContributionList.js
@@ -2,20 +2,28 @@ export default {
   contentDependencies: ['linkContribution'],
   extraDependencies: ['html'],
 
-  relations: (relation, contributions) =>
-    ({contributionLinks:
-        contributions
-          .map(contrib => relation('linkContribution', contrib))}),
+  relations: (relation, contributions) => ({
+    contributionLinks:
+      contributions
+        .map(contrib => relation('linkContribution', contrib)),
+  }),
 
-  generate: (relations, {html}) =>
+  slots: {
+    chronologyKind: {type: 'string'},
+  },
+
+  generate: (relations, slots, {html}) =>
     html.tag('ul',
-      relations.contributionLinks.map(contributionLink =>
-        html.tag('li',
-          contributionLink
-            .slots({
-              showIcons: true,
+      {[html.onlyIfContent]: true},
+
+      relations.contributionLinks
+        .map(contributionLink =>
+          html.tag('li',
+            contributionLink.slots({
+              showExternalLinks: true,
               showContribution: true,
+              showChronology: true,
               preventWrapping: false,
-              iconMode: 'tooltip',
+              chronologyKind: slots.chronologyKind,
             })))),
 };
diff --git a/src/content/dependencies/generateContributionTooltip.js b/src/content/dependencies/generateContributionTooltip.js
new file mode 100644
index 00000000..3a31014d
--- /dev/null
+++ b/src/content/dependencies/generateContributionTooltip.js
@@ -0,0 +1,48 @@
+export default {
+  contentDependencies: [
+    'generateContributionTooltipChronologySection',
+    'generateContributionTooltipExternalLinkSection',
+    'generateTooltip',
+  ],
+
+  extraDependencies: ['html'],
+
+  relations: (relation, contribution) => ({
+    tooltip:
+      relation('generateTooltip'),
+
+    externalLinkSection:
+      relation('generateContributionTooltipExternalLinkSection', contribution),
+
+    chronologySection:
+      relation('generateContributionTooltipChronologySection', contribution),
+  }),
+
+  slots: {
+    showExternalLinks: {type: 'boolean'},
+    showChronology: {type: 'boolean'},
+
+    chronologyKind: {type: 'string'},
+  },
+
+  generate: (relations, slots, {html}) =>
+    relations.tooltip.slots({
+      attributes:
+        {class: 'contribution-tooltip'},
+
+      contentAttributes: {
+        [html.joinChildren]:
+          html.tag('span', {class: 'tooltip-divider'}),
+      },
+
+      content: [
+        slots.showExternalLinks &&
+          relations.externalLinkSection,
+
+        slots.showChronology &&
+          relations.chronologySection.slots({
+            kind: slots.chronologyKind,
+          }),
+      ],
+    }),
+};
diff --git a/src/content/dependencies/generateContributionTooltipChronologySection.js b/src/content/dependencies/generateContributionTooltipChronologySection.js
new file mode 100644
index 00000000..78c9051c
--- /dev/null
+++ b/src/content/dependencies/generateContributionTooltipChronologySection.js
@@ -0,0 +1,117 @@
+export default {
+  contentDependencies: ['linkAnythingMan'],
+  extraDependencies: ['html', 'language'],
+
+  query(contribution) {
+    let previous = contribution;
+    while (previous && previous.thing === contribution.thing) {
+      previous = previous.previousBySameArtist;
+    }
+
+    let next = contribution;
+    while (next && next.thing === contribution.thing) {
+      next = next.nextBySameArtist;
+    }
+
+    return {previous, next};
+  },
+
+  relations: (relation, query, _contribution) => ({
+    previousLink:
+      (query.previous
+        ? relation('linkAnythingMan', query.previous.thing)
+        : null),
+
+    nextLink:
+      (query.next
+        ? relation('linkAnythingMan', query.next.thing)
+        : null),
+  }),
+
+  data: (query, _contribution) => ({
+    previousName:
+      (query.previous
+        ? query.previous.thing.name
+        : null),
+
+    nextName:
+      (query.next
+        ? query.next.thing.name
+        : null),
+  }),
+
+  slots: {
+    kind: {
+      validate: v =>
+        v.is(
+          'album',
+          'bannerArt',
+          'coverArt',
+          'flash',
+          'track',
+          'trackArt',
+          'trackContribution',
+          'wallpaperArt'),
+    },
+  },
+
+  generate: (data, relations, slots, {html, language}) =>
+    language.encapsulate('misc.artistLink.chronology', capsule =>
+      html.tags([
+        html.tags([
+          relations.previousLink?.slots({
+            attributes: {class: 'chronology-link'},
+            content: [
+              html.tag('span', {class: 'chronology-symbol'},
+                language.$(capsule, 'previous.symbol')),
+
+              html.tag('span', {class: 'chronology-text'},
+                language.sanitize(data.previousName)),
+            ],
+          }),
+
+          html.tag('span', {class: 'chronology-info'},
+            {[html.onlyIfSiblings]: true},
+
+            language.encapsulate(capsule, 'previous.info', workingCapsule => {
+              const workingOptions = {};
+
+              if (slots.kind) {
+                workingCapsule += '.withKind';
+                workingOptions.kind =
+                  language.$(capsule, 'kind', slots.kind);
+              }
+
+              return language.$(workingCapsule, workingOptions);
+            })),
+        ]),
+
+        html.tags([
+          relations.nextLink?.slots({
+            attributes: {class: 'chronology-link'},
+            content: [
+              html.tag('span', {class: 'chronology-symbol'},
+                language.$(capsule, 'next.symbol')),
+
+              html.tag('span', {class: 'chronology-text'},
+                language.sanitize(data.nextName)),
+            ],
+          }),
+
+          html.tag('span', {class: 'chronology-info'},
+            {[html.onlyIfSiblings]: true},
+
+            language.encapsulate(capsule, 'next.info', workingCapsule => {
+              const workingOptions = {};
+
+              if (slots.kind) {
+                workingCapsule += '.withKind';
+                workingOptions.kind =
+                  language.$(capsule, 'kind', slots.kind);
+              }
+
+              return language.$(workingCapsule, workingOptions);
+            }))
+        ]),
+      ])),
+};
diff --git a/src/content/dependencies/generateContributionTooltipExternalLinkSection.js b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js
new file mode 100644
index 00000000..4f9a23ed
--- /dev/null
+++ b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js
@@ -0,0 +1,70 @@
+import {stitchArrays} from '#sugar';
+
+export default {
+  contentDependencies: [
+    'generateExternalHandle',
+    'generateExternalIcon',
+    'generateExternalPlatform',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  relations: (relation, contribution) => ({
+    icons:
+      contribution.artist.urls
+        .map(url => relation('generateExternalIcon', url)),
+
+    handles:
+      contribution.artist.urls
+        .map(url => relation('generateExternalHandle', url)),
+
+    platforms:
+      contribution.artist.urls
+        .map(url => relation('generateExternalPlatform', url)),
+  }),
+
+  data: (contribution) => ({
+    urls: contribution.artist.urls,
+  }),
+
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('misc.artistLink', capsule =>
+      html.tags(
+        stitchArrays({
+          icon: relations.icons,
+          handle: relations.handles,
+          platform: relations.platforms,
+          url: data.urls,
+        }).map(({icon, handle, platform, url}) => {
+            for (const template of [icon, handle, platform]) {
+              template.setSlot('context', 'artist');
+            }
+
+            return [
+              html.tag('a', {class: 'external-link'},
+                {href: url},
+
+                [
+                  icon,
+
+                  html.tag('span', {class: 'external-handle'},
+                    (html.isBlank(handle)
+                      ? platform
+                      : handle)),
+                ]),
+
+              html.tag('span', {class: 'external-platform'},
+                // This is a pretty ridiculous hack, but we currently
+                // don't have a way of telling formatExternalLink to *not*
+                // use the fallback string, which just formats the URL as
+                // its host/domain... so is technically detectable.
+                (((new URL(url))
+                    .host
+                    .endsWith(
+                      html.resolve(platform, {normalize: 'string'})))
+
+                  ? language.$(capsule, 'noExternalLinkPlatformName')
+                  : platform)),
+            ];
+          }))),
+};
diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js
index 90c9db98..3d5a614f 100644
--- a/src/content/dependencies/generateCoverArtwork.js
+++ b/src/content/dependencies/generateCoverArtwork.js
@@ -1,4 +1,4 @@
-import {empty, stitchArrays} from '#sugar';
+import {stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['image', 'linkArtTag'],
@@ -89,14 +89,15 @@ export default {
             ...sizeSlots,
           }),
 
-          !empty(relations.tagLinks) &&
-            html.tag('ul', {class: 'image-details'},
-              stitchArrays({
-                tagLink: relations.tagLinks,
-                preferShortName: data.preferShortName,
-              }).map(({tagLink, preferShortName}) =>
-                  html.tag('li',
-                    tagLink.slot('preferShortName', preferShortName)))),
+          html.tag('ul', {class: 'image-details'},
+            {[html.onlyIfContent]: true},
+
+            stitchArrays({
+              tagLink: relations.tagLinks,
+              preferShortName: data.preferShortName,
+            }).map(({tagLink, preferShortName}) =>
+                html.tag('li',
+                  tagLink.slot('preferShortName', preferShortName)))),
         ]);
 
       case 'thumbnail':
diff --git a/src/content/dependencies/generateExternalHandle.js b/src/content/dependencies/generateExternalHandle.js
new file mode 100644
index 00000000..8c0368a4
--- /dev/null
+++ b/src/content/dependencies/generateExternalHandle.js
@@ -0,0 +1,20 @@
+import {isExternalLinkContext} from '#external-links';
+
+export default {
+  extraDependencies: ['html', 'language'],
+
+  data: (url) => ({url}),
+
+  slots: {
+    context: {
+      validate: () => isExternalLinkContext,
+      default: 'generic',
+    },
+  },
+
+  generate: (data, slots, {language}) =>
+    language.formatExternalLink(data.url, {
+      style: 'handle',
+      context: slots.context,
+    }),
+};
diff --git a/src/content/dependencies/generateExternalIcon.js b/src/content/dependencies/generateExternalIcon.js
new file mode 100644
index 00000000..637af658
--- /dev/null
+++ b/src/content/dependencies/generateExternalIcon.js
@@ -0,0 +1,26 @@
+import {isExternalLinkContext} from '#external-links';
+
+export default {
+  extraDependencies: ['html', 'language', 'to'],
+
+  data: (url) => ({url}),
+
+  slots: {
+    context: {
+      validate: () => isExternalLinkContext,
+      default: 'generic',
+    },
+  },
+
+  generate: (data, slots, {html, language, to}) =>
+    html.tag('span', {class: 'external-icon'},
+      html.tag('svg',
+        html.tag('use', {
+          href:
+            to('staticMisc.icon',
+              language.formatExternalLink(data.url, {
+                style: 'icon-id',
+                context: slots.context,
+              })),
+        }))),
+};
diff --git a/src/content/dependencies/generateExternalPlatform.js b/src/content/dependencies/generateExternalPlatform.js
new file mode 100644
index 00000000..c4f63ecf
--- /dev/null
+++ b/src/content/dependencies/generateExternalPlatform.js
@@ -0,0 +1,20 @@
+import {isExternalLinkContext} from '#external-links';
+
+export default {
+  extraDependencies: ['html', 'language'],
+
+  data: (url) => ({url}),
+
+  slots: {
+    context: {
+      validate: () => isExternalLinkContext,
+      default: 'generic',
+    },
+  },
+
+  generate: (data, slots, {language}) =>
+    language.formatExternalLink(data.url, {
+      style: 'platform',
+      context: slots.context,
+    }),
+};
diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js
index 17078124..1fa6de51 100644
--- a/src/content/dependencies/generateFlashActGalleryPage.js
+++ b/src/content/dependencies/generateFlashActGalleryPage.js
@@ -11,7 +11,7 @@ export default {
     'linkFlashIndex',
   ],
 
-  extraDependencies: ['html', 'language'],
+  extraDependencies: ['language'],
 
   relations: (relation, act) => ({
     layout:
@@ -50,42 +50,42 @@ export default {
         ['media.flashArt', flash.directory, flash.coverArtFileExtension])
   }),
 
-  generate(data, relations, {html, language}) {
-    return relations.layout.slots({
-      title:
-        language.$('flashPage.title', {
-          flash: new html.Tag(null, null, data.name),
-        }),
-
-      color: data.color,
-      headingMode: 'static',
-
-      mainClasses: ['flash-index'],
-      mainContent: [
-        relations.coverGrid.slots({
-          links: relations.flashLinks,
-          names: data.flashNames,
-          lazy: 6,
-
-          images:
-            stitchArrays({
-              image: relations.coverGridImages,
-              path: data.flashCoverPaths,
-            }).map(({image, path}) =>
-                image.slot('path', path)),
-        }),
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {html: relations.flashIndexLink},
-        {auto: 'current'},
-      ],
-
-      navBottomRowContent: relations.flashActNavAccent,
-
-      leftSidebar: relations.sidebar,
-    });
-  },
+  generate: (data, relations, {language}) =>
+    language.encapsulate('flashPage', pageCapsule =>
+      relations.layout.slots({
+        title:
+          language.$(pageCapsule, 'title', {
+            flash: data.name,
+          }),
+
+        color: data.color,
+        headingMode: 'static',
+
+        mainClasses: ['flash-index'],
+        mainContent: [
+          relations.coverGrid.slots({
+            links: relations.flashLinks,
+            names: data.flashNames,
+            lazy: 6,
+
+            images:
+              stitchArrays({
+                image: relations.coverGridImages,
+                path: data.flashCoverPaths,
+              }).map(({image, path}) =>
+                  image.slot('path', path)),
+          }),
+        ],
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {html: relations.flashIndexLink},
+          {auto: 'current'},
+        ],
+
+        navBottomRowContent: relations.flashActNavAccent,
+
+        leftSidebar: relations.sidebar,
+      })),
 };
diff --git a/src/content/dependencies/generateFlashCoverArtwork.js b/src/content/dependencies/generateFlashCoverArtwork.js
index 374fa3f8..af03ae6b 100644
--- a/src/content/dependencies/generateFlashCoverArtwork.js
+++ b/src/content/dependencies/generateFlashCoverArtwork.js
@@ -1,12 +1,26 @@
 export default {
   contentDependencies: ['generateCoverArtwork'],
 
-  relations: (relation) =>
-    ({coverArtwork: relation('generateCoverArtwork')}),
+  relations: (relation) => ({
+    coverArtwork:
+      relation('generateCoverArtwork'),
+  }),
 
-  data: (flash) =>
-    ({path: ['media.flashArt', flash.directory, flash.coverArtFileExtension]}),
+  data: (flash) => ({
+    path:
+      ['media.flashArt', flash.directory, flash.coverArtFileExtension],
+
+    color:
+      flash.color,
+
+    dimensions:
+      flash.coverArtDimensions,
+  }),
 
   generate: (data, relations) =>
-    relations.coverArtwork.slot('path', data.path),
+    relations.coverArtwork.slots({
+      path: data.path,
+      color: data.color,
+      dimensions: data.dimensions,
+    }),
 };
diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js
index 36bfabae..a21bb49e 100644
--- a/src/content/dependencies/generateFlashIndexPage.js
+++ b/src/content/dependencies/generateFlashIndexPage.js
@@ -1,4 +1,4 @@
-import {empty, stitchArrays} from '#sugar';
+import {stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
@@ -81,74 +81,77 @@ export default {
   }),
 
   generate: (data, relations, {html, language}) =>
-    relations.layout.slots({
-      title: language.$('flashIndex.title'),
-      headingMode: 'static',
-
-      mainClasses: ['flash-index'],
-      mainContent: [
-        !empty(data.jumpLinkLabels) && [
-          html.tag('p', {class: 'quick-info'},
-            language.$('misc.jumpTo')),
-
-          html.tag('ul', {class: 'quick-info'},
-            stitchArrays({
-              colorStyle: relations.jumpLinkColorStyles,
-              anchor: data.jumpLinkAnchors,
-              label: data.jumpLinkLabels,
-            }).map(({colorStyle, anchor, label}) =>
-                html.tag('li',
-                  html.tag('a',
-                    {href: '#' + anchor},
-                    colorStyle,
-                    label)))),
-        ],
+    language.encapsulate('flashIndex', pageCapsule =>
+      relations.layout.slots({
+        title: language.$(pageCapsule, 'title'),
+        headingMode: 'static',
+
+        mainClasses: ['flash-index'],
+        mainContent: [
+          html.tags([
+            html.tag('p', {class: 'quick-info'},
+              {[html.onlyIfSiblings]: true},
+              language.$('misc.jumpTo')),
+
+            html.tag('ul', {class: 'quick-info'},
+              {[html.onlyIfContent]: true},
+              stitchArrays({
+                colorStyle: relations.jumpLinkColorStyles,
+                anchor: data.jumpLinkAnchors,
+                label: data.jumpLinkLabels,
+              }).map(({colorStyle, anchor, label}) =>
+                  html.tag('li',
+                    html.tag('a',
+                      {href: '#' + anchor},
+                      colorStyle,
+                      label)))),
+          ]),
 
-        stitchArrays({
-          colorStyle: relations.actColorStyles,
-          actLink: relations.actLinks,
-          anchor: data.actAnchors,
-
-          coverGrid: relations.actCoverGrids,
-          coverGridImages: relations.actCoverGridImages,
-          coverGridLinks: relations.actCoverGridLinks,
-          coverGridNames: data.actCoverGridNames,
-          coverGridPaths: data.actCoverGridPaths,
-        }).map(({
-            colorStyle,
-            actLink,
-            anchor,
-
-            coverGrid,
-            coverGridImages,
-            coverGridLinks,
-            coverGridNames,
-            coverGridPaths,
-          }, index) => [
-            html.tag('h2',
-              {id: anchor},
+          stitchArrays({
+            colorStyle: relations.actColorStyles,
+            actLink: relations.actLinks,
+            anchor: data.actAnchors,
+
+            coverGrid: relations.actCoverGrids,
+            coverGridImages: relations.actCoverGridImages,
+            coverGridLinks: relations.actCoverGridLinks,
+            coverGridNames: data.actCoverGridNames,
+            coverGridPaths: data.actCoverGridPaths,
+          }).map(({
               colorStyle,
-              actLink),
-
-            coverGrid.slots({
-              links: coverGridLinks,
-              names: coverGridNames,
-              lazy: index === 0 ? 4 : true,
-
-              images:
-                stitchArrays({
-                  image: coverGridImages,
-                  path: coverGridPaths,
-                }).map(({image, path}) =>
-                    image.slot('path', path)),
-            }),
-          ]),
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {auto: 'current'},
-      ],
-    }),
+              actLink,
+              anchor,
+
+              coverGrid,
+              coverGridImages,
+              coverGridLinks,
+              coverGridNames,
+              coverGridPaths,
+            }, index) => [
+              html.tag('h2',
+                {id: anchor},
+                colorStyle,
+                actLink),
+
+              coverGrid.slots({
+                links: coverGridLinks,
+                names: coverGridNames,
+                lazy: index === 0 ? 4 : true,
+
+                images:
+                  stitchArrays({
+                    image: coverGridImages,
+                    path: coverGridPaths,
+                  }).map(({image, path}) =>
+                      image.slot('path', path)),
+              }),
+            ]),
+        ],
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {auto: 'current'},
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js
index 05964936..d06f0c01 100644
--- a/src/content/dependencies/generateFlashInfoPage.js
+++ b/src/content/dependencies/generateFlashInfoPage.js
@@ -19,180 +19,151 @@ export default {
   query(flash) {
     const query = {};
 
-    if (flash.page || !empty(flash.urls)) {
-      query.urls = [];
+    query.urls = [];
 
-      if (flash.page) {
-        query.urls.push(`https://homestuck.com/story/${flash.page}`);
-      }
+    if (flash.page) {
+      query.urls.push(`https://homestuck.com/story/${flash.page}`);
+    }
 
-      if (!empty(flash.urls)) {
-        query.urls.push(...flash.urls);
-      }
+    if (!empty(flash.urls)) {
+      query.urls.push(...flash.urls);
     }
 
     return query;
   },
 
-  relations(relation, query, flash) {
-    const relations = {};
-    const sections = relations.sections = {};
-
-    relations.layout =
-      relation('generatePageLayout');
+  relations: (relation, query, flash) => ({
+    layout:
+      relation('generatePageLayout'),
 
-    relations.sidebar =
-      relation('generateFlashActSidebar', flash.act, flash);
+    sidebar:
+      relation('generateFlashActSidebar', flash.act, flash),
 
-    if (query.urls) {
-      relations.externalLinks =
-        query.urls.map(url => relation('linkExternal', url));
-    }
-
-    // TODO: Flashes always have cover art (#175)
-    /* eslint-disable-next-line no-constant-condition */
-    if (true) {
-      relations.cover =
-        relation('generateFlashCoverArtwork', flash);
-    }
+    externalLinks:
+      query.urls
+        .map(url => relation('linkExternal', url)),
 
-    // Section: navigation bar
+    cover:
+      relation('generateFlashCoverArtwork', flash),
 
-    const nav = sections.nav = {};
+    contentHeading:
+      relation('generateContentHeading'),
 
-    nav.flashActLink =
-      relation('linkFlashAct', flash.act);
+    flashActLink:
+      relation('linkFlashAct', flash.act),
 
-    nav.flashNavAccent =
-      relation('generateFlashNavAccent', flash);
+    flashNavAccent:
+      relation('generateFlashNavAccent', flash),
 
-    // Section: Featured tracks
-
-    if (!empty(flash.featuredTracks)) {
-      const featuredTracks = sections.featuredTracks = {};
-
-      featuredTracks.heading =
-        relation('generateContentHeading');
-
-      featuredTracks.list =
-        relation('generateTrackList', flash.featuredTracks);
-    }
-
-    // Section: Contributors
-
-    if (!empty(flash.contributorContribs)) {
-      const contributors = sections.contributors = {};
-
-      contributors.heading =
-        relation('generateContentHeading');
-
-      contributors.list =
-        relation('generateContributionList', flash.contributorContribs);
-    }
-
-    // Section: Artist commentary
-
-    if (flash.commentary) {
-      sections.artistCommentary =
-        relation('generateCommentarySection', flash.commentary);
-    }
+    featuredTracksList:
+      relation('generateTrackList', flash.featuredTracks),
 
-    return relations;
-  },
+    contributorContributionList:
+      relation('generateContributionList', flash.contributorContribs),
 
-  data(query, flash) {
-    const data = {};
+    artistCommentarySection:
+      relation('generateCommentarySection', flash.commentary),
+  }),
 
-    data.name = flash.name;
-    data.color = flash.color;
-    data.date = flash.date;
+  data: (_query, flash) => ({
+    name:
+      flash.name,
 
-    return data;
-  },
+    color:
+      flash.color,
 
-  generate(data, relations, {html, language}) {
-    const {sections: sec} = relations;
+    date:
+      flash.date,
+  }),
 
-    return relations.layout.slots({
-      title:
-        language.$('flashPage.title', {
-          flash: data.name,
-        }),
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('flashPage', pageCapsule =>
+      relations.layout.slots({
+        title:
+          language.$(pageCapsule, 'title', {
+            flash: data.name,
+          }),
 
-      color: data.color,
-      headingMode: 'sticky',
+        color: data.color,
+        headingMode: 'sticky',
 
-      cover:
-        (relations.cover
-          ? relations.cover.slots({
-              alt: language.$('misc.alt.flashArt'),
-            })
-          : null),
+        cover:
+          (relations.cover
+            ? relations.cover.slots({
+                alt: language.$('misc.alt.flashArt'),
+              })
+            : null),
 
-      mainContent: [
-        html.tag('p',
-          language.$('releaseInfo.released', {
-            date: language.formatDate(data.date),
-          })),
+        mainContent: [
+          html.tag('p',
+            language.$('releaseInfo.released', {
+              date: language.formatDate(data.date),
+            })),
 
-        relations.externalLinks &&
           html.tag('p',
+            {[html.onlyIfContent]: true},
+
             language.$('releaseInfo.playOn', {
+              [language.onlyIfOptions]: ['links'],
+
               links:
                 language.formatDisjunctionList(
                   relations.externalLinks
                     .map(link => link.slot('context', 'flash'))),
             })),
 
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-          {[html.joinChildren]: html.tag('br')},
-
-          [
-            sec.artistCommentary &&
-              language.$('releaseInfo.readCommentary', {
-                link: html.tag('a',
-                  {href: '#artist-commentary'},
-                  language.$('releaseInfo.readCommentary.link')),
+          html.tag('p',
+            {[html.onlyIfContent]: true},
+            {[html.joinChildren]: html.tag('br')},
+
+            language.encapsulate('releaseInfo', capsule => [
+              !html.isBlank(relations.artistCommentarySection) &&
+                language.encapsulate(capsule, 'readCommentary', capsule =>
+                  language.$(capsule, {
+                    link:
+                      html.tag('a',
+                        {href: '#artist-commentary'},
+                        language.$(capsule, 'link')),
+                  })),
+            ])),
+
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                attributes: {id: 'features'},
+                title:
+                  language.$('releaseInfo.tracksFeatured', {
+                    flash: html.tag('i', data.name),
+                  }),
               }),
-          ]),
 
-        sec.featuredTracks && [
-          sec.featuredTracks.heading
-            .slots({
-              id: 'features',
-              title:
-                language.$('releaseInfo.tracksFeatured', {
-                  flash: html.tag('i', data.name),
-                }),
-            }),
+            relations.featuredTracksList,
+          ]),
 
-          sec.featuredTracks.list,
-        ],
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                attributes: {id: 'contributors'},
+                title: language.$('releaseInfo.contributors'),
+              }),
 
-        sec.contributors && [
-          sec.contributors.heading
-            .slots({
-              id: 'contributors',
-              title: language.$('releaseInfo.contributors'),
+            relations.contributorContributionList.slots({
+              chronologyKind: 'flash',
             }),
+          ]),
 
-          sec.contributors.list,
+          relations.artistCommentarySection,
         ],
 
-        sec.artistCommentary,
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {html: sec.nav.flashActLink.slot('color', false)},
-        {auto: 'current'},
-      ],
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {html: relations.flashActLink.slot('color', false)},
+          {auto: 'current'},
+        ],
 
-      navBottomRowContent: sec.nav.flashNavAccent,
+        navBottomRowContent: relations.flashNavAccent,
 
-      leftSidebar: relations.sidebar,
-    });
-  },
+        leftSidebar: relations.sidebar,
+      })),
 };
diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js
index d07847c6..ceb54322 100644
--- a/src/content/dependencies/generateGroupGalleryPage.js
+++ b/src/content/dependencies/generateGroupGalleryPage.js
@@ -10,6 +10,7 @@ export default {
     'generateGroupSecondaryNav',
     'generateGroupSidebar',
     'generatePageLayout',
+    'generateQuickDescription',
     'image',
     'linkAlbum',
     'linkListing',
@@ -55,6 +56,9 @@ export default {
           .map(album => relation('image', album.artTags));
     }
 
+    relations.quickDescription =
+      relation('generateQuickDescription', group);
+
     relations.coverGrid =
       relation('generateCoverGrid');
 
@@ -107,10 +111,10 @@ export default {
     return data;
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout
-      .slots({
-        title: language.$('groupGalleryPage.title', {group: data.name}),
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('groupGalleryPage', pageCapsule =>
+      relations.layout.slots({
+        title: language.$(pageCapsule, 'title', {group: data.name}),
         headingMode: 'static',
 
         color: data.color,
@@ -128,8 +132,10 @@ export default {
                     image.slot('path', path)),
             }),
 
+          relations.quickDescription,
+
           html.tag('p', {class: 'quick-info'},
-            language.$('groupGalleryPage.infoLine', {
+            language.$(pageCapsule, 'infoLine', {
               tracks:
                 html.tag('b',
                   language.countTracks(data.numTracks, {
@@ -193,6 +199,5 @@ export default {
 
         secondaryNav:
           relations.secondaryNav ?? null,
-      });
-  },
+      })),
 };
diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js
index b5b456aa..87f35656 100644
--- a/src/content/dependencies/generateGroupInfoPage.js
+++ b/src/content/dependencies/generateGroupInfoPage.js
@@ -1,210 +1,82 @@
-import {empty, stitchArrays} from '#sugar';
-
 export default {
   contentDependencies: [
-    'generateAbsoluteDatetimestamp',
-    'generateColorStyleAttribute',
-    'generateContentHeading',
+    'generateGroupInfoPageAlbumsSection',
     'generateGroupNavLinks',
     'generateGroupSecondaryNav',
     'generateGroupSidebar',
     'generatePageLayout',
-    'linkAlbum',
     'linkExternal',
-    'linkGroupGallery',
-    'linkGroup',
     'transformContent',
   ],
 
   extraDependencies: ['html', 'language', 'wikiData'],
 
-  sprawl({wikiInfo}) {
-    return {
-      enableGroupUI: wikiInfo.enableGroupUI,
-    };
-  },
-
-  query(sprawl, group) {
-    const albums =
-      group.albums;
-
-    const albumGroups =
-      albums
-        .map(album => album.groups);
-
-    const albumOtherCategory =
-      albumGroups
-        .map(groups => groups
-          .map(group => group.category)
-          .find(category => category !== group.category));
-
-    const albumOtherGroups =
-      stitchArrays({
-        groups: albumGroups,
-        category: albumOtherCategory,
-      }).map(({groups, category}) =>
-          groups
-            .filter(group => group.category === category));
-
-    return {albums, albumOtherGroups};
-  },
-
-  relations(relation, query, sprawl, group) {
-    const relations = {};
-    const sec = relations.sections = {};
-
-    relations.layout =
-      relation('generatePageLayout');
-
-    relations.navLinks =
-      relation('generateGroupNavLinks', group);
-
-    if (sprawl.enableGroupUI) {
-      relations.secondaryNav =
-        relation('generateGroupSecondaryNav', group);
-
-      relations.sidebar =
-        relation('generateGroupSidebar', group);
-    }
+  sprawl: ({wikiInfo}) => ({
+    enableGroupUI:
+      wikiInfo.enableGroupUI,
+  }),
 
-    sec.info = {};
+  relations: (relation, sprawl, group) => ({
+    layout:
+      relation('generatePageLayout'),
 
-    if (!empty(group.urls)) {
-      sec.info.visitLinks =
-        group.urls
-          .map(url => relation('linkExternal', url));
-    }
+    navLinks:
+      relation('generateGroupNavLinks', group),
 
-    if (group.description) {
-      sec.info.description =
-        relation('transformContent', group.description);
-    }
+    secondaryNav:
+      (sprawl.enableGroupUI
+        ? relation('generateGroupSecondaryNav', group)
+        : null),
 
-    if (!empty(query.albums)) {
-      sec.albums = {};
+    sidebar:
+      (sprawl.enableGroupUI
+        ? relation('generateGroupSidebar', group)
+        : null),
 
-      sec.albums.heading =
-        relation('generateContentHeading');
+    visitLinks:
+      group.urls
+        .map(url => relation('linkExternal', url)),
 
-      sec.albums.galleryLink =
-        relation('linkGroupGallery', group);
+    description:
+      relation('transformContent', group.description),
 
-      sec.albums.albumColorStyles =
-        query.albums
-          .map(album => relation('generateColorStyleAttribute', album.color));
+    albumSection:
+      relation('generateGroupInfoPageAlbumsSection', group),
+  }),
 
-      sec.albums.albumLinks =
-        query.albums
-          .map(album => relation('linkAlbum', album));
+  data: (_sprawl, group) => ({
+    name:
+      group.name,
 
-      sec.albums.otherGroupLinks =
-        query.albumOtherGroups
-          .map(groups => groups
-            .map(group => relation('linkGroup', group)));
+    color:
+      group.color,
+  }),
 
-      sec.albums.datetimestamps =
-        group.albums.map(album =>
-          (album.date
-            ? relation('generateAbsoluteDatetimestamp', album.date)
-            : null));
-    }
-
-    return relations;
-  },
-
-  data(query, sprawl, group) {
-    const data = {};
-
-    data.name = group.name;
-    data.color = group.color;
-
-    return data;
-  },
-
-  generate(data, relations, {html, language}) {
-    const {sections: sec} = relations;
-
-    return relations.layout
-      .slots({
-        title: language.$('groupInfoPage.title', {group: data.name}),
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('groupInfoPage', pageCapsule =>
+      relations.layout.slots({
+        title: language.$(pageCapsule, 'title', {group: data.name}),
         headingMode: 'sticky',
         color: data.color,
 
         mainContent: [
-          sec.info.visitLinks &&
-            html.tag('p',
-              language.$('releaseInfo.visitOn', {
-                links:
-                  language.formatDisjunctionList(
-                    sec.info.visitLinks
-                      .map(link => link.slot('context', 'group'))),
-              })),
+          html.tag('p',
+            {[html.onlyIfContent]: true},
+
+            language.$('releaseInfo.visitOn', {
+              [language.onlyIfOptions]: ['links'],
+
+              links:
+                language.formatDisjunctionList(
+                  relations.visitLinks
+                    .map(link => link.slot('context', 'group'))),
+            })),
 
           html.tag('blockquote',
             {[html.onlyIfContent]: true},
-            sec.info.description
-              ?.slot('mode', 'multiline')),
-
-          sec.albums && [
-            sec.albums.heading
-              .slots({
-                tag: 'h2',
-                title: language.$('groupInfoPage.albumList.title'),
-              }),
-
-            html.tag('p',
-              language.$('groupInfoPage.viewAlbumGallery', {
-                link:
-                  sec.albums.galleryLink
-                    .slot('content', language.$('groupInfoPage.viewAlbumGallery.link')),
-              })),
-
-            html.tag('ul',
-              stitchArrays({
-                albumLink: sec.albums.albumLinks,
-                otherGroupLinks: sec.albums.otherGroupLinks,
-                datetimestamp: sec.albums.datetimestamps,
-                albumColorStyle: sec.albums.albumColorStyles,
-              }).map(({
-                  albumLink,
-                  otherGroupLinks,
-                  datetimestamp,
-                  albumColorStyle,
-                }) => {
-                  const prefix = 'groupInfoPage.albumList.item';
-                  const parts = [prefix];
-                  const options = {};
-
-                  options.album =
-                    albumLink.slot('color', false);
-
-                  if (datetimestamp) {
-                    parts.push('withYear');
-                    options.yearAccent =
-                      language.$(prefix, 'yearAccent', {
-                        year:
-                          datetimestamp.slots({style: 'year', tooltip: true}),
-                      });
-                  }
-
-                  if (!empty(otherGroupLinks)) {
-                    parts.push('withOtherGroup');
-                    options.otherGroupAccent =
-                      html.tag('span', {class: 'other-group-accent'},
-                        language.$(prefix, 'otherGroupAccent', {
-                          groups:
-                            language.formatConjunctionList(
-                              otherGroupLinks.map(groupLink =>
-                                groupLink.slot('color', false))),
-                        }));
-                  }
-
-                  return (
-                    html.tag('li',
-                      albumColorStyle,
-                      language.$(...parts, options)));
-                })),
-          ],
+            relations.description.slot('mode', 'multiline')),
+
+          relations.albumSection,
         ],
 
         leftSidebar:
@@ -217,6 +89,5 @@ export default {
         navLinks: relations.navLinks.content,
 
         secondaryNav: relations.secondaryNav ?? null,
-      });
-  },
+      })),
 };
diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js
new file mode 100644
index 00000000..8899e98e
--- /dev/null
+++ b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js
@@ -0,0 +1,136 @@
+import {empty} from '#sugar';
+import {stitchArrays} from '#sugar';
+
+export default {
+  contentDependencies: [
+    'generateAbsoluteDatetimestamp',
+    'generateColorStyleAttribute',
+    'generateContentHeading',
+    'linkAlbum',
+    'linkGroupGallery',
+    'linkGroup',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  query(group) {
+    const albums =
+      group.albums;
+
+    const albumGroups =
+      albums
+        .map(album => album.groups);
+
+    const albumOtherCategory =
+      albumGroups
+        .map(groups => groups
+          .map(group => group.category)
+          .find(category => category !== group.category));
+
+    const albumOtherGroups =
+      stitchArrays({
+        groups: albumGroups,
+        category: albumOtherCategory,
+      }).map(({groups, category}) =>
+          groups
+            .filter(group => group.category === category));
+
+    return {albums, albumOtherGroups};
+  },
+
+  relations: (relation, query, group) => ({
+    contentHeading:
+      relation('generateContentHeading'),
+
+    galleryLink:
+      relation('linkGroupGallery', group),
+
+    albumColorStyles:
+      query.albums
+        .map(album => relation('generateColorStyleAttribute', album.color)),
+
+    albumLinks:
+      query.albums
+        .map(album => relation('linkAlbum', album)),
+
+    otherGroupLinks:
+      query.albumOtherGroups
+        .map(groups => groups
+          .map(group => relation('linkGroup', group))),
+
+    datetimestamps:
+      group.albums.map(album =>
+        (album.date
+          ? relation('generateAbsoluteDatetimestamp', album.date)
+          : null)),
+  }),
+
+  generate: (relations, {html, language}) =>
+    language.encapsulate('groupInfoPage', pageCapsule =>
+      language.encapsulate(pageCapsule, 'albumList', listCapsule =>
+        html.tags([
+          relations.contentHeading
+            .slots({
+              tag: 'h2',
+              title: language.$(listCapsule, 'title'),
+            }),
+
+          html.tag('p',
+            {[html.onlyIfSiblings]: true},
+
+            language.encapsulate(pageCapsule, 'viewAlbumGallery', capsule =>
+              language.$(capsule, {
+                link:
+                  relations.galleryLink
+                    .slot('content', language.$(capsule, 'link')),
+              }))),
+
+          html.tag('ul',
+            {[html.onlyIfContent]: true},
+
+            stitchArrays({
+              albumLink: relations.albumLinks,
+              otherGroupLinks: relations.otherGroupLinks,
+              datetimestamp: relations.datetimestamps,
+              albumColorStyle: relations.albumColorStyles,
+            }).map(({
+                albumLink,
+                otherGroupLinks,
+                datetimestamp,
+                albumColorStyle,
+              }) =>
+                html.tag('li',
+                  albumColorStyle,
+
+                  language.encapsulate(listCapsule, 'item', itemCapsule =>
+                    language.encapsulate(itemCapsule, workingCapsule => {
+                      const workingOptions = {};
+
+                      workingOptions.album =
+                        albumLink.slot('color', false);
+
+                      if (datetimestamp) {
+                        workingCapsule += '.withYear';
+                        workingOptions.yearAccent =
+                          language.$(itemCapsule, 'yearAccent', {
+                            year:
+                              datetimestamp.slots({style: 'year', tooltip: true}),
+                          });
+                      }
+
+                      if (!empty(otherGroupLinks)) {
+                        workingCapsule += '.withOtherGroup';
+                        workingOptions.otherGroupAccent =
+                          html.tag('span', {class: 'other-group-accent'},
+                            language.$(itemCapsule, 'otherGroupAccent', {
+                              groups:
+                                language.formatConjunctionList(
+                                  otherGroupLinks.map(groupLink =>
+                                    groupLink.slot('color', false))),
+                            }));
+                      }
+
+                      return language.$(workingCapsule, workingOptions);
+                    }))))),
+        ]))),
+};
diff --git a/src/content/dependencies/generateGroupSidebarCategoryDetails.js b/src/content/dependencies/generateGroupSidebarCategoryDetails.js
index 69de373b..d52c77b8 100644
--- a/src/content/dependencies/generateGroupSidebarCategoryDetails.js
+++ b/src/content/dependencies/generateGroupSidebarCategoryDetails.js
@@ -46,37 +46,37 @@ export default {
     },
   },
 
-  generate(data, relations, slots, {html, language}) {
-    return html.tag('details',
-      data.isCurrentCategory &&
-        {class: 'current', open: true},
-
-      [
-        html.tag('summary',
-          relations.colorStyle,
-
-          html.tag('span',
-            language.$('groupSidebar.groupList.category', {
-              category:
-                html.tag('span', {class: 'group-name'},
-                  data.name),
-            }))),
-
-        html.tag('ul',
-          stitchArrays(({
-            infoLink: relations.groupInfoLinks,
-            galleryLink: relations.groupGalleryLinks,
-          })).map(({infoLink, galleryLink}, index) =>
-                html.tag('li',
-                  index === data.currentGroupIndex &&
-                    {class: 'current'},
-
-                  language.$('groupSidebar.groupList.item', {
-                    group:
-                      (slots.currentExtra === 'gallery'
-                        ? galleryLink ?? infoLink
-                        : infoLink),
-                  })))),
-      ]);
-  },
+  generate: (data, relations, slots, {html, language}) =>
+    language.encapsulate('groupSidebar.groupList', capsule =>
+      html.tag('details',
+        data.isCurrentCategory &&
+          {class: 'current', open: true},
+
+        [
+          html.tag('summary',
+            relations.colorStyle,
+
+            html.tag('span',
+              language.$(capsule, 'category', {
+                category:
+                  html.tag('span', {class: 'group-name'},
+                    data.name),
+              }))),
+
+          html.tag('ul',
+            stitchArrays(({
+              infoLink: relations.groupInfoLinks,
+              galleryLink: relations.groupGalleryLinks,
+            })).map(({infoLink, galleryLink}, index) =>
+                  html.tag('li',
+                    index === data.currentGroupIndex &&
+                      {class: 'current'},
+
+                    language.$(capsule, 'item', {
+                      group:
+                        (slots.currentExtra === 'gallery'
+                          ? galleryLink ?? infoLink
+                          : infoLink),
+                    })))),
+        ])),
 };
diff --git a/src/content/dependencies/generateListAllAdditionalFilesChunk.js b/src/content/dependencies/generateListAllAdditionalFilesChunk.js
index 43a78cb3..659cf4e5 100644
--- a/src/content/dependencies/generateListAllAdditionalFilesChunk.js
+++ b/src/content/dependencies/generateListAllAdditionalFilesChunk.js
@@ -42,49 +42,50 @@ export default {
               additionalFileLinks,
               additionalFileFiles,
             }) =>
-              (additionalFileLinks.length === 1
-                ? html.tag('li',
-                    additionalFileLinks[0].slots({
-                      content:
-                        language.$('listingPage', slots.stringsKey, 'file', {
-                          title: additionalFileTitle,
-                        }),
-                    }))
+              language.encapsulate('listingPage', slots.stringsKey, 'file', capsule =>
+                (additionalFileLinks.length === 1
+                  ? html.tag('li',
+                      additionalFileLinks[0].slots({
+                        content:
+                          language.$(capsule, {
+                            title: additionalFileTitle,
+                          }),
+                      }))
 
-             : additionalFileLinks.length === 0
-                ? html.tag('li',
-                    language.$('listingPage', slots.stringsKey, 'file.withNoFiles', {
-                      title: additionalFileTitle,
-                    }))
+               : additionalFileLinks.length === 0
+                  ? html.tag('li',
+                      language.$(capsule, 'withNoFiles', {
+                        title: additionalFileTitle,
+                      }))
 
-                : html.tag('li', {class: 'has-details'},
-                    html.tag('details', [
-                      html.tag('summary',
-                        html.tag('span',
-                          language.$('listingPage', slots.stringsKey, 'file.withMultipleFiles', {
-                            title:
-                              html.tag('span', {class: 'group-name'},
-                                additionalFileTitle),
+                  : html.tag('li', {class: 'has-details'},
+                      html.tag('details', [
+                        html.tag('summary',
+                          html.tag('span',
+                            language.$(capsule, 'withMultipleFiles', {
+                              title:
+                                html.tag('span', {class: 'group-name'},
+                                  additionalFileTitle),
 
-                            files:
-                              language.countAdditionalFiles(
-                                additionalFileLinks.length,
-                                {unit: true}),
-                          }))),
+                              files:
+                                language.countAdditionalFiles(
+                                  additionalFileLinks.length,
+                                  {unit: true}),
+                            }))),
 
-                      html.tag('ul',
-                        stitchArrays({
-                          additionalFileLink: additionalFileLinks,
-                          additionalFileFile: additionalFileFiles,
-                        }).map(({additionalFileLink, additionalFileFile}) =>
-                            html.tag('li',
-                              additionalFileLink.slots({
-                                content:
-                                  language.$('listingPage', slots.stringsKey, 'file', {
-                                    title: additionalFileFile,
-                                  }),
-                              })))),
-                    ])))))),
+                        html.tag('ul',
+                          stitchArrays({
+                            additionalFileLink: additionalFileLinks,
+                            additionalFileFile: additionalFileFiles,
+                          }).map(({additionalFileLink, additionalFileFile}) =>
+                              html.tag('li',
+                                additionalFileLink.slots({
+                                  content:
+                                    language.$(capsule, {
+                                      title: additionalFileFile,
+                                    }),
+                                })))),
+                      ]))))))),
     ]);
   },
 };
diff --git a/src/content/dependencies/generateListingPage.js b/src/content/dependencies/generateListingPage.js
index 23377afb..5f9a99a9 100644
--- a/src/content/dependencies/generateListingPage.js
+++ b/src/content/dependencies/generateListingPage.js
@@ -34,13 +34,15 @@ export default {
       relations.sameTargetListingLinks =
         listing.target.listings
           .map(listing => relation('linkListing', listing));
+    } else {
+      relations.sameTargetListingLinks = [];
     }
 
-    if (!empty(listing.seeAlso)) {
-      relations.seeAlsoLinks =
-        listing.seeAlso
-          .map(listing => relation('linkListing', listing));
-    }
+    relations.seeAlsoLinks =
+      (!empty(listing.seeAlso)
+        ? listing.seeAlso
+            .map(listing => relation('linkListing', listing))
+        : []);
 
     return relations;
   },
@@ -167,33 +169,37 @@ export default {
       headingMode: 'sticky',
 
       mainContent: [
-        relations.sameTargetListingLinks &&
-          html.tag('p',
-            language.$('listingPage.listingsFor', {
-              target:
-                language.$('listingPage.target', data.targetStringsKey),
-
-              listings:
-                language.formatUnitList(
-                  stitchArrays({
-                    link: relations.sameTargetListingLinks,
-                    stringsKey: data.sameTargetListingStringsKeys,
-                  }).map(({link, stringsKey}, index) =>
-                      html.tag('span',
-                        index === data.sameTargetListingsCurrentIndex &&
-                          {class: 'current'},
-
-                        link.slots({
-                          attributes: {class: 'nowrap'},
-                          content: language.$('listingPage', stringsKey, 'title.short'),
-                        })))),
-            })),
-
-        relations.seeAlsoLinks &&
-          html.tag('p',
-            language.$('listingPage.seeAlso', {
-              listings: language.formatUnitList(relations.seeAlsoLinks),
-            })),
+        html.tag('p',
+          {[html.onlyIfContent]: true},
+          language.$('listingPage.listingsFor', {
+            [language.onlyIfOptions]: ['listings'],
+
+            target:
+              language.$('listingPage.target', data.targetStringsKey),
+
+            listings:
+              language.formatUnitList(
+                stitchArrays({
+                  link: relations.sameTargetListingLinks,
+                  stringsKey: data.sameTargetListingStringsKeys,
+                }).map(({link, stringsKey}, index) =>
+                    html.tag('span',
+                      index === data.sameTargetListingsCurrentIndex &&
+                        {class: 'current'},
+
+                      link.slots({
+                        attributes: {class: 'nowrap'},
+                        content: language.$('listingPage', stringsKey, 'title.short'),
+                      })))),
+          })),
+
+        html.tag('p',
+          {[html.onlyIfContent]: true},
+          language.$('listingPage.seeAlso', {
+            [language.onlyIfOptions]: ['listings'],
+            listings:
+              language.formatUnitList(relations.seeAlsoLinks),
+          })),
 
         slots.content,
 
@@ -243,7 +249,7 @@ export default {
                   .clone()
                   .slots({
                     tag: 'dt',
-                    id,
+                    attributes: [id && {id}],
 
                     title:
                       formatListingString({
diff --git a/src/content/dependencies/generateNewsEntryPage.js b/src/content/dependencies/generateNewsEntryPage.js
index bcba7194..2c382cfa 100644
--- a/src/content/dependencies/generateNewsEntryPage.js
+++ b/src/content/dependencies/generateNewsEntryPage.js
@@ -91,41 +91,41 @@ export default {
     };
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout.slots({
-      title:
-        language.$('newsEntryPage.title', {
-          entry: data.name,
-        }),
-
-      headingMode: 'sticky',
-
-      mainClasses: ['long-content'],
-      mainContent: [
-        html.tag('p',
-          language.$('newsEntryPage.published', {
-            date: language.formatDate(data.date),
-          })),
-
-        relations.content,
-        relations.readAnotherLinks,
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {html: relations.newsIndexLink},
-        {
-          auto: 'current',
-          accent:
-            (relations.previousNextLinks
-              ? `(${language.formatUnitList(relations.previousNextLinks.slots({
-                  previousLink: relations.previousEntryNavLink ?? null,
-                  nextLink: relations.nextEntryNavLink ?? null,
-                }).content)})`
-              : null),
-        },
-      ],
-    });
-  },
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('newsEntryPage', pageCapsule =>
+      relations.layout.slots({
+        title:
+          language.$(pageCapsule, 'title', {
+            entry: data.name,
+          }),
+
+        headingMode: 'sticky',
+
+        mainClasses: ['long-content'],
+        mainContent: [
+          html.tag('p',
+            language.$(pageCapsule, 'published', {
+              date: language.formatDate(data.date),
+            })),
+
+          relations.content,
+          relations.readAnotherLinks,
+        ],
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {html: relations.newsIndexLink},
+          {
+            auto: 'current',
+            accent:
+              (relations.previousNextLinks
+                ? `(${language.formatUnitList(relations.previousNextLinks.slots({
+                    previousLink: relations.previousEntryNavLink ?? null,
+                    nextLink: relations.nextEntryNavLink ?? null,
+                  }).content)})`
+                : null),
+          },
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generateNewsIndexPage.js b/src/content/dependencies/generateNewsIndexPage.js
index 539af804..02964ce8 100644
--- a/src/content/dependencies/generateNewsIndexPage.js
+++ b/src/content/dependencies/generateNewsIndexPage.js
@@ -57,37 +57,38 @@ export default {
     };
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout.slots({
-      title: language.$('newsIndex.title'),
-      headingMode: 'sticky',
-
-      mainClasses: ['long-content', 'news-index'],
-      mainContent:
-        stitchArrays({
-          entryLink: relations.entryLinks,
-          viewRestLink: relations.viewRestLinks,
-          content: relations.entryContents,
-          date: data.entryDates,
-          directory: data.entryDirectories,
-        }).map(({entryLink, viewRestLink, content, date, directory}) =>
-            html.tag('article', {id: directory}, [
-              html.tag('h2', [
-                html.tag('time', language.formatDate(date)),
-                entryLink,
-              ]),
-
-              content,
-
-              viewRestLink
-                ?.slot('content', language.$('newsIndex.entry.viewRest')),
-            ])),
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {auto: 'current'},
-      ],
-    });
-  },
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('newsIndex', pageCapsule =>
+      relations.layout.slots({
+        title: language.$(pageCapsule, 'title'),
+        headingMode: 'sticky',
+
+        mainClasses: ['long-content', 'news-index'],
+        mainContent:
+          stitchArrays({
+            entryLink: relations.entryLinks,
+            viewRestLink: relations.viewRestLinks,
+            content: relations.entryContents,
+            date: data.entryDates,
+            directory: data.entryDirectories,
+          }).map(({entryLink, viewRestLink, content, date, directory}) =>
+              language.encapsulate(pageCapsule, 'entry', entryCapsule =>
+                html.tag('article', {id: directory}, [
+                  html.tag('h2', [
+                    html.tag('time', language.formatDate(date)),
+                    entryLink,
+                  ]),
+
+                  content,
+
+                  viewRestLink
+                    ?.slot('content', language.$(entryCapsule, 'viewRest')),
+                ]))),
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {auto: 'current'},
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index 51f9057b..7e9e49a0 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -5,12 +5,13 @@ export default {
   contentDependencies: [
     'generateColorStyleRules',
     'generateFooterLocalizationLinks',
+    'generatePageSidebar',
+    'generateSearchSidebarBox',
     'generateStickyHeadingContainer',
     'transformContent',
   ],
 
   extraDependencies: [
-    'cachebust',
     'getColors',
     'html',
     'language',
@@ -21,6 +22,7 @@ export default {
 
   sprawl({wikiInfo}) {
     return {
+      enableSearch: wikiInfo.enableSearch,
       footerContent: wikiInfo.footerContent,
       wikiColor: wikiInfo.color,
       wikiName: wikiInfo.nameShort,
@@ -43,6 +45,14 @@ export default {
     relations.stickyHeadingContainer =
       relation('generateStickyHeadingContainer');
 
+    relations.sidebar =
+      relation('generatePageSidebar');
+
+    if (sprawl.enableSearch) {
+      relations.searchBox =
+        relation('generateSearchSidebarBox');
+    }
+
     if (sprawl.footerContent) {
       relations.defaultFooterContent =
         relation('transformContent', sprawl.footerContent);
@@ -65,6 +75,11 @@ export default {
       default: true,
     },
 
+    showSearch: {
+      type: 'boolean',
+      default: true,
+    },
+
     additionalNames: {
       type: 'html',
       mutable: false,
@@ -209,7 +224,6 @@ export default {
   },
 
   generate(data, relations, slots, {
-    cachebust,
     getColors,
     html,
     language,
@@ -374,30 +388,66 @@ export default {
             slots.navContent),
         ]);
 
-    const getSidebar = (side, id) =>
-      (html.isBlank(slots[side])
-        ? html.blank()
-        : slots[side].slots({
-            attributes:
-              slots[side]
-                .getSlotValue('attributes')
-                .with({id}),
-          }));
+    const getSidebar = (side, id, needed) => {
+      const sidebar =
+        (html.isBlank(slots[side])
+          ? (needed
+              ? relations.sidebar.clone()
+              : html.blank())
+          : slots[side]);
+
+      if (html.isBlank(sidebar) && !needed) {
+        return sidebar;
+      }
+
+      return sidebar.slots({
+        attributes:
+          sidebar
+            .getSlotValue('attributes')
+            .with({id}),
+      });
+    }
+
+    const willShowSearch =
+      slots.showSearch && relations.searchBox;
 
-    const leftSidebar = getSidebar('leftSidebar', 'sidebar-left');
-    const rightSidebar = getSidebar('rightSidebar', 'sidebar-right');
+    let showingSidebarLeft;
+    let showingSidebarRight;
+
+    const leftSidebar = getSidebar('leftSidebar', 'sidebar-left', willShowSearch);
+    const rightSidebar = getSidebar('rightSidebar', 'sidebar-right', false);
+
+    if (willShowSearch) {
+      if (html.isBlank(leftSidebar)) {
+        leftSidebar.setSlot('initiallyHidden', true);
+        showingSidebarLeft = false;
+      }
+
+      leftSidebar.setSlot(
+        'boxes',
+        html.tags([
+          relations.searchBox,
+          leftSidebar.getSlotValue('boxes'),
+        ]));
+    }
 
     const hasSidebarLeft = !html.isBlank(html.resolve(leftSidebar));
     const hasSidebarRight = !html.isBlank(html.resolve(rightSidebar));
 
+    showingSidebarLeft ??= hasSidebarLeft;
+    showingSidebarRight ??= hasSidebarRight;
+
     const processSkippers = skipperList =>
       skipperList
         .filter(({condition, id}) =>
           (condition === undefined
             ? hasID(id)
             : condition))
+
         .map(({id, string}) =>
           html.tag('span', {class: 'skipper'},
+            {'data-for': id},
+
             html.tag('a',
               {href: `#${id}`},
               language.$('misc.skippers', string))));
@@ -456,41 +506,43 @@ export default {
           html.tag('img', {id: 'image-overlay-image'}),
           html.tag('img', {id: 'image-overlay-image-thumb'}),
         ]),
-        html.tag('div', {id: 'image-overlay-action-container'}, [
-          html.tag('div', {id: 'image-overlay-action-content-without-size'},
-            language.$('releaseInfo.viewOriginalFile', {
-              link: html.tag('a', {class: 'image-overlay-view-original'},
-                language.$('releaseInfo.viewOriginalFile.link')),
-            })),
-
-          html.tag('div', {id: 'image-overlay-action-content-with-size'}, [
-            language.$('releaseInfo.viewOriginalFile.withSize', {
-              link:
-                html.tag('a', {class: 'image-overlay-view-original'},
-                  language.$('releaseInfo.viewOriginalFile.link')),
-
-              size:
-                html.tag('span',
-                  {[html.joinChildren]: ''},
-                  [
-                    html.tag('span', {id: 'image-overlay-file-size-kilobytes'},
-                      language.$('count.fileSize.kilobytes', {
-                        kilobytes:
-                          html.tag('span', {class: 'image-overlay-file-size-count'}),
-                      })),
-
-                    html.tag('span', {id: 'image-overlay-file-size-megabytes'},
-                      language.$('count.fileSize.megabytes', {
-                        megabytes:
-                          html.tag('span', {class: 'image-overlay-file-size-count'}),
-                      })),
-                  ]),
-            }),
 
-            html.tag('span', {id: 'image-overlay-file-size-warning'},
-              language.$('releaseInfo.viewOriginalFile.sizeWarning')),
-          ]),
-        ]),
+        html.tag('div', {id: 'image-overlay-action-container'},
+          language.encapsulate('releaseInfo.viewOriginalFile', capsule => [
+            html.tag('div', {id: 'image-overlay-action-content-without-size'},
+              language.$(capsule, {
+                link: html.tag('a', {class: 'image-overlay-view-original'},
+                  language.$(capsule, 'link')),
+              })),
+
+            html.tag('div', {id: 'image-overlay-action-content-with-size'}, [
+              language.$(capsule, 'withSize', {
+                link:
+                  html.tag('a', {class: 'image-overlay-view-original'},
+                    language.$(capsule, 'link')),
+
+                size:
+                  html.tag('span',
+                    {[html.joinChildren]: ''},
+                    [
+                      html.tag('span', {id: 'image-overlay-file-size-kilobytes'},
+                        language.$('count.fileSize.kilobytes', {
+                          kilobytes:
+                            html.tag('span', {class: 'image-overlay-file-size-count'}),
+                        })),
+
+                      html.tag('span', {id: 'image-overlay-file-size-megabytes'},
+                        language.$('count.fileSize.megabytes', {
+                          megabytes:
+                            html.tag('span', {class: 'image-overlay-file-size-count'}),
+                        })),
+                    ]),
+              }),
+
+              html.tag('span', {id: 'image-overlay-file-size-warning'},
+                language.$(capsule, 'sizeWarning')),
+            ]),
+          ])),
       ]));
 
     const layoutHTML = [
@@ -528,6 +580,8 @@ export default {
         {'data-rebase-localized': to('localized.root')},
         {'data-rebase-shared': to('shared.root')},
         {'data-rebase-media': to('media.root')},
+        {'data-rebase-thumb': to('thumb.root')},
+        {'data-rebase-lib': to('staticLib.root')},
         {'data-rebase-data': to('data.root')},
 
         [
@@ -598,7 +652,7 @@ export default {
 
             html.tag('link', {
               rel: 'stylesheet',
-              href: to('shared.staticFile', 'site7.css', cachebust),
+              href: to('staticCSS.path', 'site.css'),
             }),
 
             html.tag('style', [
@@ -608,25 +662,29 @@ export default {
             ]),
 
             html.tag('script', {
-              src: to('shared.staticFile', 'lazy-loading.js', cachebust),
+              src: to('staticLib.path', 'chroma-js/chroma.min.js'),
+            }),
+
+            html.tag('script', {
+              blocking: 'render',
+              src: to('staticJS.path', 'lazy-loading.js'),
+            }),
+
+            html.tag('script', {
+              blocking: 'render',
+              type: 'module',
+              src: to('staticJS.path', 'client.js'),
             }),
           ]),
 
           html.tag('body',
             [
               html.tag('div', {id: 'page-container'},
-                (hasSidebarLeft || hasSidebarRight
-                  ? {class: 'has-one-sidebar'}
-                  : {class: 'has-zero-sidebars'}),
+                showingSidebarLeft &&
+                  {class: 'showing-sidebar-left'},
 
-                hasSidebarLeft && hasSidebarRight &&
-                  {class: 'has-two-sidebars'},
-
-                hasSidebarLeft &&
-                  {class: 'has-sidebar-left'},
-
-                hasSidebarRight &&
-                  {class: 'has-sidebar-right'},
+                showingSidebarRight &&
+                  {class: 'showing-sidebar-right'},
 
                 [
                   skippersHTML,
@@ -635,11 +693,6 @@ export default {
 
               // infoCardHTML,
               imageOverlayHTML,
-
-              html.tag('script', {
-                type: 'module',
-                src: to('shared.staticFile', 'client4.js', cachebust),
-              }),
             ]),
         ])
     ]).toString();
diff --git a/src/content/dependencies/generatePageSidebar.js b/src/content/dependencies/generatePageSidebar.js
index 43015aa3..d3b55580 100644
--- a/src/content/dependencies/generatePageSidebar.js
+++ b/src/content/dependencies/generatePageSidebar.js
@@ -19,14 +19,13 @@ export default {
     // Sticky mode controls which sidebar sections, if any, follow the
     // scroll position, "sticking" to the top of the browser viewport.
     //
-    // 'last' - last or only sidebar box is sticky
     // 'column' - entire column, incl. multiple boxes from top, is sticky
     // 'static' - sidebar not sticky at all, stays at top of page
     //
     // Note: This doesn't affect the content of any sidebar section, only
     // the whole section's containing box (or the sidebar column as a whole).
     stickyMode: {
-      validate: v => v.is('last', 'column', 'static'),
+      validate: v => v.is('column', 'static'),
       default: 'static',
     },
 
@@ -37,6 +36,16 @@ export default {
       type: 'boolean',
       default: false,
     },
+
+    // Provide to include all the HTML for the sidebar in place as usual,
+    // but start it out totally invisible. This is mainly so client-side
+    // JavaScript can show the sidebar if it needs to (and has a target
+    // to slot its own content into). If there are no boxes and this
+    // option *isn't* provided, then the sidebar will just be blank.
+    initiallyHidden: {
+      type: 'boolean',
+      default: false,
+    },
   },
 
   generate(slots, {html}) {
@@ -68,7 +77,11 @@ export default {
       attributes.add('class', 'all-boxes-collapsible');
     }
 
-    if (html.isBlank(slots.boxes)) {
+    if (slots.initiallyHidden) {
+      attributes.add('class', 'initially-hidden');
+    }
+
+    if (html.isBlank(slots.boxes) && !slots.initiallyHidden) {
       return html.blank();
     } else {
       return html.tag('div', attributes, slots.boxes);
diff --git a/src/content/dependencies/generatePageSidebarBox.js b/src/content/dependencies/generatePageSidebarBox.js
index e11efc3f..26b30494 100644
--- a/src/content/dependencies/generatePageSidebarBox.js
+++ b/src/content/dependencies/generatePageSidebarBox.js
@@ -20,6 +20,8 @@ export default {
 
   generate: (slots, {html}) =>
     html.tag('div', {class: 'sidebar'},
+      {[html.onlyIfContent]: true},
+
       slots.collapsible &&
         {class: 'collapsible'},
 
diff --git a/src/content/dependencies/generateQuickDescription.js b/src/content/dependencies/generateQuickDescription.js
new file mode 100644
index 00000000..4c7c944a
--- /dev/null
+++ b/src/content/dependencies/generateQuickDescription.js
@@ -0,0 +1,134 @@
+export default {
+  contentDependencies: ['transformContent'],
+  extraDependencies: ['html', 'language'],
+
+  query: (thing) => ({
+    hasDescription:
+      !!thing.description,
+
+    hasLongerDescription:
+      thing.description &&
+      thing.descriptionShort &&
+      thing.descriptionShort !== thing.description,
+  }),
+
+  relations: (relation, query, thing) => ({
+    description:
+      (query.hasLongerDescription || !thing.description
+        ? null
+        : relation('transformContent', thing.description)),
+
+    descriptionShort:
+      (query.hasLongerDescription
+        ? relation('transformContent', thing.descriptionShort)
+        : null),
+
+    descriptionLong:
+      (query.hasLongerDescription
+        ? relation('transformContent', thing.description)
+        : null),
+  }),
+
+  data: (query) => ({
+    hasDescription: query.hasDescription,
+    hasLongerDescription: query.hasLongerDescription,
+  }),
+
+  slots: {
+    extraReadingLinks: {
+      validate: v => v.sparseArrayOf(v.isHTML),
+    },
+  },
+
+  generate(data, relations, slots, {html, language}) {
+    const prefix = 'misc.quickDescription';
+
+    const actionsWithoutLongerDescription =
+      (data.hasLongerDescription
+        ? null
+     : slots.extraReadingLinks
+        ? language.$(prefix, 'readMore', {
+            links:
+              language.formatDisjunctionList(slots.extraReadingLinks),
+          })
+        : null);
+
+    const wrapExpandCollapseLink = (expandCollapse, content) =>
+      html.tag('a', {class: `${expandCollapse}-link`},
+        {href: '#'},
+        content);
+
+    const actionsWhenCollapsed =
+      (data.hasLongerDescription && slots.extraReadingLinks
+        ? language.$(prefix, 'expandDescription.orReadMore', {
+            links:
+              language.formatDisjunctionList(slots.extraReadingLinks),
+            expand:
+              wrapExpandCollapseLink('expand',
+                language.$(prefix, 'expandDescription.orReadMore.expand')),
+          })
+     : data.hasLongerDescription
+        ? language.$(prefix, 'expandDescription', {
+            expand:
+              wrapExpandCollapseLink('expand',
+                language.$(prefix, 'expandDescription.expand')),
+          })
+        : null);
+
+    const actionsWhenExpanded =
+      (data.hasLongerDescription && slots.extraReadingLinks
+        ? language.$(prefix, 'collapseDescription.orReadMore', {
+            links:
+              language.formatDisjunctionList(slots.extraReadingLinks),
+            collapse:
+              wrapExpandCollapseLink('collapse',
+                language.$(prefix, 'collapseDescription.orReadMore.collapse')),
+          })
+     : data.hasLongerDescription
+        ? language.$(prefix, 'collapseDescription', {
+            collapse:
+              wrapExpandCollapseLink('collapse',
+                language.$(prefix, 'collapseDescription.collapse')),
+          })
+        : null);
+
+    const wrapActions = (attributes, children) =>
+      html.tag('p', {class: 'quick-description-actions'},
+        {[html.onlyIfContent]: true},
+        attributes,
+
+        children);
+
+    const wrapContent = (attributes, content) =>
+      html.tag('div', {class: 'description-content'},
+        {[html.onlyIfContent]: true},
+        attributes,
+
+        content?.slot('mode', 'multiline'));
+
+    return (
+      html.tag('div', {class: 'quick-description'},
+        {[html.onlyIfContent]: true},
+
+        data.hasLongerDescription &&
+          {class: 'collapsed'},
+
+        !data.hasLongerDescription &&
+        !slots.extraReadingLinks &&
+          {class: 'has-content-only'},
+
+        !data.hasDescription &&
+        slots.extraReadingLinks &&
+          {class: 'has-external-links-only'},
+
+        [
+          wrapContent(null, relations.description),
+          wrapContent({class: 'short'}, relations.descriptionShort),
+          wrapContent({class: 'long'}, relations.descriptionLong),
+
+          wrapActions(null, actionsWithoutLongerDescription),
+          wrapActions({class: 'when-collapsed'}, actionsWhenCollapsed),
+          wrapActions({class: 'when-expanded'}, actionsWhenExpanded),
+        ]));
+  },
+};
diff --git a/src/content/dependencies/generateReleaseInfoContributionsLine.js b/src/content/dependencies/generateReleaseInfoContributionsLine.js
index 2e6c4709..3e96ed44 100644
--- a/src/content/dependencies/generateReleaseInfoContributionsLine.js
+++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js
@@ -17,10 +17,12 @@ export default {
   },
 
   slots: {
-    stringKey: {type: 'string'},
-
     showContribution: {type: 'boolean', default: true},
-    showIcons: {type: 'boolean', default: true},
+    showExternalLinks: {type: 'boolean', default: true},
+    showChronology: {type: 'boolean', default: true},
+
+    stringKey: {type: 'string'},
+    chronologyKind: {type: 'string'},
   },
 
   generate(relations, slots, {html, language}) {
@@ -34,8 +36,9 @@ export default {
           relations.contributionLinks.map(link =>
             link.slots({
               showContribution: slots.showContribution,
-              showIcons: slots.showIcons,
-              iconMode: 'tooltip',
+              showExternalLinks: slots.showExternalLinks,
+              showChronology: slots.showChronology,
+              chronologyKind: slots.chronologyKind,
             }))),
     });
   },
diff --git a/src/content/dependencies/generateSearchSidebarBox.js b/src/content/dependencies/generateSearchSidebarBox.js
new file mode 100644
index 00000000..188a678f
--- /dev/null
+++ b/src/content/dependencies/generateSearchSidebarBox.js
@@ -0,0 +1,62 @@
+export default {
+  contentDependencies: ['generatePageSidebarBox'],
+  extraDependencies: ['html', 'language'],
+
+  relations: (relation) => ({
+    sidebarBox:
+      relation('generatePageSidebarBox'),
+  }),
+
+  generate: (relations, {html, language}) =>
+    language.encapsulate('misc.search', capsule =>
+      relations.sidebarBox.slots({
+        attributes: {class: 'wiki-search-sidebar-box'},
+        collapsible: false,
+
+        content: [
+          html.tag('label', {class: 'wiki-search-label'},
+            html.tag('input', {class: 'wiki-search-input'},
+              {type: 'search'},
+
+              {
+                placeholder:
+                  language.$(capsule, 'placeholder').toString(),
+              })),
+
+          html.tag('template', {class: 'wiki-search-preparing-string'},
+            language.$(capsule, 'preparing')),
+
+          html.tag('template', {class: 'wiki-search-loading-data-string'},
+            language.$(capsule, 'loadingData')),
+
+          html.tag('template', {class: 'wiki-search-searching-string'},
+            language.$(capsule, 'searching')),
+
+          html.tag('template', {class: 'wiki-search-failed-string'},
+            language.$(capsule, 'failed')),
+
+          html.tag('template', {class: 'wiki-search-no-results-string'},
+            language.$(capsule, 'noResults')),
+
+          html.tag('template', {class: 'wiki-search-current-result-string'},
+            language.$(capsule, 'currentResult')),
+
+          html.tag('template', {class: 'wiki-search-end-search-string'},
+            language.$(capsule, 'endSearch')),
+
+          language.encapsulate(capsule, 'resultKind', capsule => [
+            html.tag('template', {class: 'wiki-search-album-result-kind-string'},
+              language.$(capsule, 'album')),
+
+            html.tag('template', {class: 'wiki-search-artist-result-kind-string'},
+              language.$(capsule, 'artist')),
+
+            html.tag('template', {class: 'wiki-search-group-result-kind-string'},
+              language.$(capsule, 'group')),
+
+            html.tag('template', {class: 'wiki-search-tag-result-kind-string'},
+              language.$(capsule, 'artTag')),
+          ]),
+        ],
+      })),
+};
diff --git a/src/content/dependencies/generateStickyHeadingContainer.js b/src/content/dependencies/generateStickyHeadingContainer.js
index 9becfb26..7f271715 100644
--- a/src/content/dependencies/generateStickyHeadingContainer.js
+++ b/src/content/dependencies/generateStickyHeadingContainer.js
@@ -22,10 +22,17 @@ export default {
         html.tag('div', {class: 'content-sticky-heading-row'}, [
           html.tag('h1', slots.title),
 
-          !html.isBlank(slots.cover) &&
-            html.tag('div', {class: 'content-sticky-heading-cover-container'},
-              html.tag('div', {class: 'content-sticky-heading-cover'},
-                slots.cover.slot('mode', 'thumbnail'))),
+          html.tag('div', {class: 'content-sticky-heading-cover-container'},
+            {[html.onlyIfContent]: true},
+
+            html.tag('div', {class: 'content-sticky-heading-cover'},
+              {[html.onlyIfContent]: true},
+
+              // TODO: We shouldn't need to do an isBlank check here,
+              // but a live blank value doesn't have a slot functions, so.
+              (html.isBlank(slots.cover)
+                ? html.blank()
+                : slots.cover.slot('mode', 'thumbnail')))),
         ]),
 
         html.tag('div', {class: 'content-sticky-subheading-row'},
diff --git a/src/content/dependencies/generateTooltip.js b/src/content/dependencies/generateTooltip.js
index 81f74aec..8314d33c 100644
--- a/src/content/dependencies/generateTooltip.js
+++ b/src/content/dependencies/generateTooltip.js
@@ -21,10 +21,13 @@ export default {
   generate: (slots, {html}) =>
     html.tag('span', {class: 'tooltip'},
       {[html.noEdgeWhitespace]: true},
+      {[html.onlyIfContent]: true},
       slots.attributes,
 
       html.tag('span', {class: 'tooltip-content'},
         {[html.noEdgeWhitespace]: true},
+        {[html.onlyIfContent]: true},
         slots.contentAttributes,
+
         slots.content)),
 };
diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js
index f5324519..64ed0cb4 100644
--- a/src/content/dependencies/generateTrackInfoPage.js
+++ b/src/content/dependencies/generateTrackInfoPage.js
@@ -1,333 +1,142 @@
-import {sortAlbumsTracksChronologically, sortFlashesChronologically}
-  from '#sort';
-import {empty, stitchArrays} from '#sugar';
-
-import getChronologyRelations from '../util/getChronologyRelations.js';
-
 export default {
   contentDependencies: [
-    'generateAbsoluteDatetimestamp',
     'generateAlbumAdditionalFilesList',
     'generateAlbumNavAccent',
     'generateAlbumSecondaryNav',
     'generateAlbumSidebar',
     'generateAlbumStyleRules',
-    'generateChronologyLinks',
-    'generateColorStyleAttribute',
     'generateCommentarySection',
     'generateContentHeading',
     'generateContributionList',
     'generatePageLayout',
-    'generateRelativeDatetimestamp',
     'generateTrackAdditionalNamesBox',
     'generateTrackCoverArtwork',
+    'generateTrackInfoPageFeaturedByFlashesList',
+    'generateTrackInfoPageOtherReleasesList',
     'generateTrackList',
     'generateTrackListDividedByGroups',
     'generateTrackReleaseInfo',
     'generateTrackSocialEmbed',
     'linkAlbum',
-    'linkArtist',
-    'linkFlash',
     'linkTrack',
     'transformContent',
   ],
 
   extraDependencies: ['html', 'language', 'wikiData'],
 
-  sprawl({wikiInfo}) {
-    return {
-      divideTrackListsByGroups: wikiInfo.divideTrackListsByGroups,
-      enableFlashesAndGames: wikiInfo.enableFlashesAndGames,
-    };
-  },
-
-  relations(relation, sprawl, track) {
-    const relations = {};
-    const sections = relations.sections = {};
-    const {album} = track;
-
-    relations.layout =
-      relation('generatePageLayout');
-
-    relations.albumStyleRules =
-      relation('generateAlbumStyleRules', track.album, track);
-
-    relations.socialEmbed =
-      relation('generateTrackSocialEmbed', track);
-
-    relations.artistChronologyContributions =
-      getChronologyRelations(track, {
-        contributions: [
-          ...track.artistContribs ?? [],
-          ...track.contributorContribs ?? [],
-        ],
-
-        linkArtist: artist => relation('linkArtist', artist),
-        linkThing: track => relation('linkTrack', track),
-
-        getThings(artist) {
-          const getDate = thing => thing.date;
-
-          const things = [
-            ...artist.tracksAsArtist,
-            ...artist.tracksAsContributor,
-          ].filter(getDate);
-
-          return sortAlbumsTracksChronologically(things, {getDate});
-        },
-      });
-
-    relations.coverArtistChronologyContributions =
-      getChronologyRelations(track, {
-        contributions: track.coverArtistContribs ?? [],
-
-        linkArtist: artist => relation('linkArtist', artist),
-
-        linkThing: trackOrAlbum =>
-          (trackOrAlbum.album
-            ? relation('linkTrack', trackOrAlbum)
-            : relation('linkAlbum', trackOrAlbum)),
-
-        getThings(artist) {
-          const getDate = thing => thing.coverArtDate ?? thing.date;
-
-          const things = [
-            ...artist.albumsAsCoverArtist,
-            ...artist.tracksAsCoverArtist,
-          ].filter(getDate);
-
-          return sortAlbumsTracksChronologically(things, {getDate});
-        },
-      }),
-
-    relations.albumLink =
-      relation('linkAlbum', track.album);
-
-    relations.trackLink =
-      relation('linkTrack', track);
-
-    relations.albumNavAccent =
-      relation('generateAlbumNavAccent', track.album, track);
-
-    relations.chronologyLinks =
-      relation('generateChronologyLinks');
-
-    relations.secondaryNav =
-      relation('generateAlbumSecondaryNav', track.album);
-
-    relations.sidebar =
-      relation('generateAlbumSidebar', track.album, track);
-
-    const additionalFilesSection = additionalFiles => ({
-      heading: relation('generateContentHeading'),
-      list: relation('generateAlbumAdditionalFilesList', album, additionalFiles),
-    });
+  sprawl: ({wikiInfo}) => ({
+    divideTrackListsByGroups:
+      wikiInfo.divideTrackListsByGroups,
+  }),
 
-    // This'll take care of itself being blank if there's nothing to show here.
-    relations.additionalNamesBox =
-      relation('generateTrackAdditionalNamesBox', track);
+  relations: (relation, sprawl, track) => ({
+    layout:
+      relation('generatePageLayout'),
 
-    if (track.hasUniqueCoverArt || album.hasCoverArt) {
-      relations.cover =
-        relation('generateTrackCoverArtwork', track);
-    }
+    albumStyleRules:
+      relation('generateAlbumStyleRules', track.album, track),
 
-    // Section: Release info
+    socialEmbed:
+      relation('generateTrackSocialEmbed', track),
 
-    relations.releaseInfo =
-      relation('generateTrackReleaseInfo', track);
+    albumLink:
+      relation('linkAlbum', track.album),
 
-    // Section: Other releases
+    trackLink:
+      relation('linkTrack', track),
 
-    if (!empty(track.otherReleases)) {
-      const otherReleases = sections.otherReleases = {};
+    albumNavAccent:
+      relation('generateAlbumNavAccent', track.album, track),
 
-      otherReleases.heading =
-        relation('generateContentHeading');
+    secondaryNav:
+      relation('generateAlbumSecondaryNav', track.album),
 
-      otherReleases.colorStyles =
-        track.otherReleases
-          .map(track => relation('generateColorStyleAttribute', track.color));
+    sidebar:
+      relation('generateAlbumSidebar', track.album, track),
 
-      otherReleases.trackLinks =
-        track.otherReleases
-          .map(track => relation('linkTrack', track));
+    additionalNamesBox:
+      relation('generateTrackAdditionalNamesBox', track),
 
-      otherReleases.albumLinks =
-        track.otherReleases
-          .map(track => relation('linkAlbum', track.album));
+    cover:
+      (track.hasUniqueCoverArt || track.album.hasCoverArt
+        ? relation('generateTrackCoverArtwork', track)
+        : null),
 
-      otherReleases.datetimestamps =
-        track.otherReleases.map(track2 =>
-          (track2.date
-            ? (track.date
-                ? relation('generateRelativeDatetimestamp',
-                    track2.date,
-                    track.date)
-                : relation('generateAbsoluteDatetimestamp',
-                    track2.date))
-            : null));
+    contentHeading:
+      relation('generateContentHeading'),
 
-      otherReleases.items =
-        track.otherReleases.map(track => ({
-          trackLink: relation('linkTrack', track),
-          albumLink: relation('linkAlbum', track.album),
-        }));
-    }
+    releaseInfo:
+      relation('generateTrackReleaseInfo', track),
 
-    // Section: Contributors
+    otherReleasesList:
+        relation('generateTrackInfoPageOtherReleasesList', track),
 
-    if (!empty(track.contributorContribs)) {
-      const contributors = sections.contributors = {};
+    contributorContributionList:
+      relation('generateContributionList', track.contributorContribs),
 
-      contributors.heading =
-        relation('generateContentHeading');
+    referencedTracksList:
+      relation('generateTrackList', track.referencedTracks),
 
-      contributors.list =
-        relation('generateContributionList', track.contributorContribs);
-    }
+    sampledTracksList:
+      relation('generateTrackList', track.sampledTracks),
 
-    // Section: Referenced tracks
+    referencedByTracksList:
+      relation('generateTrackListDividedByGroups',
+        track.referencedByTracks,
+        sprawl.divideTrackListsByGroups),
 
-    if (!empty(track.referencedTracks)) {
-      const references = sections.references = {};
+    sampledByTracksList:
+      relation('generateTrackListDividedByGroups',
+        track.sampledByTracks,
+        sprawl.divideTrackListsByGroups),
 
-      references.heading =
-        relation('generateContentHeading');
+    flashesThatFeatureList:
+      relation('generateTrackInfoPageFeaturedByFlashesList', track),
 
-      references.list =
-        relation('generateTrackList', track.referencedTracks);
-    }
+    lyrics:
+      relation('transformContent', track.lyrics),
 
-    // Section: Sampled tracks
+    sheetMusicFilesList:
+      relation('generateAlbumAdditionalFilesList',
+        track.album,
+        track.sheetMusicFiles),
 
-    if (!empty(track.sampledTracks)) {
-      const samples = sections.samples = {};
+    midiProjectFilesList:
+      relation('generateAlbumAdditionalFilesList',
+        track.album,
+        track.midiProjectFiles),
 
-      samples.heading =
-        relation('generateContentHeading');
+    additionalFilesList:
+      relation('generateAlbumAdditionalFilesList',
+        track.album,
+        track.additionalFiles),
 
-      samples.list =
-        relation('generateTrackList', track.sampledTracks);
-    }
+    artistCommentarySection:
+      relation('generateCommentarySection', track.commentary),
+  }),
 
-    // Section: Tracks that reference
+  data: (sprawl, track) => ({
+    name:
+      track.name,
 
-    if (!empty(track.referencedByTracks)) {
-      const referencedBy = sections.referencedBy = {};
+    color:
+      track.color,
 
-      referencedBy.heading =
-        relation('generateContentHeading');
+    hasTrackNumbers:
+      track.album.hasTrackNumbers,
 
-      referencedBy.list =
-        relation('generateTrackListDividedByGroups',
-          track.referencedByTracks,
-          sprawl.divideTrackListsByGroups);
-    }
-
-    // Section: Tracks that sample
-
-    if (!empty(track.sampledByTracks)) {
-      const sampledBy = sections.sampledBy = {};
-
-      sampledBy.heading =
-        relation('generateContentHeading');
-
-      sampledBy.list =
-        relation('generateTrackListDividedByGroups',
-          track.sampledByTracks,
-          sprawl.divideTrackListsByGroups);
-    }
-
-    // Section: Flashes that feature
-
-    if (sprawl.enableFlashesAndGames) {
-      const sortedFeatures =
-        sortFlashesChronologically(
-          [track, ...track.otherReleases].flatMap(track =>
-            track.featuredInFlashes.map(flash => ({
-              // These aren't going to be exposed directly, they're processed
-              // into the appropriate relations after this sort.
-              flash, track,
-
-              // These properties are only used for the sort.
-              act: flash.act,
-              date: flash.date,
-            }))));
-
-      if (!empty(sortedFeatures)) {
-        const flashesThatFeature = sections.flashesThatFeature = {};
-
-        flashesThatFeature.heading =
-          relation('generateContentHeading');
-
-        flashesThatFeature.entries =
-          sortedFeatures.map(({flash, track: directlyFeaturedTrack}) =>
-            (directlyFeaturedTrack === track
-              ? {
-                  flashLink: relation('linkFlash', flash),
-                }
-              : {
-                  flashLink: relation('linkFlash', flash),
-                  trackLink: relation('linkTrack', directlyFeaturedTrack),
-                }));
-      }
-    }
-
-    // Section: Lyrics
-
-    if (track.lyrics) {
-      const lyrics = sections.lyrics = {};
-
-      lyrics.heading =
-        relation('generateContentHeading');
-
-      lyrics.content =
-        relation('transformContent', track.lyrics);
-    }
-
-    // Sections: Sheet music files, MIDI/proejct files, additional files
-
-    if (!empty(track.sheetMusicFiles)) {
-      sections.sheetMusicFiles = additionalFilesSection(track.sheetMusicFiles);
-    }
-
-    if (!empty(track.midiProjectFiles)) {
-      sections.midiProjectFiles = additionalFilesSection(track.midiProjectFiles);
-    }
-
-    if (!empty(track.additionalFiles)) {
-      sections.additionalFiles = additionalFilesSection(track.additionalFiles);
-    }
-
-    // Section: Artist commentary
-
-    if (track.commentary) {
-      sections.artistCommentary =
-        relation('generateCommentarySection', track.commentary);
-    }
-
-    return relations;
-  },
-
-  data(sprawl, track) {
-    return {
-      name: track.name,
-      color: track.color,
+    trackNumber:
+      track.album.tracks.indexOf(track) + 1,
+  }),
 
-      hasTrackNumbers: track.album.hasTrackNumbers,
-      trackNumber: track.album.tracks.indexOf(track) + 1,
-
-      numAdditionalFiles: track.additionalFiles.length,
-    };
-  },
-
-  generate(data, relations, {html, language}) {
-    const {sections: sec} = relations;
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('trackPage', pageCapsule =>
+      relations.layout.slots({
+        title:
+          language.$(pageCapsule, 'title', {
+            track: data.name,
+          }),
 
-    return relations.layout
-      .slots({
-        title: language.$('trackPage.title', {track: data.name}),
         headingMode: 'sticky',
 
         additionalNames: relations.additionalNamesBox,
@@ -349,227 +158,232 @@ export default {
             {[html.onlyIfContent]: true},
             {[html.joinChildren]: html.tag('br')},
 
-            [
-              sec.sheetMusicFiles &&
-                language.$('releaseInfo.sheetMusicFiles.shortcut', {
-                  link: html.tag('a',
-                    {href: '#sheet-music-files'},
-                    language.$('releaseInfo.sheetMusicFiles.shortcut.link')),
-                }),
+            language.encapsulate('releaseInfo', capsule => [
+              !html.isBlank(relations.sheetMusicFilesList) &&
+                language.encapsulate(capsule, 'sheetMusicFiles.shortcut', capsule =>
+                  language.$(capsule, {
+                    link:
+                      html.tag('a',
+                        {href: '#sheet-music-files'},
+                        language.$(capsule, 'link')),
+                  })),
 
-              sec.midiProjectFiles &&
-                language.$('releaseInfo.midiProjectFiles.shortcut', {
-                  link: html.tag('a',
-                    {href: '#midi-project-files'},
-                    language.$('releaseInfo.midiProjectFiles.shortcut.link')),
-                }),
+              !html.isBlank(relations.midiProjectFilesList) &&
+                language.encapsulate(capsule, 'midiProjectFiles.shortcut', capsule =>
+                  language.$(capsule, {
+                    link:
+                      html.tag('a',
+                        {href: '#midi-project-files'},
+                        language.$(capsule, 'link')),
+                  })),
 
-              sec.additionalFiles &&
-                language.$('releaseInfo.additionalFiles.shortcut', {
-                  link: html.tag('a',
-                    {href: '#midi-project-files'},
-                    language.$('releaseInfo.additionalFiles.shortcut.link')),
-                }),
+              !html.isBlank(relations.additionalFilesList) &&
+                language.encapsulate(capsule, 'additionalFiles.shortcut', capsule =>
+                  language.$(capsule, {
+                    link:
+                      html.tag('a',
+                        {href: '#midi-project-files'},
+                        language.$(capsule, 'link')),
+                  })),
 
-              sec.artistCommentary &&
-                language.$('releaseInfo.readCommentary', {
-                  link: html.tag('a',
-                    {href: '#artist-commentary'},
-                    language.$('releaseInfo.readCommentary.link')),
-                }),
-            ]),
+              !html.isBlank(relations.artistCommentarySection) &&
+                language.encapsulate(capsule, 'readCommentary', capsule =>
+                  language.$(capsule, {
+                    link:
+                      html.tag('a',
+                        {href: '#artist-commentary'},
+                        language.$(capsule, 'link')),
+                  })),
+            ])),
 
-          sec.otherReleases && [
-            sec.otherReleases.heading
+          html.tags([
+            relations.contentHeading.clone()
               .slots({
-                id: 'also-released-as',
+                attributes: {id: 'also-released-as'},
                 title: language.$('releaseInfo.alsoReleasedAs'),
               }),
 
-            html.tag('ul',
-              stitchArrays({
-                trackLink: sec.otherReleases.trackLinks,
-                albumLink: sec.otherReleases.albumLinks,
-                datetimestamp: sec.otherReleases.datetimestamps,
-                colorStyle: sec.otherReleases.colorStyles,
-              }).map(({
-                  trackLink,
-                  albumLink,
-                  datetimestamp,
-                  colorStyle,
-                }) => {
-                  const parts = ['releaseInfo.alsoReleasedAs.item'];
-                  const options = {};
-
-                  options.track = trackLink.slot('color', false);
-                  options.album = albumLink;
-
-                  if (datetimestamp) {
-                    parts.push('withYear');
-                    options.year =
-                      datetimestamp.slots({
-                        style: 'year',
-                        tooltip: true,
-                      });
-                  }
-
-                  return (
-                    html.tag('li',
-                      colorStyle,
-                      language.$(...parts, options)));
-                })),
-          ],
+            relations.otherReleasesList,
+          ]),
 
-          sec.contributors && [
-            sec.contributors.heading
+          html.tags([
+            relations.contentHeading.clone()
               .slots({
-                id: 'contributors',
+                attributes: {id: 'contributors'},
                 title: language.$('releaseInfo.contributors'),
               }),
 
-            sec.contributors.list,
-          ],
+            relations.contributorContributionList.slots({
+              chronologyKind: 'trackContribution',
+            }),
+          ]),
+
+          html.tags([
+            language.encapsulate('releaseInfo.tracksReferenced', capsule =>
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'references'},
+
+                  title:
+                    language.$(capsule, {
+                      track:
+                        html.tag('i', data.name),
+                    }),
+
+                  stickyTitle:
+                    language.$(capsule, 'sticky'),
+                })),
 
-          sec.references && [
-            sec.references.heading
-              .slots({
-                id: 'references',
-                title:
-                  language.$('releaseInfo.tracksReferenced', {
-                    track: html.tag('i', data.name),
-                  }),
-              }),
+            relations.referencedTracksList,
+          ]),
 
-            sec.references.list,
-          ],
+          html.tags([
+            language.encapsulate('releaseInfo.tracksSampled', capsule =>
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'samples'},
 
-          sec.samples && [
-            sec.samples.heading
-              .slots({
-                id: 'samples',
-                title:
-                  language.$('releaseInfo.tracksSampled', {
-                    track: html.tag('i', data.name),
-                  }),
-              }),
+                  title:
+                    language.$(capsule, {
+                      track:
+                        html.tag('i', data.name),
+                    }),
 
-            sec.samples.list,
-          ],
+                  stickyTitle:
+                    language.$(capsule, 'sticky'),
+                })),
 
-          sec.referencedBy && [
-            sec.referencedBy.heading
-              .slots({
-                id: 'referenced-by',
-                title:
-                  language.$('releaseInfo.tracksThatReference', {
-                    track: html.tag('i', data.name),
-                  }),
-              }),
+            relations.sampledTracksList,
+          ]),
 
-            sec.referencedBy.list,
-          ],
+          language.encapsulate('releaseInfo.tracksThatReference', capsule =>
+            html.tags([
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'referenced-by'},
 
-          sec.sampledBy && [
-            sec.sampledBy.heading
-              .slots({
-                id: 'referenced-by',
-                title:
-                  language.$('releaseInfo.tracksThatSample', {
-                    track: html.tag('i', data.name),
-                  }),
-              }),
+                  title:
+                    language.$(capsule, {
+                      track: html.tag('i', data.name),
+                    }),
 
-            sec.sampledBy.list,
-          ],
+                  stickyTitle:
+                    language.$(capsule, 'sticky'),
+                }),
 
-          sec.flashesThatFeature && [
-            sec.flashesThatFeature.heading
-              .slots({
-                id: 'featured-in',
-                title:
-                  language.$('releaseInfo.flashesThatFeature', {
-                    track: html.tag('i', data.name),
-                  }),
-              }),
+              relations.referencedByTracksList
+                .slots({
+                  headingString: capsule,
+                }),
+            ])),
+
+          language.encapsulate('releaseInfo.tracksThatSample', capsule =>
+            html.tags([
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'sampled-by'},
+
+                  title:
+                    language.$(capsule, {
+                      track: html.tag('i', data.name),
+                    }),
+
+                  stickyTitle:
+                    language.$(capsule, 'sticky'),
+                }),
+
+              relations.sampledByTracksList
+                .slots({
+                  headingString: capsule,
+                }),
+            ])),
+
+          html.tags([
+            language.encapsulate('releaseInfo.flashesThatFeature', capsule =>
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'featured-in'},
+
+                  title:
+                    language.$(capsule, {
+                      track: html.tag('i', data.name),
+                    }),
+
+                  stickyTitle:
+                    language.$(capsule, 'sticky'),
+                })),
+
+            relations.flashesThatFeatureList,
+          ]),
 
-            html.tag('ul', sec.flashesThatFeature.entries.map(({flashLink, trackLink}) =>
-              (trackLink
-                ? html.tag('li', {class: 'rerelease'},
-                    language.$('releaseInfo.flashesThatFeature.item.asDifferentRelease', {
-                      flash: flashLink,
-                      track: trackLink,
-                    }))
-                : html.tag('li',
-                    language.$('releaseInfo.flashesThatFeature.item', {
-                      flash: flashLink,
-                    }))))),
-          ],
-
-          sec.lyrics && [
-            sec.lyrics.heading
+          html.tags([
+            relations.contentHeading.clone()
               .slots({
-                id: 'lyrics',
+                attributes: {id: 'lyrics'},
                 title: language.$('releaseInfo.lyrics'),
               }),
 
             html.tag('blockquote',
-              sec.lyrics.content
-                .slot('mode', 'lyrics')),
-          ],
+              {[html.onlyIfContent]: true},
+              relations.lyrics.slot('mode', 'lyrics')),
+          ]),
 
-          sec.sheetMusicFiles && [
-            sec.sheetMusicFiles.heading
+          html.tags([
+            relations.contentHeading.clone()
               .slots({
-                id: 'sheet-music-files',
+                attributes: {id: 'sheet-music-files'},
                 title: language.$('releaseInfo.sheetMusicFiles.heading'),
               }),
 
-            sec.sheetMusicFiles.list,
-          ],
+            relations.sheetMusicFilesList,
+          ]),
 
-          sec.midiProjectFiles && [
-            sec.midiProjectFiles.heading
+          html.tags([
+            relations.contentHeading.clone()
               .slots({
-                id: 'midi-project-files',
+                attributes: {id: 'midi-project-files'},
                 title: language.$('releaseInfo.midiProjectFiles.heading'),
               }),
 
-            sec.midiProjectFiles.list,
-          ],
+            relations.midiProjectFilesList,
+          ]),
 
-          sec.additionalFiles && [
-            sec.additionalFiles.heading
+          html.tags([
+            relations.contentHeading.clone()
               .slots({
-                id: 'additional-files',
-                title:
-                  language.$('releaseInfo.additionalFiles.heading', {
-                    additionalFiles:
-                      language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}),
-                  }),
+                attributes: {id: 'additional-files'},
+                title: language.$('releaseInfo.additionalFiles.heading'),
               }),
 
-            sec.additionalFiles.list,
-          ],
+            relations.additionalFilesList,
+          ]),
 
-          sec.artistCommentary,
+          relations.artistCommentarySection,
         ],
 
         navLinkStyle: 'hierarchical',
+
         navLinks: [
           {auto: 'home'},
+
           {html: relations.albumLink.slot('color', false)},
+
           {
             html:
-              (data.hasTrackNumbers
-                ? language.$('trackPage.nav.track.withNumber', {
-                    number: data.trackNumber,
-                    track: relations.trackLink
-                      .slot('attributes', {class: 'current'}),
-                  })
-                : language.$('trackPage.nav.track', {
-                    track: relations.trackLink
-                      .slot('attributes', {class: 'current'}),
-                  })),
+              language.encapsulate(pageCapsule, 'nav.track', workingCapsule => {
+                const workingOptions = {};
+
+                workingOptions.track =
+                  relations.trackLink
+                    .slot('attributes', {class: 'current'});
+
+                if (data.hasTrackNumbers) {
+                  workingCapsule += '.withNumber';
+                  workingOptions.number = data.trackNumber;
+                }
+
+                return language.$(workingCapsule, workingOptions);
+              }),
           },
         ],
 
@@ -579,20 +393,6 @@ export default {
             showExtraLinks: false,
           }),
 
-        navContent:
-          relations.chronologyLinks.slots({
-            chronologyInfoSets: [
-              {
-                headingString: 'misc.chronology.heading.track',
-                contributions: relations.artistChronologyContributions,
-              },
-              {
-                headingString: 'misc.chronology.heading.coverArt',
-                contributions: relations.coverArtistChronologyContributions,
-              },
-            ],
-          }),
-
         secondaryNav:
           relations.secondaryNav
             .slot('mode', 'track'),
@@ -600,8 +400,7 @@ export default {
         leftSidebar: relations.sidebar,
 
         socialEmbed: relations.socialEmbed,
-      });
-  },
+      })),
 };
 
 /*
diff --git a/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js
new file mode 100644
index 00000000..5958be9a
--- /dev/null
+++ b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js
@@ -0,0 +1,62 @@
+import {sortFlashesChronologically} from '#sort';
+import {stitchArrays} from '#sugar';
+
+export default {
+  contentDependencies: ['linkFlash', 'linkTrack'],
+  extraDependencies: ['html', 'language', 'wikiData'],
+
+  sprawl: ({wikiInfo}) => ({
+    enableFlashesAndGames:
+      wikiInfo.enableFlashesAndGames,
+  }),
+
+  query: (sprawl, track) => ({
+    sortedFeatures:
+      (sprawl.enableFlashesAndGames
+        ? sortFlashesChronologically(
+            [track, ...track.otherReleases].flatMap(track =>
+              track.featuredInFlashes.map(flash => ({
+                flash,
+                track,
+
+                // These properties are only used for the sort.
+                act: flash.act,
+                date: flash.date,
+              }))))
+        : []),
+  }),
+
+  relations: (relation, query, _sprawl, track) => ({
+    flashLinks:
+      query.sortedFeatures
+        .map(({flash}) => relation('linkFlash', flash)),
+
+    trackLinks:
+      query.sortedFeatures
+        .map(({track: directlyFeaturedTrack}) =>
+          (directlyFeaturedTrack === track
+            ? null
+            : relation('linkTrack', directlyFeaturedTrack))),
+  }),
+
+  generate: (relations, {html, language}) =>
+    html.tag('ul',
+      {[html.onlyIfContent]: true},
+
+      stitchArrays({
+        flashLink: relations.flashLinks,
+        trackLink: relations.trackLinks,
+      }).map(({flashLink, trackLink}) => {
+          const attributes = html.attributes();
+          const parts = ['releaseInfo.flashesThatFeature.item'];
+          const options = {flash: flashLink};
+
+          if (trackLink) {
+            attributes.add('class', 'rerelease');
+            parts.push('asDifferentRelease');
+            options.track = trackLink;
+          }
+
+          return html.tag('li', attributes, language.$(...parts, options));
+        })),
+};
diff --git a/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js
new file mode 100644
index 00000000..004bba6d
--- /dev/null
+++ b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js
@@ -0,0 +1,80 @@
+import {stitchArrays} from '#sugar';
+
+export default {
+  contentDependencies: [
+    'generateAbsoluteDatetimestamp',
+    'generateColorStyleAttribute',
+    'generateRelativeDatetimestamp',
+    'linkAlbum',
+    'linkTrack',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  relations: (relation, track) => ({
+    colorStyles:
+      track.otherReleases
+        .map(track => relation('generateColorStyleAttribute', track.color)),
+
+    trackLinks:
+      track.otherReleases
+        .map(track => relation('linkTrack', track)),
+
+    albumLinks:
+      track.otherReleases
+        .map(track => relation('linkAlbum', track.album)),
+
+    datetimestamps:
+      track.otherReleases.map(track2 =>
+        (track2.date
+          ? (track.date
+              ? relation('generateRelativeDatetimestamp',
+                  track2.date,
+                  track.date)
+              : relation('generateAbsoluteDatetimestamp',
+                  track2.date))
+          : null)),
+
+    items:
+      track.otherReleases.map(track => ({
+        trackLink: relation('linkTrack', track),
+        albumLink: relation('linkAlbum', track.album),
+      })),
+  }),
+
+  generate: (relations, {html, language}) =>
+    html.tag('ul',
+      {[html.onlyIfContent]: true},
+
+      stitchArrays({
+        trackLink: relations.trackLinks,
+        albumLink: relations.albumLinks,
+        datetimestamp: relations.datetimestamps,
+        colorStyle: relations.colorStyles,
+      }).map(({
+          trackLink,
+          albumLink,
+          datetimestamp,
+          colorStyle,
+        }) => {
+          const parts = ['releaseInfo.alsoReleasedAs.item'];
+          const options = {};
+
+          options.track = trackLink.slot('color', false);
+          options.album = albumLink;
+
+          if (datetimestamp) {
+            parts.push('withYear');
+            options.year =
+              datetimestamp.slots({
+                style: 'year',
+                tooltip: true,
+              });
+          }
+
+          return (
+            html.tag('li',
+              colorStyle,
+              language.$(...parts, options)));
+        })),
+};
diff --git a/src/content/dependencies/generateTrackList.js b/src/content/dependencies/generateTrackList.js
index 3c36d248..7c3b11c1 100644
--- a/src/content/dependencies/generateTrackList.js
+++ b/src/content/dependencies/generateTrackList.js
@@ -5,55 +5,42 @@ export default {
 
   extraDependencies: ['html', 'language'],
 
-  relations(relation, tracks) {
-    if (empty(tracks)) {
-      return {};
-    }
-
-    return {
-      trackLinks:
-        tracks
-          .map(track => relation('linkTrack', track)),
-
-      contributionLinks:
-        tracks
-          .map(track =>
-            (empty(track.artistContribs)
-              ? null
-              : track.artistContribs
-                  .map(contrib => relation('linkContribution', contrib)))),
-    };
-  },
-
-  slots: {
-    showContribution: {type: 'boolean', default: false},
-    showIcons: {type: 'boolean', default: false},
-  },
-
-  generate(relations, slots, {html, language}) {
-    return (
-      html.tag('ul',
-        stitchArrays({
-          trackLink: relations.trackLinks,
-          contributionLinks: relations.contributionLinks,
-        }).map(({trackLink, contributionLinks}) =>
-            html.tag('li',
-              (empty(contributionLinks)
-                ? trackLink
-                : language.$('trackList.item.withArtists', {
-                    track: trackLink,
-                    by:
-                      html.tag('span', {class: 'by'},
-                        html.metatag('chunkwrap', {split: ','},
-                          language.$('trackList.item.withArtists.by', {
-                            artists:
-                              language.formatConjunctionList(
-                                contributionLinks.map(link =>
-                                  link.slots({
-                                    showContribution: slots.showContribution,
-                                    showIcons: slots.showIcons,
-                                  }))),
-                          }))),
-                  }))))));
-  },
+  relations: (relation, tracks) => ({
+    trackLinks:
+      tracks
+        .map(track => relation('linkTrack', track)),
+
+    contributionLinks:
+      tracks
+        .map(track =>
+          track.artistContribs
+            .map(contrib => relation('linkContribution', contrib))),
+  }),
+
+  generate: (relations, {html, language}) =>
+    html.tag('ul',
+      {[html.onlyIfContent]: true},
+
+      stitchArrays({
+        trackLink: relations.trackLinks,
+        contributionLinks: relations.contributionLinks,
+      }).map(({trackLink, contributionLinks}) =>
+          html.tag('li',
+            language.encapsulate('trackList.item', itemCapsule =>
+              language.encapsulate(itemCapsule, workingCapsule => {
+                const workingOptions = {track: trackLink};
+
+                if (!empty(contributionLinks)) {
+                  workingCapsule += '.withArtists';
+                  workingOptions.by =
+                    html.tag('span', {class: 'by'},
+                      html.metatag('chunkwrap', {split: ','},
+                        language.$(itemCapsule, 'withArtists.by', {
+                          artists:
+                            language.formatConjunctionList(contributionLinks),
+                        })));
+                }
+
+                return language.$(workingCapsule, workingOptions);
+              }))))),
 };
diff --git a/src/content/dependencies/generateTrackListDividedByGroups.js b/src/content/dependencies/generateTrackListDividedByGroups.js
index e070ac35..3cba479e 100644
--- a/src/content/dependencies/generateTrackListDividedByGroups.js
+++ b/src/content/dependencies/generateTrackListDividedByGroups.js
@@ -1,53 +1,138 @@
-import {empty} from '#sugar';
-
-import groupTracksByGroup from '../util/groupTracksByGroup.js';
+import {empty, filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateTrackList', 'linkGroup'],
+  contentDependencies: [
+    'generateContentHeading',
+    'generateTrackList',
+    'linkGroup',
+  ],
+
   extraDependencies: ['html', 'language'],
 
-  relations(relation, tracks, groups) {
-    if (empty(tracks)) {
-      return {};
+  query(tracks, dividingGroups) {
+    const groupings = new Map();
+    const ungroupedTracks = [];
+
+    // Entry order matters! Add blank lists for each group
+    // in the order that those groups are provided.
+    for (const group of dividingGroups) {
+      groupings.set(group, []);
     }
 
-    if (empty(groups)) {
-      return {
-        flatList:
-          relation('generateTrackList', tracks),
-      };
+    for (const track of tracks) {
+      const firstMatchingGroup =
+        dividingGroups.find(group => group.albums.includes(track.album));
+
+      if (firstMatchingGroup) {
+        groupings.get(firstMatchingGroup).push(track);
+      } else {
+        ungroupedTracks.push(track);
+      }
     }
 
-    const lists = groupTracksByGroup(tracks, groups);
+    const groups = Array.from(groupings.keys());
+    const groupedTracks = Array.from(groupings.values());
 
-    return {
-      groupedLists:
-        Array.from(lists.entries()).map(([groupOrOther, tracks]) => ({
-          ...(groupOrOther === 'other'
-                ? {other: true}
-                : {groupLink: relation('linkGroup', groupOrOther)}),
+    // Drop the empty lists, so just the groups which
+    // at least a single track matched are left.
+    filterMultipleArrays(
+      groups,
+      groupedTracks,
+      (_group, tracks) => !empty(tracks));
 
-          list:
-            relation('generateTrackList', tracks),
-        })),
-    };
+    return {groups, groupedTracks, ungroupedTracks};
   },
 
-  generate(relations, {html, language}) {
-    if (relations.flatList) {
-      return relations.flatList;
-    }
+  relations: (relation, query, tracks, groups) => ({
+    flatList:
+      (empty(groups)
+        ? relation('generateTrackList', tracks)
+        : null),
+
+    contentHeading:
+      relation('generateContentHeading'),
+
+    groupLinks:
+      query.groups
+        .map(group => relation('linkGroup', group)),
+
+    groupedTrackLists:
+      query.groupedTracks
+        .map(tracks => relation('generateTrackList', tracks)),
+
+    ungroupedTrackList:
+      (empty(query.ungroupedTracks)
+        ? null
+        : relation('generateTrackList', query.ungroupedTracks)),
+  }),
+
+  data: (query) => ({
+    groupNames:
+      query.groups
+        .map(group => group.name),
+  }),
 
-    return html.tag('dl',
-      relations.groupedLists.map(({other, groupLink, list}) => [
-        html.tag('dt',
-          (other
-            ? language.$('trackList.group.fromOther')
-            : language.$('trackList.group', {
-                group: groupLink
-              }))),
-
-        html.tag('dd', list),
-      ]));
+  slots: {
+    headingString: {
+      type: 'string',
+    },
   },
+
+  generate: (data, relations, slots, {html, language}) =>
+    relations.flatList ??
+
+    html.tag('dl',
+      {[html.onlyIfContent]: true},
+
+      language.encapsulate('trackList', listCapsule => [
+        stitchArrays({
+          groupName: data.groupNames,
+          groupLink: relations.groupLinks,
+          trackList: relations.groupedTrackLists,
+        }).map(({
+            groupName,
+            groupLink,
+            trackList,
+          }) => [
+            language.encapsulate(listCapsule, 'fromGroup', capsule =>
+              (slots.headingString
+                ? relations.contentHeading.clone().slots({
+                    tag: 'dt',
+
+                    title:
+                      language.$(capsule, {
+                        group: groupLink
+                      }),
+
+                    stickyTitle:
+                      language.$(slots.headingString, 'sticky', 'fromGroup', {
+                        group: groupName,
+                      }),
+                  })
+                : html.tag('dt',
+                    language.$(capsule, {
+                      group: groupLink
+                    })))),
+
+            html.tag('dd', trackList),
+          ]),
+
+        relations.ungroupedTrackList && [
+          language.encapsulate(listCapsule, 'fromOther', capsule =>
+            (slots.headingString
+              ? relations.contentHeading.clone().slots({
+                  tag: 'dt',
+
+                  title:
+                    language.$(capsule),
+
+                  stickyTitle:
+                    language.$(slots.headingString, 'sticky', 'fromOther'),
+                })
+              : html.tag('dt',
+                  language.$(capsule)))),
+
+          html.tag('dd', relations.ungroupedTrackList),
+        ],
+      ])),
 };
diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js
index 3bdeaa4f..8a081046 100644
--- a/src/content/dependencies/generateTrackReleaseInfo.js
+++ b/src/content/dependencies/generateTrackReleaseInfo.js
@@ -47,44 +47,51 @@ export default {
   },
 
   generate: (data, relations, {html, language}) =>
-    html.tags([
-      html.tag('p',
-        {[html.onlyIfContent]: true},
-        {[html.joinChildren]: html.tag('br')},
-
-        [
-          relations.artistContributionLinks
-            .slots({stringKey: 'releaseInfo.by'}),
+    language.encapsulate('releaseInfo', capsule =>
+      html.tags([
+        html.tag('p',
+          {[html.onlyIfContent]: true},
+          {[html.joinChildren]: html.tag('br')},
+
+          [
+            relations.artistContributionLinks.slots({
+              stringKey: capsule + '.by',
+              chronologyKind: 'track',
+            }),
 
-          relations.coverArtistContributionsLine
-            ?.slots({stringKey: 'releaseInfo.coverArtBy'}),
+            relations.coverArtistContributionsLine?.slots({
+              stringKey: capsule + '.coverArtBy',
+              chronologyKind: 'trackArt',
+            }),
 
-          data.date &&
-            language.$('releaseInfo.released', {
+            language.$(capsule, 'released', {
+              [language.onlyIfOptions]: ['date'],
               date: language.formatDate(data.date),
             }),
 
-          data.coverArtDate &&
-            language.$('releaseInfo.artReleased', {
+            language.$(capsule, 'artReleased', {
+              [language.onlyIfOptions]: ['date'],
               date: language.formatDate(data.coverArtDate),
             }),
 
-          data.duration &&
-            language.$('releaseInfo.duration', {
+            language.$(capsule, 'duration', {
+              [language.onlyIfOptions]: ['duration'],
               duration: language.formatDuration(data.duration),
             }),
-        ]),
-
-      html.tag('p',
-        (relations.externalLinks
-          ? language.$('releaseInfo.listenOn', {
-              links:
-                language.formatDisjunctionList(
-                  relations.externalLinks
-                    .map(link => link.slot('context', 'track'))),
-            })
-          : language.$('releaseInfo.listenOn.noLinks', {
-              name: html.tag('i', data.name),
-            }))),
-    ]),
+          ]),
+
+        html.tag('p',
+          language.encapsulate(capsule, 'listenOn', capsule =>
+            (relations.externalLinks
+              ? language.$(capsule, {
+                  links:
+                    language.formatDisjunctionList(
+                      relations.externalLinks
+                        .map(link => link.slot('context', 'track'))),
+                })
+              : language.$(capsule, 'noLinks', {
+                  name:
+                    html.tag('i', data.name),
+                })))),
+      ])),
 };
diff --git a/src/content/dependencies/generateTrackSocialEmbed.js b/src/content/dependencies/generateTrackSocialEmbed.js
index 0337fc46..9868f0e2 100644
--- a/src/content/dependencies/generateTrackSocialEmbed.js
+++ b/src/content/dependencies/generateTrackSocialEmbed.js
@@ -39,35 +39,35 @@ export default {
     return data;
   },
 
-  generate(data, relations, {absoluteTo, language, urls}) {
-    return relations.socialEmbed.slots({
-      title:
-        language.$('trackPage.socialEmbed.title', {
-          track: data.trackName,
-        }),
+  generate: (data, relations, {absoluteTo, language, urls}) =>
+    language.encapsulate('trackPage.socialEmbed', embedCapsule =>
+      relations.socialEmbed.slots({
+        title:
+          language.$(embedCapsule, 'title', {
+            track: data.trackName,
+          }),
 
-      headingContent:
-        language.$('trackPage.socialEmbed.heading', {
-          album: data.albumName,
-        }),
+        headingContent:
+          language.$(embedCapsule, 'heading', {
+            album: data.albumName,
+          }),
 
-      headingLink:
-        absoluteTo('localized.album', data.albumDirectory),
+        headingLink:
+          absoluteTo('localized.album', data.albumDirectory),
 
-      imagePath:
-        (data.imageSource === 'album'
-          ? '/' +
-            urls
-              .from('shared.root')
-              .to('media.albumCover', data.albumDirectory, data.coverArtFileExtension)
-       : data.imageSource === 'track'
-          ? '/' +
-            urls
-              .from('shared.root')
-              .to('media.trackCover', data.albumDirectory, data.trackDirectory, data.coverArtFileExtension)
-          : null),
-    });
-  },
+        imagePath:
+          (data.imageSource === 'album'
+            ? '/' +
+              urls
+                .from('shared.root')
+                .to('media.albumCover', data.albumDirectory, data.coverArtFileExtension)
+         : data.imageSource === 'track'
+            ? '/' +
+              urls
+                .from('shared.root')
+                .to('media.trackCover', data.albumDirectory, data.trackDirectory, data.coverArtFileExtension)
+            : null),
+      })),
 };
 
 /*
diff --git a/src/content/dependencies/generateWikiHomeAlbumsRow.js b/src/content/dependencies/generateWikiHomeAlbumsRow.js
index a19f104c..16c22bb3 100644
--- a/src/content/dependencies/generateWikiHomeAlbumsRow.js
+++ b/src/content/dependencies/generateWikiHomeAlbumsRow.js
@@ -113,10 +113,10 @@ export default {
           image.slots({
             path,
             missingSourceContent:
-              name &&
-                language.$('misc.albumGrid.noCoverArt', {
-                  album: name,
-                }),
+              language.$('misc.albumGrid.noCoverArt', {
+                [language.onlyIfOptions]: ['album'],
+                album: name,
+              }),
             }));
 
     commonSlots.actionLinks =
diff --git a/src/content/dependencies/generateWikiHomeNewsBox.js b/src/content/dependencies/generateWikiHomeNewsBox.js
index bd0e4797..83a27695 100644
--- a/src/content/dependencies/generateWikiHomeNewsBox.js
+++ b/src/content/dependencies/generateWikiHomeNewsBox.js
@@ -1,4 +1,4 @@
-import {empty, stitchArrays} from '#sugar';
+import {stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
@@ -39,49 +39,48 @@ export default {
         .map(entry => entry.date),
   }),
 
-  generate(data, relations, {html, language}) {
-    if (empty(relations.entryContents)) {
-      return html.blank();
-    }
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('homepage.news', boxCapsule =>
+      relations.box.slots({
+        attributes: {class: 'latest-news-sidebar-box'},
+        collapsible: false,
 
-    return relations.box.slots({
-      attributes: {class: 'latest-news-sidebar-box'},
-      collapsible: false,
+        content: [
+          html.tag('h1',
+            {[html.onlyIfSiblings]: true},
+            language.$(boxCapsule, 'title')),
 
-      content: [
-        html.tag('h1', language.$('homepage.news.title')),
+          stitchArrays({
+            date: data.entryDates,
+            content: relations.entryContents,
+            mainLink: relations.entryMainLinks,
+            readMoreLink: relations.entryReadMoreLinks,
+          }).map(({
+              date,
+              content,
+              mainLink,
+              readMoreLink,
+            }, index) =>
+              language.encapsulate(boxCapsule, 'entry', entryCapsule =>
+                html.tag('article', {class: 'news-entry'},
+                  index === 0 &&
+                    {class: 'first-news-entry'},
 
-        stitchArrays({
-          date: data.entryDates,
-          content: relations.entryContents,
-          mainLink: relations.entryMainLinks,
-          readMoreLink: relations.entryReadMoreLinks,
-        }).map(({
-            date,
-            content,
-            mainLink,
-            readMoreLink,
-          }, index) =>
-            html.tag('article', {class: 'news-entry'},
-              index === 0 &&
-                {class: 'first-news-entry'},
+                  [
+                    html.tag('h2', [
+                      html.tag('time', language.formatDate(date)),
+                      mainLink,
+                    ]),
 
-              [
-                html.tag('h2', [
-                  html.tag('time', language.formatDate(date)),
-                  mainLink,
-                ]),
+                    content.slot('thumb', 'medium'),
 
-                content.slot('thumb', 'medium'),
-
-                html.tag('p',
-                  {[html.onlyIfContent]: true},
-                  readMoreLink
-                    ?.slots({
-                      content: language.$('homepage.news.entry.viewRest'),
-                    })),
-              ])),
-      ],
-    });
-  },
+                    html.tag('p',
+                      {[html.onlyIfContent]: true},
+                      readMoreLink
+                        ?.slots({
+                          content: language.$(entryCapsule, 'viewRest'),
+                        })),
+                  ]))),
+        ],
+      })),
 };
diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js
index 822efe3f..b1f02819 100644
--- a/src/content/dependencies/image.js
+++ b/src/content/dependencies/image.js
@@ -3,7 +3,6 @@ import {empty} from '#sugar';
 
 export default {
   extraDependencies: [
-    'cachebust',
     'checkIfImagePathHasCachedThumbnails',
     'getDimensionsOfImagePath',
     'getSizeOfImagePath',
@@ -82,7 +81,6 @@ export default {
   },
 
   generate(data, relations, slots, {
-    cachebust,
     checkIfImagePathHasCachedThumbnails,
     getDimensionsOfImagePath,
     getSizeOfImagePath,
@@ -133,15 +131,8 @@ export default {
       !isMissingImageFile &&
       !empty(contentWarnings);
 
-    const hasBothDimensions =
-      !!(slots.dimensions &&
-         slots.dimensions[0] !== null &&
-         slots.dimensions[1] !== null);
-
     const willSquare =
-      (hasBothDimensions
-        ? slots.dimensions[0] === slots.dimensions[1]
-        : slots.square);
+      slots.square;
 
     const imgAttributes = html.attributes([
       {class: 'image'},
@@ -152,7 +143,7 @@ export default {
         {width: slots.dimensions[0]},
 
       slots.dimensions?.[1] &&
-        {width: slots.dimensions[1]},
+        {height: slots.dimensions[1]},
     ]);
 
     const isPlaceholder =
@@ -172,7 +163,7 @@ export default {
     if (willReveal) {
       reveal = [
         html.tag('img', {class: 'reveal-symbol'},
-          {src: to('shared.staticFile', 'warning.svg', cachebust)}),
+          {src: to('staticMisc.path', 'warning.svg')}),
 
         html.tag('br'),
 
diff --git a/src/content/dependencies/linkAnythingMan.js b/src/content/dependencies/linkAnythingMan.js
new file mode 100644
index 00000000..d4697403
--- /dev/null
+++ b/src/content/dependencies/linkAnythingMan.js
@@ -0,0 +1,25 @@
+export default {
+  contentDependencies: [
+    'linkAlbum',
+    'linkFlash',
+    'linkTrack',
+  ],
+
+  query: (thing) => ({
+    referenceType: thing.constructor[Symbol.for('Thing.referenceType')],
+  }),
+
+  relations: (relation, query, thing) => ({
+    link:
+      (query.referenceType === 'album'
+        ? relation('linkAlbum', thing)
+     : query.referenceType === 'flash'
+        ? relation('linkFlash', thing)
+     : query.referenceType === 'track'
+        ? relation('linkTrack', thing)
+        : null),
+  }),
+
+  generate: (relations) =>
+    relations.link,
+};
diff --git a/src/content/dependencies/linkContribution.js b/src/content/dependencies/linkContribution.js
index 1a51c387..26f0b2d7 100644
--- a/src/content/dependencies/linkContribution.js
+++ b/src/content/dependencies/linkContribution.js
@@ -1,145 +1,78 @@
-import {empty, stitchArrays} from '#sugar';
-
 export default {
   contentDependencies: [
+    'generateContributionTooltip',
     'generateTextWithTooltip',
-    'generateTooltip',
     'linkArtist',
-    'linkExternalAsIcon',
   ],
 
   extraDependencies: ['html', 'language'],
 
-  relations(relation, contribution) {
-    const relations = {};
-
-    relations.artistLink =
-      relation('linkArtist', contribution.artist);
+  relations: (relation, contribution) => ({
+    artistLink:
+      relation('linkArtist', contribution.artist),
 
-    relations.textWithTooltip =
-      relation('generateTextWithTooltip');
+    textWithTooltip:
+      relation('generateTextWithTooltip'),
 
-    relations.tooltip =
-      relation('generateTooltip');
+    tooltip:
+      relation('generateContributionTooltip', contribution),
+  }),
 
-    if (!empty(contribution.artist.urls)) {
-      relations.artistIcons =
-        contribution.artist.urls
-          .map(url => relation('linkExternalAsIcon', url));
-    }
-
-    return relations;
-  },
-
-  data(contribution) {
-    return {
-      contribution: contribution.annotation,
-      urls: contribution.artist.urls,
-    };
-  },
+  data: (contribution) => ({
+    contribution: contribution.annotation,
+    urls: contribution.artist.urls,
+  }),
 
   slots: {
     showContribution: {type: 'boolean', default: false},
-    showIcons: {type: 'boolean', default: false},
-    preventWrapping: {type: 'boolean', default: true},
+    showExternalLinks: {type: 'boolean', default: false},
+    showChronology: {type: 'boolean', default: false},
 
-    iconMode: {
-      validate: v => v.is('inline', 'tooltip'),
-      default: 'inline'
-    },
+    preventWrapping: {type: 'boolean', default: true},
+    chronologyKind: {type: 'string'},
   },
 
-  generate(data, relations, slots, {html, language}) {
-    const hasContribution = !!(slots.showContribution && data.contribution);
-    const hasExternalIcons = !!(slots.showIcons && relations.artistIcons);
-
-    const parts = ['misc.artistLink'];
-    const options = {};
-
-    options.artist =
-      (hasExternalIcons && slots.iconMode === 'tooltip'
-        ? relations.textWithTooltip.slots({
-            customInteractionCue: true,
-
-            text:
-              relations.artistLink.slots({
-                attributes: {class: 'text-with-tooltip-interaction-cue'},
-              }),
-
-            tooltip:
-              relations.tooltip.slots({
-                attributes:
-                  {class: ['icons', 'icons-tooltip']},
-
-                contentAttributes:
-                  {[html.joinChildren]: ''},
-
-                content:
-                  stitchArrays({
-                    icon: relations.artistIcons,
-                    url: data.urls,
-                  }).map(({icon, url}) => {
-                      icon.setSlots({
-                        context: 'artist',
-                        withText: true,
-                      });
-
-                      let platformText =
-                        language.formatExternalLink(url, {
-                          context: 'artist',
-                          style: 'platform',
-                        });
-
-                      // This is a pretty ridiculous hack, but we currently
-                      // don't have a way of telling formatExternalLink to *not*
-                      // use the fallback string, which just formats the URL as
-                      // its host/domain... so is technically detectable.
-                      if (platformText.toString() === (new URL(url)).host) {
-                        platformText =
-                          language.$('misc.artistLink.noExternalLinkPlatformName');
-                      }
-
-                      const platformSpan =
-                        html.tag('span', {class: 'icon-platform'},
-                          platformText);
-
-                      return [icon, platformSpan];
-                    }),
-              }),
-          })
-        : relations.artistLink);
-
-    if (hasContribution) {
-      parts.push('withContribution');
-      options.contrib = data.contribution;
-    }
-
-    if (hasExternalIcons && slots.iconMode === 'inline') {
-      parts.push('withExternalLinks');
-      options.links =
-        html.tag('span', {class: ['icons', 'icons-inline']},
-          {[html.noEdgeWhitespace]: true},
-          language.formatUnitList(
-            relations.artistIcons
-              .slice(0, 4)
-              .map(icon => icon.slot('context', 'artist'))));
-    }
-
-    const contributionPart =
-      language.formatString(...parts, options);
-
-    if (!hasContribution && !hasExternalIcons) {
-      return contributionPart;
-    }
-
-    return (
-      html.tag('span', {class: 'contribution'},
-        {[html.noEdgeWhitespace]: true},
-
-        parts.length > 1 &&
-        slots.preventWrapping &&
-          {class: 'nowrap'},
-
-        contributionPart));
-  },
+  generate: (data, relations, slots, {html, language}) =>
+    html.tag('span', {class: 'contribution'},
+      {[html.noEdgeWhitespace]: true},
+
+      slots.preventWrapping &&
+        {class: 'nowrap'},
+
+      language.encapsulate('misc.artistLink', workingCapsule => {
+        const workingOptions = {};
+
+        relations.tooltip.setSlots({
+          showExternalLinks: slots.showExternalLinks,
+          showChronology: slots.showChronology,
+          chronologyKind: slots.chronologyKind,
+        });
+
+        workingOptions.artist =
+          (html.isBlank(relations.tooltip)
+            ? relations.artistLink
+            : relations.textWithTooltip.slots({
+                customInteractionCue: true,
+
+                text:
+                  relations.artistLink.slots({
+                    attributes: {class: 'text-with-tooltip-interaction-cue'},
+                  }),
+
+                tooltip:
+                  relations.tooltip.slots({
+                    showExternalLinks: slots.showExternalLinks,
+                    showChronology: slots.showChronology,
+                    chronologyKind: slots.chronologyKind,
+                  }),
+              }));
+
+        if (slots.showContribution && data.contribution) {
+          workingCapsule += '.withContribution';
+          workingOptions.contrib =
+            data.contribution;
+        }
+
+        return language.formatString(workingCapsule, workingOptions);
+      })),
 };
diff --git a/src/content/dependencies/linkExternalAsIcon.js b/src/content/dependencies/linkExternalAsIcon.js
deleted file mode 100644
index 6f37529e..00000000
--- a/src/content/dependencies/linkExternalAsIcon.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import {isExternalLinkContext} from '#external-links';
-
-export default {
-  extraDependencies: ['html', 'language', 'to'],
-
-  data: (url) => ({url}),
-
-  slots: {
-    context: {
-      // This awkward syntax is because the slot descriptor validator can't
-      // differentiate between a function that returns a validator (the usual
-      // syntax) and a function that is itself a validator.
-      validate: () => isExternalLinkContext,
-      default: 'generic',
-    },
-
-    withText: {type: 'boolean'},
-  },
-
-  generate(data, slots, {html, language, to}) {
-    const format = style =>
-      language.formatExternalLink(data.url, {style, context: slots.context});
-
-    const platformText = format('platform');
-    const handleText = format('handle');
-    const iconId = format('icon-id');
-
-    return html.tag('a', {class: 'icon'},
-      {href: data.url},
-
-      slots.withText &&
-        {class: 'has-text'},
-
-      [
-        html.tag('svg', [
-          !slots.withText &&
-            html.tag('title', platformText),
-
-          html.tag('use', {
-            href: to('shared.staticIcon', iconId),
-          }),
-        ]),
-
-        slots.withText &&
-          html.tag('span', {class: 'icon-text'},
-            (html.isBlank(handleText)
-              ? platformText
-              : handleText)),
-      ]);
-  },
-};
diff --git a/src/content/dependencies/listArtistsByContributions.js b/src/content/dependencies/listArtistsByContributions.js
index 0af586cd..41944959 100644
--- a/src/content/dependencies/listArtistsByContributions.js
+++ b/src/content/dependencies/listArtistsByContributions.js
@@ -1,6 +1,13 @@
 import {sortAlphabetically, sortByCount} from '#sort';
-import {empty, filterByCount, filterMultipleArrays, stitchArrays, unique}
-  from '#sugar';
+
+import {
+  accumulateSum,
+  empty,
+  filterByCount,
+  filterMultipleArrays,
+  stitchArrays,
+  unique,
+} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkArtist'],
@@ -38,26 +45,33 @@ export default {
       'artistsByTrackContributions',
       'countsByTrackContributions',
       artist =>
-        unique([
-          ...artist.tracksAsContributor,
-          ...artist.tracksAsArtist,
-        ]).length);
+        (unique(
+          ([
+            artist.trackArtistContributions,
+            artist.trackContributorContributions,
+          ]).flat()
+            .map(({thing}) => thing)
+        )).length);
 
     queryContributionInfo(
       'artistsByArtworkContributions',
       'countsByArtworkContributions',
       artist =>
-        artist.tracksAsCoverArtist.length +
-        artist.albumsAsCoverArtist.length +
-        artist.albumsAsWallpaperArtist.length +
-        artist.albumsAsBannerArtist.length);
+        accumulateSum(
+          [
+            artist.albumCoverArtistContributions,
+            artist.albumWallpaperArtistContributions,
+            artist.albumBannerArtistContributions,
+            artist.trackCoverArtistContributions,
+          ],
+          contribs => contribs.length));
 
     if (sprawl.enableFlashesAndGames) {
       queryContributionInfo(
         'artistsByFlashContributions',
         'countsByFlashContributions',
         artist =>
-          artist.flashesAsContributor.length);
+          artist.flashContributorContributions.length);
     }
 
     return query;
diff --git a/src/content/dependencies/listArtistsByDuration.js b/src/content/dependencies/listArtistsByDuration.js
index f677d82c..6b2a18a0 100644
--- a/src/content/dependencies/listArtistsByDuration.js
+++ b/src/content/dependencies/listArtistsByDuration.js
@@ -1,6 +1,5 @@
 import {sortAlphabetically, sortByCount} from '#sort';
 import {filterByCount, stitchArrays} from '#sugar';
-import {getTotalDuration} from '#wiki-data';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkArtist'],
@@ -16,11 +15,7 @@ export default {
         artistData.filter(artist => !artist.isAlias));
 
     const durations =
-      artists.map(artist =>
-        getTotalDuration([
-          ...(artist.tracksAsArtist ?? []),
-          ...(artist.tracksAsContributor ?? []),
-        ], {originalReleasesOnly: true}));
+      artists.map(artist => artist.totalDuration);
 
     filterByCount(artists, durations);
     sortByCount(artists, durations, {greatestFirst: true});
diff --git a/src/content/dependencies/listArtistsByGroup.js b/src/content/dependencies/listArtistsByGroup.js
index 30884d24..0bf9dd2d 100644
--- a/src/content/dependencies/listArtistsByGroup.js
+++ b/src/content/dependencies/listArtistsByGroup.js
@@ -1,6 +1,13 @@
 import {sortAlphabetically} from '#sort';
-import {empty, filterMultipleArrays, stitchArrays, unique} from '#sugar';
-import {getArtistNumContributions} from '#wiki-data';
+
+import {
+  empty,
+  filterByCount,
+  filterMultipleArrays,
+  stitchArrays,
+  transposeArrays,
+  unique,
+} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'],
@@ -15,29 +22,69 @@ export default {
       sortAlphabetically(
         sprawl.artistData.filter(artist => !artist.isAlias));
 
-    const groups =
+    const interestingGroups =
       sprawl.wikiInfo.divideTrackListsByGroups;
 
-    if (empty(groups)) {
-      return {spec, artists};
+    if (empty(interestingGroups)) {
+      return {spec};
     }
 
-    const artistGroups =
+    // We don't actually care about *which* things belong to each group, only
+    // how many belong to each group. So we'll just compute a list of all the
+    // (interesting) groups that each of each artists' things belongs to.
+    const artistThingGroups =
       artists.map(artist =>
-        unique(
-          unique([
-            ...artist.albumsAsAny,
-            ...artist.tracksAsAny.map(track => track.album),
-          ]).flatMap(album => album.groups)))
-
-    const artistsByGroup =
-      groups.map(group =>
-        artists.filter((artist, index) => artistGroups[index].includes(group)));
-
-    filterMultipleArrays(groups, artistsByGroup,
-      (group, artists) => !empty(artists));
-
-    return {spec, groups, artistsByGroup};
+        ([
+          (unique(
+            ([
+              artist.albumArtistContributions,
+              artist.albumCoverArtistContributions,
+              artist.albumWallpaperArtistContributions,
+              artist.albumBannerArtistContributions,
+            ]).flat()
+              .map(({thing}) => thing)
+          )).map(album => album.groups),
+          (unique(
+            ([
+              artist.trackArtistContributions,
+              artist.trackContributorContributions,
+              artist.trackCoverArtistContributions,
+            ]).flat()
+              .map(({thing}) => thing)
+          )).map(track => track.album.groups),
+        ]).flat()
+          .map(groups => groups
+            .filter(group => interestingGroups.includes(group))));
+
+    const [artistsByGroup, countsByGroup] =
+      transposeArrays(interestingGroups.map(group => {
+        const counts =
+          artistThingGroups
+            .map(thingGroups => thingGroups
+              .filter(thingGroups => thingGroups.includes(group))
+              .length);
+
+        const filteredArtists = artists.slice();
+
+        filterByCount(filteredArtists, counts);
+
+        return [filteredArtists, counts];
+      }));
+
+    const groups = interestingGroups;
+
+    filterMultipleArrays(
+      groups,
+      artistsByGroup,
+      countsByGroup,
+      (_group, artists, _counts) => !empty(artists));
+
+    return {
+      spec,
+      groups,
+      artistsByGroup,
+      countsByGroup,
+    };
   },
 
   relations(relation, query) {
@@ -46,12 +93,6 @@ export default {
     relations.page =
       relation('generateListingPage', query.spec);
 
-    if (query.artists) {
-      relations.artistLinks =
-        query.artists
-          .map(artist => relation('linkArtist', artist));
-    }
-
     if (query.artistsByGroup) {
       relations.groupLinks =
         query.groups
@@ -69,65 +110,43 @@ export default {
   data(query) {
     const data = {};
 
-    if (query.artists) {
-      data.counts =
-        query.artists
-          .map(artist => getArtistNumContributions(artist));
-    }
-
     if (query.artistsByGroup) {
       data.groupDirectories =
         query.groups
           .map(group => group.directory);
 
       data.countsByGroup =
-        query.artistsByGroup
-          .map(artists => artists
-            .map(artist => getArtistNumContributions(artist)));
+        query.countsByGroup;
     }
 
     return data;
   },
 
-  generate(data, relations, {language}) {
-    return (
-      (relations.artistLinksByGroup
-        ? relations.page.slots({
-            type: 'chunks',
-
-            showSkipToSection: true,
-            chunkIDs:
-              data.groupDirectories
-                .map(directory => `contributed-to-${directory}`),
-
-            chunkTitles:
-              relations.groupLinks.map(groupLink => ({
-                group: groupLink,
-              })),
-
-            chunkRows:
-              stitchArrays({
-                artistLinks: relations.artistLinksByGroup,
-                counts: data.countsByGroup,
-              }).map(({artistLinks, counts}) =>
-                  stitchArrays({
-                    link: artistLinks,
-                    count: counts,
-                  }).map(({link, count}) => ({
-                      artist: link,
-                      contributions: language.countContributions(count, {unit: true}),
-                    }))),
-          })
-        : relations.page.slots({
-            type: 'rows',
-            rows:
-              stitchArrays({
-                link: relations.artistLinks,
-                count: data.counts,
-              }).map(({link, count}) => ({
-                  artist: link,
-                  contributions: language.countContributions(count, {unit: true}),
-                })),
-          })));
-  },
+  generate: (data, relations, {language}) =>
+    relations.page.slots({
+      type: 'chunks',
+
+      showSkipToSection: true,
+      chunkIDs:
+        data.groupDirectories
+          .map(directory => `contributed-to-${directory}`),
+
+      chunkTitles:
+        relations.groupLinks.map(groupLink => ({
+          group: groupLink,
+        })),
+
+      chunkRows:
+        stitchArrays({
+          artistLinks: relations.artistLinksByGroup,
+          counts: data.countsByGroup,
+        }).map(({artistLinks, counts}) =>
+            stitchArrays({
+              link: artistLinks,
+              count: counts,
+            }).map(({link, count}) => ({
+                artist: link,
+                contributions: language.countContributions(count, {unit: true}),
+              }))),
+    }),
 };
diff --git a/src/content/dependencies/listRandomPageLinks.js b/src/content/dependencies/listRandomPageLinks.js
index ab2eca93..79bba441 100644
--- a/src/content/dependencies/listRandomPageLinks.js
+++ b/src/content/dependencies/listRandomPageLinks.js
@@ -74,20 +74,22 @@ export default {
   },
 
   generate(data, relations, {html, language}) {
+    const capsule = language.encapsulate('listingPage.other.randomPages');
+
     const miscellaneousChunkRows = [
-      {
+      language.encapsulate(capsule, 'chunk.item.randomArtist', capsule => ({
         stringsKey: 'randomArtist',
 
         mainLink:
           html.tag('a',
             {href: '#', 'data-random': 'artist'},
-            language.$('listingPage.other.randomPages.chunk.item.randomArtist.mainLink')),
+            language.$(capsule, 'mainLink')),
 
         atLeastTwoContributions:
           html.tag('a',
             {href: '#', 'data-random': 'artist-more-than-one-contrib'},
-            language.$('listingPage.other.randomPages.chunk.item.randomArtist.atLeastTwoContributions')),
-      },
+            language.$(capsule, 'atLeastTwoContributions')),
+      })),
 
       {stringsKey: 'randomAlbumWholeSite'},
       {stringsKey: 'randomTrackWholeSite'},
@@ -104,24 +106,25 @@ export default {
 
       content: [
         html.tag('p',
-          language.$('listingPage.other.randomPages.chooseLinkLine', {
-            fromPart:
-              (relations.groupLinks
-                ? language.$('listingPage.other.randomPages.chooseLinkLine.fromPart.dividedByGroups')
-                : language.$('listingPage.other.randomPages.chooseLinkLine.fromPart.notDividedByGroups')),
+          language.encapsulate(capsule, 'chooseLinkLine', capsule =>
+            language.$(capsule, {
+              fromPart:
+                (relations.groupLinks
+                  ? language.$(capsule, 'fromPart.dividedByGroups')
+                  : language.$(capsule, 'fromPart.notDividedByGroups')),
 
-            browserSupportPart:
-              language.$('listingPage.other.randomPages.chooseLinkLine.browserSupportPart'),
-          })),
+              browserSupportPart:
+                language.$(capsule, 'browserSupportPart'),
+            }))),
 
         html.tag('p', {id: 'data-loading-line'},
-          language.$('listingPage.other.randomPages.dataLoadingLine')),
+          language.$(capsule, 'dataLoadingLine')),
 
         html.tag('p', {id: 'data-loaded-line'},
-          language.$('listingPage.other.randomPages.dataLoadedLine')),
+          language.$(capsule, 'dataLoadedLine')),
 
         html.tag('p', {id: 'data-error-line'},
-          language.$('listingPage.other.randomPages.dataErrorLine')),
+          language.$(capsule, 'dataErrorLine')),
       ],
 
       showSkipToSection: true,
@@ -148,17 +151,18 @@ export default {
 
         ...
           (relations.groupLinks
-            ? relations.groupLinks.map(() => ({
-                randomAlbum:
-                  html.tag('a',
-                    {href: '#', 'data-random': 'album-in-group-dl'},
-                    language.$('listingPage.other.randomPages.chunk.title.fromGroup.accent.randomAlbum')),
-
-                randomTrack:
-                  html.tag('a',
-                    {href: '#', 'data-random': 'track-in-group-dl'},
-                    language.$('listingPage.other.randomPages.chunk.title.fromGroup.accent.randomTrack')),
-              }))
+            ? relations.groupLinks.map(() =>
+                language.encapsulate(capsule, 'chunk.title.fromGroup.accent', capsule => ({
+                  randomAlbum:
+                    html.tag('a',
+                      {href: '#', 'data-random': 'album-in-group-dl'},
+                      language.$(capsule, 'randomAlbum')),
+
+                  randomTrack:
+                    html.tag('a',
+                      {href: '#', 'data-random': 'track-in-group-dl'},
+                      language.$(capsule, 'randomTrack')),
+                })))
             : [null]),
       ],
 
diff --git a/src/content/dependencies/listTracksByDate.js b/src/content/dependencies/listTracksByDate.js
index 01ce4e2d..0a2bfd6c 100644
--- a/src/content/dependencies/listTracksByDate.js
+++ b/src/content/dependencies/listTracksByDate.js
@@ -15,7 +15,8 @@ export default {
 
       chunks:
         chunkByProperties(
-          sortAlbumsTracksChronologically(trackData.slice()),
+          sortAlbumsTracksChronologically(
+            trackData.filter(track => track.date)),
           ['album', 'date']),
     };
   },
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js
index 0904cde6..5f803a3b 100644
--- a/src/content/dependencies/transformContent.js
+++ b/src/content/dependencies/transformContent.js
@@ -45,7 +45,7 @@ export default {
   sprawl(wikiData, content) {
     const find = bindFind(wikiData);
 
-    const parsedNodes = parseInput(content);
+    const parsedNodes = parseInput(content ?? '');
 
     return {
       nodes: parsedNodes
@@ -262,6 +262,10 @@ export default {
                   height && {height},
                   style && {style},
 
+                  align === 'center' &&
+                  !link &&
+                    {class: 'align-center'},
+
                   pixelate &&
                     {class: 'pixelate'});
 
@@ -271,16 +275,20 @@ export default {
                     {href: link},
                     {target: '_blank'},
 
+                    align === 'center' &&
+                      {class: 'align-center'},
+
                     {title:
-                      language.$('misc.external.opensInNewTab', {
-                        link:
-                          language.formatExternalLink(link, {
-                            style: 'platform',
-                          }),
+                      language.encapsulate('misc.external.opensInNewTab', capsule =>
+                        language.$(capsule, {
+                          link:
+                            language.formatExternalLink(link, {
+                              style: 'platform',
+                            }),
 
-                        annotation:
-                          language.$('misc.external.opensInNewTab.annotation'),
-                      }).toString()},
+                          annotation:
+                            language.$(capsule, 'annotation'),
+                        }).toString())},
 
                     content);
               }
@@ -505,7 +513,11 @@ export default {
         addText(markedOutput.slice(parseFrom));
       }
 
-      return html.tags(tags, {[html.joinChildren]: ''});
+      return (
+        html.tags(tags, {
+          [html.joinChildren]: '',
+          [html.onlyIfContent]: true,
+        }));
     };
 
     if (slots.mode === 'inline') {
@@ -530,9 +542,9 @@ export default {
           // Expand line breaks which don't follow a list, quote,
           // or <br> / "  ", and which don't precede or follow
           // indented text (by at least two spaces).
-          .replace(/(?<!^ *-.*|^>.*|^  .*\n*|  $|<br>$)\n+(?!  |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */
+          .replace(/(?<!^ *(?:-|\d\.).*|^>.*|^  .*\n*|  $|<br>$)\n+(?!  |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */
           // Expand line breaks which are at the end of a list.
-          .replace(/(?<=^ *-.*)\n+(?!^ *-)/gm, '\n\n')
+          .replace(/(?<=^ *(?:-|\d\.).*)\n+(?!^ *(?:-|\d\.))/gm, '\n\n')
           // Expand line breaks which are at the end of a quote.
           .replace(/(?<=^>.*)\n+(?!^>)/gm, '\n\n');
 
diff --git a/src/content/util/getChronologyRelations.js b/src/content/util/getChronologyRelations.js
deleted file mode 100644
index c4a62dad..00000000
--- a/src/content/util/getChronologyRelations.js
+++ /dev/null
@@ -1,55 +0,0 @@
-export default function getChronologyRelations(thing, {
-  contributions,
-  linkArtist,
-  linkThing,
-  getThings,
-}) {
-  // One call to getChronologyRelations is considered "lumping" together all
-  // contributions as carrying equivalent meaning (for example, "artist"
-  // contributions and "contributor" contributions are bunched together in
-  // one call to getChronologyRelations, while "cover artist" contributions
-  // are a separate call). getChronologyRelations prevents duplicates that
-  // carry the same meaning by only using the first instance of each artist
-  // in the contributions array passed to it. It's expected that the string
-  // identifying which kind of contribution ("track" or "cover art") is
-  // shared and applied to all contributions, as providing them together
-  // in one call to getChronologyRelations implies they carry the same
-  // meaning.
-
-  const artistsSoFar = new Set();
-
-  contributions = contributions.filter(({artist}) => {
-    if (artistsSoFar.has(artist)) {
-      return false;
-    } else {
-      artistsSoFar.add(artist);
-      return true;
-    }
-  });
-
-  return contributions.map(({artist}) => {
-    const things = Array.from(new Set(getThings(artist)));
-
-    // Don't show a line if this contribution isn't part of the artist's
-    // chronology at all (usually because this thing isn't dated).
-    const index = things.indexOf(thing);
-    if (index === -1) {
-      return;
-    }
-
-    // Don't show a line if this contribution is the *only* item in the
-    // artist's chronology (since there's nothing to navigate there).
-    const previous = things[index - 1];
-    const next = things[index + 1];
-    if (!previous && !next) {
-      return;
-    }
-
-    return {
-      index: index + 1,
-      artistLink: linkArtist(artist),
-      previousLink: previous ? linkThing(previous) : null,
-      nextLink: next ? linkThing(next) : null,
-    };
-  }).filter(Boolean);
-}
diff --git a/src/content/util/groupTracksByGroup.js b/src/content/util/groupTracksByGroup.js
deleted file mode 100644
index 4e189007..00000000
--- a/src/content/util/groupTracksByGroup.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import {empty} from '#sugar';
-
-export default function groupTracksByGroup(tracks, groups) {
-  const lists = new Map(groups.map(group => [group, []]));
-  lists.set('other', []);
-
-  for (const track of tracks) {
-    const group = groups.find(group => group.albums.includes(track.album));
-    if (group) {
-      lists.get(group).push(track);
-    } else {
-      lists.get('other').push(track);
-    }
-  }
-
-  for (const [key, tracks] of lists.entries()) {
-    if (empty(tracks)) {
-      lists.delete(key);
-    }
-  }
-
-  return lists;
-}