« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/page/album.js20
-rw-r--r--src/page/index.js24
-rw-r--r--src/page/track.js331
-rwxr-xr-xsrc/upd8.js308
-rw-r--r--src/util/magic-constants.js11
-rw-r--r--src/util/wiki-data.js6
6 files changed, 400 insertions, 300 deletions
diff --git a/src/page/album.js b/src/page/album.js
index f8597790..bc1668a0 100644
--- a/src/page/album.js
+++ b/src/page/album.js
@@ -5,6 +5,8 @@
 //   - generateAlbumNavLinks
 //   - generateAlbumChronologyLinks
 
+// Imports
+
 import fixWS from 'fix-whitespace';
 
 import {
@@ -20,6 +22,8 @@ import {
 
 import * as html from '../util/html.js';
 
+// Page exports
+
 export function targets({wikiData}) {
     return wikiData.albumData;
 }
@@ -225,7 +229,7 @@ export function write(album, {wikiData}) {
                     album.tracks.length > 1 &&
                     {
                         divider: false,
-                        html: generateAlbumNavLinks(album, null, {link, strings})
+                        html: generateAlbumNavLinks(album, null, {strings})
                     }
                 ],
                 content: html.tag('div', generateAlbumChronologyLinks(album, null, {chronologyLinks}))
@@ -236,6 +240,8 @@ export function write(album, {wikiData}) {
     return [page, data];
 }
 
+// Utility exports
+
 export function generateAlbumSidebar(album, currentTrack, {
     fancifyURL,
     link,
@@ -344,13 +350,15 @@ export function generateAlbumSidebar(album, currentTrack, {
     }
 }
 
-export function generateAlbumNavLinks(album, currentTrack, {link, strings}) {
+export function generateAlbumNavLinks(album, currentTrack, {
+    generatePreviousNextLinks,
+    strings
+}) {
     if (album.tracks.length <= 1) {
         return '';
     }
 
     const previousNextLinks = currentTrack && generatePreviousNextLinks(currentTrack, {
-        link, strings,
         data: album.tracks,
         linkKey: 'track'
     });
@@ -365,14 +373,14 @@ export function generateAlbumNavLinks(album, currentTrack, {link, strings}) {
         : `<span class="js-hide-until-data">(${randomLink})</span>`);
 }
 
-export function generateAlbumChronologyLinks(album, currentTrack, {chronologyLinks}) {
+export function generateAlbumChronologyLinks(album, currentTrack, {generateChronologyLinks}) {
     return [
-        currentTrack && chronologyLinks(currentTrack, {
+        currentTrack && generateChronologyLinks(currentTrack, {
             contribKey: 'artists',
             getThings: artist => [...artist.tracks.asArtist, ...artist.tracks.asContributor],
             headingString: 'misc.chronology.heading.track'
         }),
-        chronologyLinks(currentTrack || album, {
+        generateChronologyLinks(currentTrack || album, {
             contribKey: 'coverArtists',
             getThings: artist => [...artist.albums.asCoverArtist, ...artist.tracks.asCoverArtist],
             headingString: 'misc.chronology.heading.coverArt'
diff --git a/src/page/index.js b/src/page/index.js
index ca782389..a37f8a90 100644
--- a/src/page/index.js
+++ b/src/page/index.js
@@ -1,5 +1,29 @@
 // NB: This is the index for the page/ directory and contains exports for all
 // other modules here! It's not the page spec for the homepage - see
 // homepage.js for that.
+//
+// Each module published in this list should follow a particular format,
+// including the following exports:
+//
+// targets({wikiData})
+//     Gets the objects which this page's write() function should be called on.
+//     Usually this will simply mean returning the appropriate thingData array,
+//     but it may also apply filter/map/etc if useful.
+//
+// write(thing, {wikiData})
+//     Gets descriptors for any page and data writes associated with the given
+//     thing (which will be a value from the targets() array). This includes
+//     page (HTML) writes, data (JSON) writes, etc. Notably, this function does
+//     not perform any file operations itself; it only describes the operations
+//     which will be processed elsewhere, once for each translation language.
+//     The write function also immediately transforms any data which will be
+//     reused across writes of the same page, so that this data is effectively
+//     cached (rather than recalculated for each language/write).
+//
+// As these modules are effectively the HTML templates for all site layout,
+// common patterns may also be exported alongside the special exports above.
+// These functions should be referenced only from adjacent modules, as they
+// pertain only to site page generation.
 
 export * as album from './album.js';
+export * as track from './track.js';
diff --git a/src/page/track.js b/src/page/track.js
new file mode 100644
index 00000000..2dec9bd3
--- /dev/null
+++ b/src/page/track.js
@@ -0,0 +1,331 @@
+// Track page specification.
+
+// Imports
+
+import fixWS from 'fix-whitespace';
+
+import {
+    generateAlbumChronologyLinks,
+    generateAlbumNavLinks,
+    generateAlbumSidebar
+} from './album.js';
+
+import {
+    getThemeString
+} from '../util/colors.js';
+
+import * as html from '../util/html.js';
+
+import {
+    OFFICIAL_GROUP_DIRECTORY,
+    UNRELEASED_TRACKS_DIRECTORY
+} from '../util/magic-constants.js';
+
+import {
+    bindOpts
+} from '../util/sugar.js';
+
+import {
+    getTrackCover,
+    getAlbumListTag,
+    sortByDate
+} from '../util/wiki-data.js';
+
+// Page exports
+
+export function targets({wikiData}) {
+    return wikiData.trackData;
+}
+
+export function write(track, {wikiData}) {
+    const { groupData, wikiInfo } = wikiData;
+    const { album } = track;
+
+    const tracksThatReference = track.referencedBy;
+    const useDividedReferences = groupData.some(group => group.directory === OFFICIAL_GROUP_DIRECTORY);
+    const ttrFanon = (useDividedReferences &&
+        tracksThatReference.filter(t => t.album.groups.every(group => group.directory !== OFFICIAL_GROUP_DIRECTORY)));
+    const ttrOfficial = (useDividedReferences &&
+        tracksThatReference.filter(t => t.album.groups.some(group => group.directory === OFFICIAL_GROUP_DIRECTORY)));
+
+    const tracksReferenced = track.references;
+    const otherReleases = track.otherReleases;
+    const listTag = getAlbumListTag(album);
+
+    let flashesThatFeature;
+    if (wikiInfo.features.flashesAndGames) {
+        flashesThatFeature = sortByDate([track, ...otherReleases]
+            .flatMap(track => track.flashes.map(flash => ({flash, as: track}))));
+    }
+
+    const unbound_generateTrackList = (tracks, {getArtistString, link, strings}) => html.tag('ul',
+        tracks.map(track => {
+            const line = strings('trackList.item.withArtists', {
+                track: link.track(track),
+                by: `<span class="by">${strings('trackList.item.withArtists.by', {
+                    artists: getArtistString(track.artists)
+                })}</span>`
+            });
+            return (track.aka
+                ? `<li class="rerelease">${strings('trackList.item.rerelease', {track: line})}</li>`
+                : `<li>${line}</li>`);
+        })
+    );
+
+    const hasCommentary = track.commentary || otherReleases.some(t => t.commentary);
+    const generateCommentary = ({
+        link,
+        strings,
+        transformMultiline
+    }) => transformMultiline([
+        track.commentary,
+        ...otherReleases.map(track =>
+            (track.commentary?.split('\n')
+                .filter(line => line.replace(/<\/b>/g, '').includes(':</i>'))
+                .map(line => fixWS`
+                    ${line}
+                    ${strings('releaseInfo.artistCommentary.seeOriginalRelease', {
+                        original: link.track(track)
+                    })}
+                `)
+                .join('\n')))
+    ].filter(Boolean).join('\n'));
+
+    const data = {
+        type: 'data',
+        path: ['track', track.directory],
+        data: ({
+            serializeContribs,
+            serializeCover,
+            serializeGroupsForTrack,
+            serializeLink
+        }) => ({
+            name: track.name,
+            directory: track.directory,
+            dates: {
+                released: track.date,
+                originallyReleased: track.originalDate,
+                coverArtAdded: track.coverArtDate
+            },
+            duration: track.duration,
+            color: track.color,
+            cover: serializeCover(track, getTrackCover),
+            artists: serializeContribs(track.artists),
+            contributors: serializeContribs(track.contributors),
+            coverArtists: serializeContribs(track.coverArtists || []),
+            album: serializeLink(track.album),
+            groups: serializeGroupsForTrack(track),
+            references: track.references.map(serializeLink),
+            referencedBy: track.referencedBy.map(serializeLink),
+            alsoReleasedAs: otherReleases.map(track => ({
+                track: serializeLink(track),
+                album: serializeLink(track.album)
+            }))
+        })
+    };
+
+    const page = {
+        type: 'page',
+        path: ['track', track.directory],
+        page: ({
+            fancifyURL,
+            generateChronologyLinks,
+            generateCoverLink,
+            generatePreviousNextLinks,
+            getAlbumStylesheet,
+            getArtistString,
+            getTrackCover,
+            link,
+            strings,
+            transformInline,
+            transformLyrics,
+            transformMultiline,
+            to
+        }) => {
+            const generateTrackList = bindOpts(unbound_generateTrackList, {getArtistString, link, strings});
+
+            return {
+                title: strings('trackPage.title', {track: track.name}),
+                stylesheet: getAlbumStylesheet(album, {to}),
+                theme: getThemeString(track.color, [
+                    `--album-directory: ${album.directory}`,
+                    `--track-directory: ${track.directory}`
+                ]),
+
+                // disabled for now! shifting banner position per height of page is disorienting
+                /*
+                banner: album.bannerArtists && {
+                    classes: ['dim'],
+                    dimensions: album.bannerDimensions,
+                    path: ['media.albumBanner', album.directory],
+                    alt: strings('misc.alt.albumBanner'),
+                    position: 'bottom'
+                },
+                */
+
+                main: {
+                    content: fixWS`
+                        ${generateCoverLink({
+                            src: getTrackCover(track),
+                            alt: strings('misc.alt.trackCover'),
+                            tags: track.artTags
+                        })}
+                        <h1>${strings('trackPage.title', {track: track.name})}</h1>
+                        <p>
+                            ${[
+                                strings('releaseInfo.by', {
+                                    artists: getArtistString(track.artists, {
+                                        showContrib: true,
+                                        showIcons: true
+                                    })
+                                }),
+                                track.coverArtists && strings('releaseInfo.coverArtBy', {
+                                    artists: getArtistString(track.coverArtists, {
+                                        showContrib: true,
+                                        showIcons: true
+                                    })
+                                }),
+                                album.directory !== UNRELEASED_TRACKS_DIRECTORY && strings('releaseInfo.released', {
+                                    date: strings.count.date(track.date)
+                                }),
+                                +track.coverArtDate !== +track.date && strings('releaseInfo.artReleased', {
+                                    date: strings.count.date(track.coverArtDate)
+                                }),
+                                track.duration && strings('releaseInfo.duration', {
+                                    duration: strings.count.duration(track.duration)
+                                })
+                            ].filter(Boolean).join('<br>\n')}
+                        </p>
+                        <p>${
+                            (track.urls.length
+                                ? strings('releaseInfo.listenOn', {
+                                    links: strings.list.or(track.urls.map(url => fancifyURL(url, {strings})))
+                                })
+                                : strings('releaseInfo.listenOn.noLinks'))
+                        }</p>
+                        ${otherReleases.length && fixWS`
+                            <p>${strings('releaseInfo.alsoReleasedAs')}</p>
+                            <ul>
+                                ${otherReleases.map(track => fixWS`
+                                    <li>${strings('releaseInfo.alsoReleasedAs.item', {
+                                        track: link.track(track),
+                                        album: link.album(track.album)
+                                    })}</li>
+                                `).join('\n')}
+                            </ul>
+                        `}
+                        ${track.contributors.textContent && fixWS`
+                            <p>
+                                ${strings('releaseInfo.contributors')}
+                                <br>
+                                ${transformInline(track.contributors.textContent)}
+                            </p>
+                        `}
+                        ${track.contributors.length && fixWS`
+                            <p>${strings('releaseInfo.contributors')}</p>
+                            <ul>
+                                ${(track.contributors
+                                    .map(contrib => `<li>${getArtistString([contrib], {
+                                        showContrib: true,
+                                        showIcons: true
+                                    })}</li>`)
+                                    .join('\n'))}
+                            </ul>
+                        `}
+                        ${tracksReferenced.length && fixWS`
+                            <p>${strings('releaseInfo.tracksReferenced', {track: `<i>${track.name}</i>`})}</p>
+                            ${generateTrackList(tracksReferenced)}
+                        `}
+                        ${tracksThatReference.length && fixWS`
+                            <p>${strings('releaseInfo.tracksThatReference', {track: `<i>${track.name}</i>`})}</p>
+                            ${useDividedReferences && fixWS`
+                                <dl>
+                                    ${ttrOfficial.length && fixWS`
+                                        <dt>${strings('trackPage.referenceList.official')}</dt>
+                                        <dd>${generateTrackList(ttrOfficial)}</dd>
+                                    `}
+                                    ${ttrFanon.length && fixWS`
+                                        <dt>${strings('trackPage.referenceList.fandom')}</dt>
+                                        <dd>${generateTrackList(ttrFanon)}</dd>
+                                    `}
+                                </dl>
+                            `}
+                            ${!useDividedReferences && generateTrackList(tracksThatReference)}
+                        `}
+                        ${wikiInfo.features.flashesAndGames && flashesThatFeature.length && fixWS`
+                            <p>${strings('releaseInfo.flashesThatFeature', {track: `<i>${track.name}</i>`})}</p>
+                            <ul>
+                                ${flashesThatFeature.map(({ flash, as }) => html.tag('li',
+                                    {class: as !== track && 'rerelease'},
+                                    (as === track
+                                        ? strings('releaseInfo.flashesThatFeature.item', {
+                                            flash: link.flash(flash)
+                                        })
+                                        : strings('releaseInfo.flashesThatFeature.item.asDifferentRelease', {
+                                            flash: link.flash(flash),
+                                            track: link.track(as)
+                                        })))).join('\n')}
+                            </ul>
+                        `}
+                        ${track.lyrics && fixWS`
+                            <p>${strings('releaseInfo.lyrics')}</p>
+                            <blockquote>
+                                ${transformLyrics(track.lyrics)}
+                            </blockquote>
+                        `}
+                        ${hasCommentary && fixWS`
+                            <p>${strings('releaseInfo.artistCommentary')}</p>
+                            <blockquote>
+                                ${generateCommentary({link, strings, transformMultiline})}
+                            </blockquote>
+                        `}
+                    `
+                },
+
+                sidebarLeft: generateAlbumSidebar(album, track, {
+                    fancifyURL,
+                    link,
+                    strings,
+                    transformMultiline,
+                    wikiData
+                }),
+
+                nav: {
+                    links: [
+                        {toHome: true},
+                        {
+                            path: ['localized.album', album.directory],
+                            title: album.name
+                        },
+                        listTag === 'ol' ? {
+                            html: strings('trackPage.nav.track.withNumber', {
+                                number: album.tracks.indexOf(track) + 1,
+                                track: link.track(track, {class: 'current', to})
+                            })
+                        } : {
+                            html: strings('trackPage.nav.track', {
+                                track: link.track(track, {class: 'current', to})
+                            })
+                        },
+                        album.tracks.length > 1 &&
+                        {
+                            divider: false,
+                            html: generateAlbumNavLinks(album, track, {
+                                generatePreviousNextLinks,
+                                strings
+                            })
+                        }
+                    ].filter(Boolean),
+                    content: fixWS`
+                        <div>
+                            ${generateAlbumChronologyLinks(album, track, {generateChronologyLinks})}
+                        </div>
+                    `
+                }
+            };
+        }
+    };
+
+    return [data, page];
+}
+
diff --git a/src/upd8.js b/src/upd8.js
index 621cae57..e199b1f7 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -183,6 +183,13 @@ import {
     thumb
 } from './util/urls.js';
 
+// Pensive emoji!
+import {
+    FANDOM_GROUP_DIRECTORY,
+    OFFICIAL_GROUP_DIRECTORY,
+    UNRELEASED_TRACKS_DIRECTORY
+} from './util/magic-constants.js';
+
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 const CACHEBUST = 7;
@@ -197,10 +204,6 @@ const GROUP_DATA_FILE = 'groups.txt';
 const STATIC_PAGE_DATA_FILE = 'static-pages.txt';
 const DEFAULT_STRINGS_FILE = 'strings-default.json';
 
-const UNRELEASED_TRACKS_DIRECTORY = 'unreleased-tracks';
-const OFFICIAL_GROUP_DIRECTORY = 'official';
-const FANDOM_GROUP_DIRECTORY = 'fandom';
-
 // Code that's common 8etween the 8uild code (i.e. upd8.js) and gener8ted
 // site code should 8e put here. Which, uh, ~~only really means this one
 // file~~ is now a variety of useful utilities!
@@ -2649,292 +2652,6 @@ function getAlbumStylesheet(album, {to}) {
     ].filter(Boolean).join('\n');
 }
 
-function writeTrackPages({wikiData}) {
-    return wikiData.trackData.map(track => writeTrackPage(track, {wikiData}));
-}
-
-function writeTrackPage(track, {wikiData}) {
-    const { groupData, wikiInfo } = wikiData;
-    const { album } = track;
-
-    const tracksThatReference = track.referencedBy;
-    const useDividedReferences = groupData.some(group => group.directory === OFFICIAL_GROUP_DIRECTORY);
-    const ttrFanon = (useDividedReferences &&
-        tracksThatReference.filter(t => t.album.groups.every(group => group.directory !== OFFICIAL_GROUP_DIRECTORY)));
-    const ttrOfficial = (useDividedReferences &&
-        tracksThatReference.filter(t => t.album.groups.some(group => group.directory === OFFICIAL_GROUP_DIRECTORY)));
-
-    const tracksReferenced = track.references;
-    const otherReleases = track.otherReleases;
-    const listTag = getAlbumListTag(album);
-
-    let flashesThatFeature;
-    if (wikiInfo.features.flashesAndGames) {
-        flashesThatFeature = sortByDate([track, ...otherReleases]
-            .flatMap(track => track.flashes.map(flash => ({flash, as: track}))));
-    }
-
-    const unbound_generateTrackList = (tracks, {getArtistString, link, strings}) => html.tag('ul',
-        tracks.map(track => {
-            const line = strings('trackList.item.withArtists', {
-                track: link.track(track),
-                by: `<span class="by">${strings('trackList.item.withArtists.by', {
-                    artists: getArtistString(track.artists)
-                })}</span>`
-            });
-            return (track.aka
-                ? `<li class="rerelease">${strings('trackList.item.rerelease', {track: line})}</li>`
-                : `<li>${line}</li>`);
-        })
-    );
-
-    const hasCommentary = track.commentary || otherReleases.some(t => t.commentary);
-    const generateCommentary = ({
-        link,
-        strings,
-        transformMultiline
-    }) => transformMultiline([
-        track.commentary,
-        ...otherReleases.map(track =>
-            (track.commentary?.split('\n')
-                .filter(line => line.replace(/<\/b>/g, '').includes(':</i>'))
-                .map(line => fixWS`
-                    ${line}
-                    ${strings('releaseInfo.artistCommentary.seeOriginalRelease', {
-                        original: link.track(track)
-                    })}
-                `)
-                .join('\n')))
-    ].filter(Boolean).join('\n'));
-
-    const data = {
-        type: 'data',
-        path: ['track', track.directory],
-        data: () => ({
-            name: track.name,
-            directory: track.directory,
-            dates: {
-                released: track.date,
-                originallyReleased: track.originalDate,
-                coverArtAdded: track.coverArtDate
-            },
-            duration: track.duration,
-            color: track.color,
-            cover: serializeCover(track, getTrackCover),
-            artists: serializeContribs(track.artists),
-            contributors: serializeContribs(track.contributors),
-            coverArtists: serializeContribs(track.coverArtists || []),
-            album: serializeLink(track.album),
-            groups: serializeGroupsForTrack(track),
-            references: track.references.map(serializeLink),
-            referencedBy: track.referencedBy.map(serializeLink),
-            alsoReleasedAs: otherReleases.map(track => ({
-                track: serializeLink(track),
-                album: serializeLink(track.album)
-            }))
-        })
-    };
-
-    const page = {
-        type: 'page',
-        path: ['track', track.directory],
-        page: ({
-            generateCoverLink,
-            getArtistString,
-            getTrackCover,
-            link,
-            strings,
-            transformInline,
-            transformLyrics,
-            transformMultiline,
-            to
-        }) => {
-            const generateTrackList = bindOpts(unbound_generateTrackList, {getArtistString, link, strings});
-
-            return {
-                title: strings('trackPage.title', {track: track.name}),
-                stylesheet: getAlbumStylesheet(album, {to}),
-                theme: getThemeString(track.color, [
-                    `--album-directory: ${album.directory}`,
-                    `--track-directory: ${track.directory}`
-                ]),
-
-                // disabled for now! shifting banner position per height of page is disorienting
-                /*
-                banner: album.bannerArtists && {
-                    classes: ['dim'],
-                    dimensions: album.bannerDimensions,
-                    path: ['media.albumBanner', album.directory],
-                    alt: strings('misc.alt.albumBanner'),
-                    position: 'bottom'
-                },
-                */
-
-                main: {
-                    content: fixWS`
-                        ${generateCoverLink({
-                            src: getTrackCover(track),
-                            alt: strings('misc.alt.trackCover'),
-                            tags: track.artTags
-                        })}
-                        <h1>${strings('trackPage.title', {track: track.name})}</h1>
-                        <p>
-                            ${[
-                                strings('releaseInfo.by', {
-                                    artists: getArtistString(track.artists, {
-                                        showContrib: true,
-                                        showIcons: true
-                                    })
-                                }),
-                                track.coverArtists && strings('releaseInfo.coverArtBy', {
-                                    artists: getArtistString(track.coverArtists, {
-                                        showContrib: true,
-                                        showIcons: true
-                                    })
-                                }),
-                                album.directory !== UNRELEASED_TRACKS_DIRECTORY && strings('releaseInfo.released', {
-                                    date: strings.count.date(track.date)
-                                }),
-                                +track.coverArtDate !== +track.date && strings('releaseInfo.artReleased', {
-                                    date: strings.count.date(track.coverArtDate)
-                                }),
-                                track.duration && strings('releaseInfo.duration', {
-                                    duration: strings.count.duration(track.duration)
-                                })
-                            ].filter(Boolean).join('<br>\n')}
-                        </p>
-                        <p>${
-                            (track.urls.length
-                                ? strings('releaseInfo.listenOn', {
-                                    links: strings.list.or(track.urls.map(url => fancifyURL(url, {strings})))
-                                })
-                                : strings('releaseInfo.listenOn.noLinks'))
-                        }</p>
-                        ${otherReleases.length && fixWS`
-                            <p>${strings('releaseInfo.alsoReleasedAs')}</p>
-                            <ul>
-                                ${otherReleases.map(track => fixWS`
-                                    <li>${strings('releaseInfo.alsoReleasedAs.item', {
-                                        track: link.track(track),
-                                        album: link.album(track.album)
-                                    })}</li>
-                                `).join('\n')}
-                            </ul>
-                        `}
-                        ${track.contributors.textContent && fixWS`
-                            <p>
-                                ${strings('releaseInfo.contributors')}
-                                <br>
-                                ${transformInline(track.contributors.textContent)}
-                            </p>
-                        `}
-                        ${track.contributors.length && fixWS`
-                            <p>${strings('releaseInfo.contributors')}</p>
-                            <ul>
-                                ${(track.contributors
-                                    .map(contrib => `<li>${getArtistString([contrib], {
-                                        showContrib: true,
-                                        showIcons: true
-                                    })}</li>`)
-                                    .join('\n'))}
-                            </ul>
-                        `}
-                        ${tracksReferenced.length && fixWS`
-                            <p>${strings('releaseInfo.tracksReferenced', {track: `<i>${track.name}</i>`})}</p>
-                            ${generateTrackList(tracksReferenced)}
-                        `}
-                        ${tracksThatReference.length && fixWS`
-                            <p>${strings('releaseInfo.tracksThatReference', {track: `<i>${track.name}</i>`})}</p>
-                            ${useDividedReferences && fixWS`
-                                <dl>
-                                    ${ttrOfficial.length && fixWS`
-                                        <dt>${strings('trackPage.referenceList.official')}</dt>
-                                        <dd>${generateTrackList(ttrOfficial)}</dd>
-                                    `}
-                                    ${ttrFanon.length && fixWS`
-                                        <dt>${strings('trackPage.referenceList.fandom')}</dt>
-                                        <dd>${generateTrackList(ttrFanon)}</dd>
-                                    `}
-                                </dl>
-                            `}
-                            ${!useDividedReferences && generateTrackList(tracksThatReference)}
-                        `}
-                        ${wikiInfo.features.flashesAndGames && flashesThatFeature.length && fixWS`
-                            <p>${strings('releaseInfo.flashesThatFeature', {track: `<i>${track.name}</i>`})}</p>
-                            <ul>
-                                ${flashesThatFeature.map(({ flash, as }) => fixWS`
-                                    <li ${classes(as !== track && 'rerelease')}>${
-                                        (as === track
-                                            ? strings('releaseInfo.flashesThatFeature.item', {
-                                                flash: link.flash(flash)
-                                            })
-                                            : strings('releaseInfo.flashesThatFeature.item.asDifferentRelease', {
-                                                flash: link.flash(flash),
-                                                track: link.track(as)
-                                            }))
-                                    }</li>
-                                `).join('\n')}
-                            </ul>
-                        `}
-                        ${track.lyrics && fixWS`
-                            <p>${strings('releaseInfo.lyrics')}</p>
-                            <blockquote>
-                                ${transformLyrics(track.lyrics)}
-                            </blockquote>
-                        `}
-                        ${hasCommentary && fixWS`
-                            <p>${strings('releaseInfo.artistCommentary')}</p>
-                            <blockquote>
-                                ${generateCommentary({link, strings, transformMultiline})}
-                            </blockquote>
-                        `}
-                    `
-                },
-
-                sidebarLeft: generateSidebarForAlbum(album, {
-                    currentTrack: track,
-                    link,
-                    strings,
-                    transformMultiline,
-                    wikiData
-                }),
-
-                nav: {
-                    links: [
-                        {toHome: true},
-                        {
-                            path: ['localized.album', album.directory],
-                            title: album.name
-                        },
-                        listTag === 'ol' ? {
-                            html: strings('trackPage.nav.track.withNumber', {
-                                number: album.tracks.indexOf(track) + 1,
-                                track: link.track(track, {class: 'current', to})
-                            })
-                        } : {
-                            html: strings('trackPage.nav.track', {
-                                track: link.track(track, {class: 'current', to})
-                            })
-                        },
-                        album.tracks.length > 1 &&
-                        {
-                            divider: false,
-                            html: generateAlbumNavLinks(album, track, {link, strings})
-                        }
-                    ].filter(Boolean),
-                    content: fixWS`
-                        <div>
-                            ${generateAlbumChronologyLinks(album, track, {link, strings})}
-                        </div>
-                    `
-                }
-            };
-        }
-    };
-
-    return [data, page];
-}
-
 function writeArtistPages({wikiData}) {
     return [
         ...wikiData.artistData.map(artist => writeArtistPage(artist, {wikiData})),
@@ -3598,7 +3315,7 @@ function generateNavForFlash(flash, {link, strings, wikiData}) {
 
         content: fixWS`
             <div>
-                ${chronologyLinks(flash, {
+                ${generateChronologyLinks(flash, {
                     link, strings, wikiData,
                     headingString: 'misc.chronology.heading.flash',
                     contribKey: 'contributors',
@@ -4863,7 +4580,7 @@ function iconifyURL(url, {strings, to}) {
     return fixWS`<a href="${url}" class="icon"><svg><title>${msg}</title><use href="${to('shared.staticFile', `icons.svg#icon-${id}`)}"></use></svg></a>`;
 }
 
-function chronologyLinks(currentThing, {
+function generateChronologyLinks(currentThing, {
     contribKey,
     getThings,
     headingString,
@@ -6069,7 +5786,7 @@ async function main() {
                     to
                 });
 
-                bound.chronologyLinks = bindOpts(chronologyLinks, {
+                bound.generateChronologyLinks = bindOpts(generateChronologyLinks, {
                     link: bound.link,
                     strings,
                     wikiData
@@ -6083,6 +5800,11 @@ async function main() {
                     wikiData
                 });
 
+                bound.generatePreviousNextLinks = bindOpts(generatePreviousNextLinks, {
+                    link: bound.link,
+                    strings
+                });
+
                 bound.getGridHTML = bindOpts(getGridHTML, {
                     [bindOpts.bindIndex]: 0,
                     strings
diff --git a/src/util/magic-constants.js b/src/util/magic-constants.js
new file mode 100644
index 00000000..3174daec
--- /dev/null
+++ b/src/util/magic-constants.js
@@ -0,0 +1,11 @@
+// Magic constants only! These are hard-coded, and any use of them should be
+// considered a flaw in the codebase - areas where we use hard-coded behavior
+// to support one use of the wiki software (i.e. HSMusic, usually), rather than
+// implementing the feature more generally/customizably.
+//
+// All such uses should eventually be replaced with better code in due time
+// (TM).
+
+export const UNRELEASED_TRACKS_DIRECTORY = 'unreleased-tracks';
+export const OFFICIAL_GROUP_DIRECTORY = 'official';
+export const FANDOM_GROUP_DIRECTORY = 'fandom';
diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js
index 13b86090..e4142c85 100644
--- a/src/util/wiki-data.js
+++ b/src/util/wiki-data.js
@@ -1,5 +1,9 @@
 // Utility functions for interacting with wiki data.
 
+import {
+    UNRELEASED_TRACKS_DIRECTORY
+} from '../util/magic-constants.js';
+
 // Generic value operations
 
 export function getKebabCase(name) {
@@ -95,7 +99,7 @@ export function getAlbumCover(album, {to}) {
 
 export function getAlbumListTag(album) {
     // TODO: This is hard-coded! No. 8ad.
-    return (album.directory === 'unreleased-tracks' ? 'ul' : 'ol');
+    return (album.directory === UNRELEASED_TRACKS_DIRECTORY ? 'ul' : 'ol');
 }
 
 // This gets all the track o8jects defined in every al8um, and sorts them 8y