« 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/listing-spec.js9
-rw-r--r--src/misc-templates.js369
-rw-r--r--src/page/album-commentary.js7
-rw-r--r--src/page/album.js275
-rw-r--r--src/page/flash.js7
-rw-r--r--src/page/group.js32
-rw-r--r--src/page/homepage.js5
-rw-r--r--src/page/listing.js77
-rw-r--r--src/page/tag.js5
-rw-r--r--src/page/track.js7
-rwxr-xr-xsrc/upd8.js434
-rw-r--r--src/util/colors.js28
-rw-r--r--src/util/link.js9
13 files changed, 669 insertions, 595 deletions
diff --git a/src/listing-spec.js b/src/listing-spec.js
index a5239a41..90a2de5a 100644
--- a/src/listing-spec.js
+++ b/src/listing-spec.js
@@ -1,10 +1,6 @@
 import fixWS from 'fix-whitespace';
 
 import {
-    getLinkThemeString
-} from './util/colors.js';
-
-import {
     UNRELEASED_TRACKS_DIRECTORY
 } from './util/magic-constants.js';
 
@@ -757,7 +753,10 @@ const listingSpec = [
             fandomAlbumData: wikiData.fandomAlbumData
         }),
 
-        html: ({officialAlbumData, fandomAlbumData}, {strings}) => fixWS`
+        html: ({officialAlbumData, fandomAlbumData}, {
+            getLinkThemeString,
+            strings
+        }) => fixWS`
             <p>Choose a link to go to a random page in that category or album! If your browser doesn't support relatively modern JavaScript or you've disabled it, these links won't work - sorry.</p>
             <p class="js-hide-once-data">(Data files are downloading in the background! Please wait for data to load.)</p>
             <p class="js-show-once-data">(Data files have finished being downloaded. The links should work!)</p>
diff --git a/src/misc-templates.js b/src/misc-templates.js
new file mode 100644
index 00000000..d4e4af38
--- /dev/null
+++ b/src/misc-templates.js
@@ -0,0 +1,369 @@
+// Miscellaneous utility functions which are useful across page specifications.
+// These are made available right on a page spec's ({wikiData, strings, ...})
+// args object!
+
+import fixWS from 'fix-whitespace';
+
+import * as html from './util/html.js';
+
+import {
+    getColors
+} from './util/colors.js';
+
+import {
+    UNRELEASED_TRACKS_DIRECTORY
+} from './util/magic-constants.js';
+
+import {
+    unique
+} from './util/sugar.js';
+
+import {
+    getTotalDuration,
+    sortByDate
+} from './util/wiki-data.js';
+
+// Artist strings
+
+export function getArtistString(artists, {
+    iconifyURL, link, strings,
+    showIcons = false,
+    showContrib = false
+}) {
+    return strings.list.and(artists.map(({ who, what }) => {
+        const { urls, directory, name } = who;
+        return [
+            link.artist(who),
+            showContrib && what && `(${what})`,
+            showIcons && urls.length && `<span class="icons">(${
+                strings.list.unit(urls.map(url => iconifyURL(url, {strings})))
+            })</span>`
+        ].filter(Boolean).join(' ');
+    }));
+}
+
+// Chronology links
+
+export function generateChronologyLinks(currentThing, {
+    contribKey,
+    getThings,
+    headingString,
+    link,
+    linkAnythingMan,
+    strings,
+    wikiData
+}) {
+    const { albumData } = wikiData;
+
+    const contributions = currentThing[contribKey];
+    if (!contributions) {
+        return '';
+    }
+
+    if (contributions.length > 8) {
+        return `<div class="chronology">${strings('misc.chronology.seeArtistPages')}</div>`;
+    }
+
+    return contributions.map(({ who: artist }) => {
+        const things = sortByDate(unique(getThings(artist)));
+        const releasedThings = things.filter(thing => {
+            const album = albumData.includes(thing) ? thing : thing.album;
+            return !(album && album.directory === UNRELEASED_TRACKS_DIRECTORY);
+        });
+        const index = releasedThings.indexOf(currentThing);
+
+        if (index === -1) return '';
+
+        // TODO: This can pro8a8ly 8e made to use generatePreviousNextLinks?
+        // We'd need to make generatePreviousNextLinks use toAnythingMan tho.
+        const previous = releasedThings[index - 1];
+        const next = releasedThings[index + 1];
+        const parts = [
+            previous && linkAnythingMan(previous, {
+                color: false,
+                text: strings('misc.nav.previous')
+            }),
+            next && linkAnythingMan(next, {
+                color: false,
+                text: strings('misc.nav.next')
+            })
+        ].filter(Boolean);
+
+        const stringOpts = {
+            index: strings.count.index(index + 1, {strings}),
+            artist: link.artist(artist)
+        };
+
+        return fixWS`
+            <div class="chronology">
+                <span class="heading">${strings(headingString, stringOpts)}</span>
+                ${parts.length && `<span class="buttons">(${parts.join(', ')})</span>`}
+            </div>
+        `;
+    }).filter(Boolean).join('\n');
+}
+
+// Content warning tags
+
+export function getRevealStringFromWarnings(warnings, {strings}) {
+    return strings('misc.contentWarnings', {warnings}) + `<br><span class="reveal-interaction">${strings('misc.contentWarnings.reveal')}</span>`
+}
+
+export function getRevealStringFromTags(tags, {strings}) {
+    return tags && tags.some(tag => tag.isCW) && (
+        getRevealStringFromWarnings(strings.list.unit(tags.filter(tag => tag.isCW).map(tag => tag.name)), {strings}));
+}
+
+// Cover art links
+
+export function generateCoverLink({
+    img, link, strings, to, wikiData,
+    src,
+    path,
+    alt,
+    tags = []
+}) {
+    const { wikiInfo } = wikiData;
+
+    if (!src && path) {
+        src = to(...path);
+    }
+
+    if (!src) {
+        throw new Error(`Expected src or path`);
+    }
+
+    return fixWS`
+        <div id="cover-art-container">
+            ${img({
+                src,
+                alt,
+                thumb: 'medium',
+                id: 'cover-art',
+                link: true,
+                square: true,
+                reveal: getRevealStringFromTags(tags, {strings})
+            })}
+            ${wikiInfo.features.artTagUI && tags.filter(tag => !tag.isCW).length && fixWS`
+                <p class="tags">
+                    ${strings('releaseInfo.artTags')}
+                    ${(tags
+                        .filter(tag => !tag.isCW)
+                        .map(link.tag)
+                        .join(',\n'))}
+                </p>
+            `}
+        </div>
+    `;
+}
+
+// CSS & color shenanigans
+
+export function getThemeString(color, additionalVariables = []) {
+    if (!color) return '';
+
+    const { primary, dim, bg } = getColors(color);
+
+    const variables = [
+        `--primary-color: ${primary}`,
+        `--dim-color: ${dim}`,
+        `--bg-color: ${bg}`,
+        ...additionalVariables
+    ].filter(Boolean);
+
+    if (!variables.length) return '';
+
+    return (
+        `:root {\n` +
+        variables.map(line => `    ` + line + ';\n').join('') +
+        `}`
+    );
+}
+export function getAlbumStylesheet(album, {to}) {
+    return [
+        album.wallpaperArtists && fixWS`
+            body::before {
+                background-image: url("${to('media.albumWallpaper', album.directory)}");
+                ${album.wallpaperStyle}
+            }
+        `,
+        album.bannerStyle && fixWS`
+            #banner img {
+                ${album.bannerStyle}
+            }
+        `
+    ].filter(Boolean).join('\n');
+}
+
+// Fancy lookin' links
+
+export function fancifyURL(url, {strings, album = false} = {}) {
+    const domain = new URL(url).hostname;
+    return fixWS`<a href="${url}" class="nowrap">${
+        domain.includes('bandcamp.com') ? strings('misc.external.bandcamp') :
+        [
+            'music.solatrux.com'
+        ].includes(domain) ? strings('misc.external.bandcamp.domain', {domain}) :
+        [
+            'types.pl'
+        ].includes(domain) ? strings('misc.external.mastodon.domain', {domain}) :
+        domain.includes('youtu') ? (album
+            ? (url.includes('list=')
+                ? strings('misc.external.youtube.playlist')
+                : strings('misc.external.youtube.fullAlbum'))
+            : strings('misc.external.youtube')) :
+        domain.includes('soundcloud') ? strings('misc.external.soundcloud') :
+        domain.includes('tumblr.com') ? strings('misc.external.tumblr') :
+        domain.includes('twitter.com') ? strings('misc.external.twitter') :
+        domain.includes('deviantart.com') ? strings('misc.external.deviantart') :
+        domain.includes('wikipedia.org') ? strings('misc.external.wikipedia') :
+        domain.includes('poetryfoundation.org') ? strings('misc.external.poetryFoundation') :
+        domain.includes('instagram.com') ? strings('misc.external.instagram') :
+        domain.includes('patreon.com') ? strings('misc.external.patreon') :
+        domain
+    }</a>`;
+}
+
+export function fancifyFlashURL(url, flash, {strings}) {
+    const link = fancifyURL(url, {strings});
+    return `<span class="nowrap">${
+        url.includes('homestuck.com') ? (isNaN(Number(flash.page))
+            ? strings('misc.external.flash.homestuck.secret', {link})
+            : strings('misc.external.flash.homestuck.page', {link, page: flash.page})) :
+        url.includes('bgreco.net') ? strings('misc.external.flash.bgreco', {link}) :
+        url.includes('youtu') ? strings('misc.external.flash.youtube', {link}) :
+        link
+    }</span>`;
+}
+
+export function iconifyURL(url, {strings, to}) {
+    const domain = new URL(url).hostname;
+    const [ id, msg ] = (
+        domain.includes('bandcamp.com') ? ['bandcamp', strings('misc.external.bandcamp')] :
+        (
+            domain.includes('music.solatrus.com')
+        ) ? ['bandcamp', strings('misc.external.bandcamp.domain', {domain})] :
+        (
+            domain.includes('types.pl')
+        ) ? ['mastodon', strings('misc.external.mastodon.domain', {domain})] :
+        domain.includes('youtu') ? ['youtube', strings('misc.external.youtube')] :
+        domain.includes('soundcloud') ? ['soundcloud', strings('misc.external.soundcloud')] :
+        domain.includes('tumblr.com') ? ['tumblr', strings('misc.external.tumblr')] :
+        domain.includes('twitter.com') ? ['twitter', strings('misc.external.twitter')] :
+        domain.includes('deviantart.com') ? ['deviantart', strings('misc.external.deviantart')] :
+        domain.includes('instagram.com') ? ['instagram', strings('misc.external.bandcamp')] :
+        ['globe', strings('misc.external.domain', {domain})]
+    );
+    return fixWS`<a href="${url}" class="icon"><svg><title>${msg}</title><use href="${to('shared.staticFile', `icons.svg#icon-${id}`)}"></use></svg></a>`;
+}
+
+// Grids
+
+export function getGridHTML({
+    getLinkThemeString,
+    img,
+    strings,
+
+    entries,
+    srcFn,
+    hrefFn,
+    altFn = () => '',
+    detailsFn = null,
+    lazy = true
+}) {
+    return entries.map(({ large, item }, i) => html.tag('a',
+        {
+            class: ['grid-item', 'box', large && 'large-grid-item'],
+            href: hrefFn(item),
+            style: getLinkThemeString(item.color)
+        },
+        fixWS`
+            ${img({
+                src: srcFn(item),
+                alt: altFn(item),
+                thumb: 'small',
+                lazy: (typeof lazy === 'number' ? i >= lazy : lazy),
+                square: true,
+                reveal: getRevealStringFromTags(item.artTags, {strings})
+            })}
+            <span>${item.name}</span>
+            ${detailsFn && `<span>${detailsFn(item)}</span>`}
+        `)).join('\n');
+}
+
+export function getAlbumGridHTML({
+    getAlbumCover, getGridHTML, strings, to,
+    details = false,
+    ...props
+}) {
+    return getGridHTML({
+        srcFn: getAlbumCover,
+        hrefFn: album => to('localized.album', album.directory),
+        detailsFn: details && (album => strings('misc.albumGridDetails', {
+            tracks: strings.count.tracks(album.tracks.length, {unit: true}),
+            time: strings.count.duration(getTotalDuration(album.tracks))
+        })),
+        ...props
+    });
+}
+
+export function getFlashGridHTML({
+    getFlashCover, getGridHTML, to,
+    ...props
+}) {
+    return getGridHTML({
+        srcFn: getFlashCover,
+        hrefFn: flash => to('localized.flash', flash.directory),
+        ...props
+    });
+}
+// Nav-bar links
+
+export function generateInfoGalleryLinks(currentThing, isGallery, {
+    link, strings,
+    linkKeyGallery,
+    linkKeyInfo
+}) {
+    return [
+        link[linkKeyInfo](currentThing, {
+            class: isGallery ? '' : 'current',
+            text: strings('misc.nav.info')
+        }),
+        link[linkKeyGallery](currentThing, {
+            class: isGallery ? 'current' : '',
+            text: strings('misc.nav.gallery')
+        })
+    ].join(', ');
+}
+
+export function generatePreviousNextLinks(current, {
+    data,
+    link,
+    linkKey,
+    strings
+}) {
+    const linkFn = link[linkKey];
+
+    const index = data.indexOf(current);
+    const previous = data[index - 1];
+    const next = data[index + 1];
+
+    return [
+        previous && linkFn(previous, {
+            attributes: {
+                id: 'previous-button',
+                title: previous.name
+            },
+            text: strings('misc.nav.previous'),
+            color: false
+        }),
+        next && linkFn(next, {
+            attributes: {
+                id: 'next-button',
+                title: next.name
+            },
+            text: strings('misc.nav.next'),
+            color: false
+        })
+    ].filter(Boolean).join(', ');
+}
diff --git a/src/page/album-commentary.js b/src/page/album-commentary.js
index 77ca3ef1..c03ae3db 100644
--- a/src/page/album-commentary.js
+++ b/src/page/album-commentary.js
@@ -5,11 +5,6 @@
 import fixWS from 'fix-whitespace';
 
 import {
-    getLinkThemeString,
-    getThemeString
-} from '../util/colors.js';
-
-import {
     filterAlbumsByCommentary
 } from '../util/wiki-data.js';
 
@@ -34,6 +29,8 @@ export function write(album, {wikiData}) {
         path: ['albumCommentary', album.directory],
         page: ({
             getAlbumStylesheet,
+            getLinkThemeString,
+            getThemeString,
             link,
             strings,
             to,
diff --git a/src/page/album.js b/src/page/album.js
index 16da6021..adcc0584 100644
--- a/src/page/album.js
+++ b/src/page/album.js
@@ -4,14 +4,13 @@
 
 import fixWS from 'fix-whitespace';
 
-import {
-    getLinkThemeString,
-    getThemeString
-} from '../util/colors.js';
-
 import * as html from '../util/html.js';
 
 import {
+    bindOpts
+} from '../util/sugar.js';
+
+import {
     getAlbumCover,
     getAlbumListTag,
     getTotalDuration
@@ -26,7 +25,12 @@ export function targets({wikiData}) {
 export function write(album, {wikiData}) {
     const { wikiInfo } = wikiData;
 
-    const trackToListItem = (track, {getArtistString, link, strings}) => {
+    const unbound_trackToListItem = (track, {
+        getArtistString,
+        getLinkThemeString,
+        link,
+        strings
+    }) => {
         const itemOpts = {
             duration: strings.count.duration(track.duration),
             track: link.track(track)
@@ -96,140 +100,152 @@ export function write(album, {wikiData}) {
             generateCoverLink,
             getAlbumStylesheet,
             getArtistString,
+            getLinkThemeString,
+            getThemeString,
             link,
             strings,
             transformMultiline
-        }) => ({
-            title: strings('albumPage.title', {album: album.name}),
-            stylesheet: getAlbumStylesheet(album),
-            theme: getThemeString(album.color, [
-                `--album-directory: ${album.directory}`
-            ]),
+        }) => {
+            const trackToListItem = bindOpts(unbound_trackToListItem, {
+                getArtistString,
+                getLinkThemeString,
+                link,
+                strings
+            });
 
-            banner: album.bannerArtists && {
-                dimensions: album.bannerDimensions,
-                path: ['media.albumBanner', album.directory],
-                alt: strings('misc.alt.albumBanner'),
-                position: 'top'
-            },
+            return {
+                title: strings('albumPage.title', {album: album.name}),
+                stylesheet: getAlbumStylesheet(album),
+                theme: getThemeString(album.color, [
+                    `--album-directory: ${album.directory}`
+                ]),
 
-            main: {
-                content: fixWS`
-                    ${generateCoverLink({
-                        path: ['media.albumCover', album.directory],
-                        alt: strings('misc.alt.albumCover'),
-                        tags: album.artTags
-                    })}
-                    <h1>${strings('albumPage.title', {album: album.name})}</h1>
-                    <p>
-                        ${[
-                            album.artists && strings('releaseInfo.by', {
-                                artists: getArtistString(album.artists, {
-                                    showContrib: true,
-                                    showIcons: true
-                                })
-                            }),
-                            album.coverArtists && strings('releaseInfo.coverArtBy', {
-                                artists: getArtistString(album.coverArtists, {
-                                    showContrib: true,
-                                    showIcons: true
-                                })
-                            }),
-                            album.wallpaperArtists && strings('releaseInfo.wallpaperArtBy', {
-                                artists: getArtistString(album.wallpaperArtists, {
-                                    showContrib: true,
-                                    showIcons: true
+                banner: album.bannerArtists && {
+                    dimensions: album.bannerDimensions,
+                    path: ['media.albumBanner', album.directory],
+                    alt: strings('misc.alt.albumBanner'),
+                    position: 'top'
+                },
+
+                main: {
+                    content: fixWS`
+                        ${generateCoverLink({
+                            path: ['media.albumCover', album.directory],
+                            alt: strings('misc.alt.albumCover'),
+                            tags: album.artTags
+                        })}
+                        <h1>${strings('albumPage.title', {album: album.name})}</h1>
+                        <p>
+                            ${[
+                                album.artists && strings('releaseInfo.by', {
+                                    artists: getArtistString(album.artists, {
+                                        showContrib: true,
+                                        showIcons: true
+                                    })
+                                }),
+                                album.coverArtists && strings('releaseInfo.coverArtBy', {
+                                    artists: getArtistString(album.coverArtists, {
+                                        showContrib: true,
+                                        showIcons: true
+                                    })
+                                }),
+                                album.wallpaperArtists && strings('releaseInfo.wallpaperArtBy', {
+                                    artists: getArtistString(album.wallpaperArtists, {
+                                        showContrib: true,
+                                        showIcons: true
+                                    })
+                                }),
+                                album.bannerArtists && strings('releaseInfo.bannerArtBy', {
+                                    artists: getArtistString(album.bannerArtists, {
+                                        showContrib: true,
+                                        showIcons: true
+                                    })
+                                }),
+                                strings('releaseInfo.released', {
+                                    date: strings.count.date(album.date)
+                                }),
+                                +album.coverArtDate !== +album.date && strings('releaseInfo.artReleased', {
+                                    date: strings.count.date(album.coverArtDate)
+                                }),
+                                strings('releaseInfo.duration', {
+                                    duration: strings.count.duration(albumDuration, {approximate: album.tracks.length > 1})
                                 })
-                            }),
-                            album.bannerArtists && strings('releaseInfo.bannerArtBy', {
-                                artists: getArtistString(album.bannerArtists, {
-                                    showContrib: true,
-                                    showIcons: true
+                            ].filter(Boolean).join('<br>\n')}
+                        </p>
+                        ${commentaryEntries && `<p>${
+                            strings('releaseInfo.viewCommentary', {
+                                link: link.albumCommentary(album, {
+                                    text: strings('releaseInfo.viewCommentary.link')
                                 })
-                            }),
-                            strings('releaseInfo.released', {
-                                date: strings.count.date(album.date)
-                            }),
-                            +album.coverArtDate !== +album.date && strings('releaseInfo.artReleased', {
-                                date: strings.count.date(album.coverArtDate)
-                            }),
-                            strings('releaseInfo.duration', {
-                                duration: strings.count.duration(albumDuration, {approximate: album.tracks.length > 1})
-                            })
-                        ].filter(Boolean).join('<br>\n')}
-                    </p>
-                    ${commentaryEntries && `<p>${
-                        strings('releaseInfo.viewCommentary', {
-                            link: link.albumCommentary(album, {
-                                text: strings('releaseInfo.viewCommentary.link')
                             })
-                        })
-                    }</p>`}
-                    ${album.urls.length && `<p>${
-                        strings('releaseInfo.listenOn', {
-                            links: strings.list.or(album.urls.map(url => fancifyURL(url, {album: true})))
-                        })
-                    }</p>`}
-                    ${album.trackGroups ? fixWS`
-                        <dl class="album-group-list">
-                            ${album.trackGroups.map(({ name, color, startIndex, tracks }) => fixWS`
-                                <dt>${
-                                    strings('trackList.group', {
-                                        duration: strings.count.duration(getTotalDuration(tracks), {approximate: tracks.length > 1}),
-                                        group: name
-                                    })
-                                }</dt>
-                                <dd><${listTag === 'ol' ? `ol start="${startIndex + 1}"` : listTag}>
-                                    ${tracks.map(t => trackToListItem(t, {getArtistString, link, strings})).join('\n')}
-                                </${listTag}></dd>
-                            `).join('\n')}
-                        </dl>
-                    ` : fixWS`
-                        <${listTag}>
-                            ${album.tracks.map(t => trackToListItem(t, {getArtistString, link, strings})).join('\n')}
-                        </${listTag}>
-                    `}
-                    <p>
-                        ${[
-                            strings('releaseInfo.addedToWiki', {
-                                date: strings.count.date(album.dateAdded)
+                        }</p>`}
+                        ${album.urls.length && `<p>${
+                            strings('releaseInfo.listenOn', {
+                                links: strings.list.or(album.urls.map(url => fancifyURL(url, {album: true})))
                             })
-                        ].filter(Boolean).join('<br>\n')}
-                    </p>
-                    ${album.commentary && fixWS`
-                        <p>${strings('releaseInfo.artistCommentary')}</p>
-                        <blockquote>
-                            ${transformMultiline(album.commentary)}
-                        </blockquote>
-                    `}
-                `
-            },
+                        }</p>`}
+                        ${album.trackGroups ? fixWS`
+                            <dl class="album-group-list">
+                                ${album.trackGroups.map(({ name, color, startIndex, tracks }) => fixWS`
+                                    <dt>${
+                                        strings('trackList.group', {
+                                            duration: strings.count.duration(getTotalDuration(tracks), {approximate: tracks.length > 1}),
+                                            group: name
+                                        })
+                                    }</dt>
+                                    <dd><${listTag === 'ol' ? `ol start="${startIndex + 1}"` : listTag}>
+                                        ${tracks.map(trackToListItem).join('\n')}
+                                    </${listTag}></dd>
+                                `).join('\n')}
+                            </dl>
+                        ` : fixWS`
+                            <${listTag}>
+                                ${album.tracks.map(trackToListItem).join('\n')}
+                            </${listTag}>
+                        `}
+                        <p>
+                            ${[
+                                strings('releaseInfo.addedToWiki', {
+                                    date: strings.count.date(album.dateAdded)
+                                })
+                            ].filter(Boolean).join('<br>\n')}
+                        </p>
+                        ${album.commentary && fixWS`
+                            <p>${strings('releaseInfo.artistCommentary')}</p>
+                            <blockquote>
+                                ${transformMultiline(album.commentary)}
+                            </blockquote>
+                        `}
+                    `
+                },
 
-            sidebarLeft: generateAlbumSidebar(album, null, {
-                fancifyURL,
-                link,
-                strings,
-                transformMultiline,
-                wikiData
-            }),
+                sidebarLeft: generateAlbumSidebar(album, null, {
+                    fancifyURL,
+                    getLinkThemeString,
+                    link,
+                    strings,
+                    transformMultiline,
+                    wikiData
+                }),
 
-            nav: {
-                links: [
-                    {toHome: true},
-                    {
-                        html: strings('albumPage.nav.album', {
-                            album: link.album(album, {class: 'current'})
-                        })
-                    },
-                    album.tracks.length > 1 &&
-                    {
-                        divider: false,
-                        html: generateAlbumNavLinks(album, null, {strings})
-                    }
-                ],
-                content: html.tag('div', generateAlbumChronologyLinks(album, null, {generateChronologyLinks}))
-            }
-        })
+                nav: {
+                    links: [
+                        {toHome: true},
+                        {
+                            html: strings('albumPage.nav.album', {
+                                album: link.album(album, {class: 'current'})
+                            })
+                        },
+                        album.tracks.length > 1 &&
+                        {
+                            divider: false,
+                            html: generateAlbumNavLinks(album, null, {strings})
+                        }
+                    ],
+                    content: html.tag('div', generateAlbumChronologyLinks(album, null, {generateChronologyLinks}))
+                }
+            };
+        }
     };
 
     return [page, data];
@@ -239,6 +255,7 @@ export function write(album, {wikiData}) {
 
 export function generateAlbumSidebar(album, currentTrack, {
     fancifyURL,
+    getLinkThemeString,
     link,
     strings,
     transformMultiline,
diff --git a/src/page/flash.js b/src/page/flash.js
index 4ffaefc6..9c59016d 100644
--- a/src/page/flash.js
+++ b/src/page/flash.js
@@ -4,11 +4,6 @@
 
 import fixWS from 'fix-whitespace';
 
-import {
-    getLinkThemeString,
-    getThemeString
-} from '../util/colors.js';
-
 import * as html from '../util/html.js';
 
 import {
@@ -36,6 +31,7 @@ export function write(flash, {wikiData}) {
             generatePreviousNextLinks,
             getArtistString,
             getFlashCover,
+            getThemeString,
             link,
             strings,
             transformInline
@@ -118,6 +114,7 @@ export function writeTargetless({wikiData}) {
         path: ['flashIndex'],
         page: ({
             getFlashGridHTML,
+            getLinkThemeString,
             link,
             strings
         }) => ({
diff --git a/src/page/group.js b/src/page/group.js
index 5db5a8ff..698c17ec 100644
--- a/src/page/group.js
+++ b/src/page/group.js
@@ -8,11 +8,6 @@ import {
     UNRELEASED_TRACKS_DIRECTORY
 } from '../util/magic-constants.js';
 
-import {
-    getLinkThemeString,
-    getThemeString
-} from '../util/colors.js';
-
 import * as html from '../util/html.js';
 
 import {
@@ -44,6 +39,8 @@ export function write(group, {wikiData}) {
         page: ({
             generateInfoGalleryLinks,
             generatePreviousNextLinks,
+            getLinkThemeString,
+            getThemeString,
             fancifyURL,
             link,
             strings,
@@ -92,7 +89,13 @@ export function write(group, {wikiData}) {
                 `
             },
 
-            sidebarLeft: generateGroupSidebar(group, false, {link, strings, wikiData}),
+            sidebarLeft: generateGroupSidebar(group, false, {
+                getLinkThemeString,
+                link,
+                strings,
+                wikiData
+            }),
+
             nav: generateGroupNav(group, false, {
                 generateInfoGalleryLinks,
                 generatePreviousNextLinks,
@@ -110,6 +113,8 @@ export function write(group, {wikiData}) {
             generateInfoGalleryLinks,
             generatePreviousNextLinks,
             getAlbumGridHTML,
+            getLinkThemeString,
+            getThemeString,
             link,
             strings
         }) => ({
@@ -144,7 +149,13 @@ export function write(group, {wikiData}) {
                 `
             },
 
-            sidebarLeft: generateGroupSidebar(group, true, {link, strings, wikiData}),
+            sidebarLeft: generateGroupSidebar(group, true, {
+                getLinkThemeString,
+                link,
+                strings,
+                wikiData
+            }),
+
             nav: generateGroupNav(group, true, {
                 generateInfoGalleryLinks,
                 generatePreviousNextLinks,
@@ -160,7 +171,12 @@ export function write(group, {wikiData}) {
 
 // Utility functions
 
-function generateGroupSidebar(currentGroup, isGallery, {link, strings, wikiData}) {
+function generateGroupSidebar(currentGroup, isGallery, {
+    getLinkThemeString,
+    link,
+    strings,
+    wikiData
+}) {
     const { groupCategoryData, wikiInfo } = wikiData;
 
     if (!wikiInfo.features.groupUI) {
diff --git a/src/page/homepage.js b/src/page/homepage.js
index d1dcc680..37ec4426 100644
--- a/src/page/homepage.js
+++ b/src/page/homepage.js
@@ -4,10 +4,6 @@
 
 import fixWS from 'fix-whitespace';
 
-import {
-    getLinkThemeString
-} from '../util/colors.js';
-
 import find from '../util/find.js';
 
 import * as html from '../util/html.js';
@@ -27,6 +23,7 @@ export function writeTargetless({wikiData}) {
         path: ['home'],
         page: ({
             getAlbumGridHTML,
+            getLinkThemeString,
             link,
             strings,
             to,
diff --git a/src/page/listing.js b/src/page/listing.js
index b0766b28..d4021747 100644
--- a/src/page/listing.js
+++ b/src/page/listing.js
@@ -46,44 +46,45 @@ export function write(listing, {wikiData}) {
     const page = {
         type: 'page',
         path: ['listing', listing.directory],
-        page: ({
-            link,
-            strings
-        }) => ({
-            title: listing.title({strings}),
-
-            main: {
-                content: fixWS`
-                    <h1>${listing.title({strings})}</h1>
-                    ${listing.html && (listing.data
-                        ? listing.html(data, {link, strings})
-                        : listing.html({link, strings}))}
-                    ${listing.row && fixWS`
-                        <ul>
-                            ${(data
-                                .map(item => listing.row(item, {link, strings}))
-                                .map(row => `<li>${row}</li>`)
-                                .join('\n'))}
-                        </ul>
-                    `}
-                `
-            },
-
-            sidebarLeft: {
-                content: generateSidebarForListings(listing, {link, strings, wikiData})
-            },
-
-            nav: {
-                links: [
-                    {toHome: true},
-                    {
-                        path: ['localized.listingIndex'],
-                        title: strings('listingIndex.title')
-                    },
-                    {toCurrentPage: true}
-                ]
-            }
-        })
+        page: opts => {
+            const { link, strings } = opts;
+
+            return {
+                title: listing.title({strings}),
+
+                main: {
+                    content: fixWS`
+                        <h1>${listing.title({strings})}</h1>
+                        ${listing.html && (listing.data
+                            ? listing.html(data, opts)
+                            : listing.html(opts))}
+                        ${listing.row && fixWS`
+                            <ul>
+                                ${(data
+                                    .map(item => listing.row(item, opts))
+                                    .map(row => `<li>${row}</li>`)
+                                    .join('\n'))}
+                            </ul>
+                        `}
+                    `
+                },
+
+                sidebarLeft: {
+                    content: generateSidebarForListings(listing, {link, strings, wikiData})
+                },
+
+                nav: {
+                    links: [
+                        {toHome: true},
+                        {
+                            path: ['localized.listingIndex'],
+                            title: strings('listingIndex.title')
+                        },
+                        {toCurrentPage: true}
+                    ]
+                }
+            };
+        }
     };
 
     return [page];
diff --git a/src/page/tag.js b/src/page/tag.js
index c6f64bfc..610f4665 100644
--- a/src/page/tag.js
+++ b/src/page/tag.js
@@ -4,10 +4,6 @@
 
 import fixWS from 'fix-whitespace';
 
-import {
-    getThemeString
-} from '../util/colors.js';
-
 // Page exports
 
 export function condition({wikiData}) {
@@ -28,6 +24,7 @@ export function write(tag, {wikiData}) {
         page: ({
             getAlbumCover,
             getGridHTML,
+            getThemeString,
             getTrackCover,
             link,
             strings,
diff --git a/src/page/track.js b/src/page/track.js
index 2dec9bd3..0941ee89 100644
--- a/src/page/track.js
+++ b/src/page/track.js
@@ -10,10 +10,6 @@ import {
     generateAlbumSidebar
 } from './album.js';
 
-import {
-    getThemeString
-} from '../util/colors.js';
-
 import * as html from '../util/html.js';
 
 import {
@@ -134,6 +130,8 @@ export function write(track, {wikiData}) {
             generatePreviousNextLinks,
             getAlbumStylesheet,
             getArtistString,
+            getLinkThemeString,
+            getThemeString,
             getTrackCover,
             link,
             strings,
@@ -284,6 +282,7 @@ export function write(track, {wikiData}) {
 
                 sidebarLeft: generateAlbumSidebar(album, track, {
                     fancifyURL,
+                    getLinkThemeString,
                     link,
                     strings,
                     transformMultiline,
diff --git a/src/upd8.js b/src/upd8.js
index 345f166b..8652f65c 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -116,7 +116,25 @@ import * as pageSpecs from './page/index.js';
 
 import find from './util/find.js';
 import * as html from './util/html.js';
-import unbound_link from './util/link.js';
+import unbound_link, {getLinkThemeString} from './util/link.js';
+
+import {
+    fancifyFlashURL,
+    fancifyURL,
+    generateChronologyLinks,
+    generateCoverLink,
+    generateInfoGalleryLinks,
+    generatePreviousNextLinks,
+    getAlbumGridHTML,
+    getAlbumStylesheet,
+    getArtistString,
+    getFlashGridHTML,
+    getGridHTML,
+    getRevealStringFromTags,
+    getRevealStringFromWarnings,
+    getThemeString,
+    iconifyURL
+} from './misc-templates.js';
 
 import {
     decorateTime,
@@ -128,11 +146,6 @@ import {
 } from './util/cli.js';
 
 import {
-    getLinkThemeString,
-    getThemeString
-} from './util/colors.js';
-
-import {
     validateReplacerSpec,
     transformInline
 } from './util/replacer.js';
@@ -1761,51 +1774,43 @@ writePage.html = (pageFn, {
 
     const collapseSidebars = (sidebarLeft.collapse !== false) && (sidebarRight.collapse !== false);
 
-    const mainHTML = main.content && fixWS`
-        <main id="content" ${classes(...main.classes || [])}>
-            ${main.content}
-        </main>
-    `;
+    const mainHTML = main.content && html.tag('main', {
+        id: 'content',
+        class: main.classes
+    }, main.content);
 
-    const footerHTML = footer.content && fixWS`
-        <footer id="footer" ${classes(...footer.classes || [])}>
-            ${footer.content}
-        </footer>
-    `;
+    const footerHTML = footer.content && html.tag('footer', {
+        id: 'footer',
+        class: footer.classes
+    }, footer.content);
 
     const generateSidebarHTML = (id, {
         content,
         multiple,
-        classes: sidebarClasses = [],
+        classes,
         collapse = true,
         wide = false
-    }) => (content ? fixWS`
-        <div id="${id}" ${classes(
-            'sidebar-column',
-            'sidebar',
-            wide && 'wide',
-            !collapse && 'no-hide',
-            ...sidebarClasses
-        )}>
-            ${content}
-        </div>
-    ` : multiple ? fixWS`
-        <div id="${id}" ${classes(
-            'sidebar-column',
-            'sidebar-multiple',
-            wide && 'wide',
-            !collapse && 'no-hide'
-        )}>
-            ${multiple.map(content => fixWS`
-                <div ${classes(
-                    'sidebar',
-                    ...sidebarClasses
-                )}>
-                    ${content}
-                </div>
-            `).join('\n')}
-        </div>
-    ` : '');
+    }) => (content
+        ? html.tag('div',
+            {id, class: [
+                'sidebar-column',
+                'sidebar',
+                wide && 'wide',
+                !collapse && 'no-hide',
+                ...classes
+            ]},
+            content)
+        : multiple ? html.tag('div',
+            {id, class: [
+                'sidebar-column',
+                'sidebar-multiple',
+                wide && 'wide',
+                !collapse && 'no-hide'
+            ]},
+            multiple.map(content => html.tag('div',
+                {class: ['sidebar', ...classes]},
+                content)))
+        : '');
 
     const sidebarLeftHTML = generateSidebarHTML('sidebar-left', sidebarLeft);
     const sidebarRightHTML = generateSidebarHTML('sidebar-right', sidebarRight);
@@ -2012,58 +2017,6 @@ writePage.paths = (baseDirectory, fullKey, directory = '', {
     };
 };
 
-function getGridHTML({
-    strings,
-    entries,
-    srcFn,
-    hrefFn,
-    altFn = () => '',
-    detailsFn = null,
-    lazy = true
-}) {
-    return entries.map(({ large, item }, i) => fixWS`
-        <a ${classes('grid-item', 'box', large && 'large-grid-item')} href="${hrefFn(item)}" style="${getLinkThemeString(item.color)}">
-            ${img({
-                src: srcFn(item),
-                alt: altFn(item),
-                thumb: 'small',
-                lazy: (typeof lazy === 'number' ? i >= lazy : lazy),
-                square: true,
-                reveal: getRevealStringFromTags(item.artTags, {strings})
-            })}
-            <span>${item.name}</span>
-            ${detailsFn && `<span>${detailsFn(item)}</span>`}
-        </a>
-    `).join('\n');
-}
-
-function getAlbumGridHTML({
-    getAlbumCover, getGridHTML, strings, to,
-    details = false,
-    ...props
-}) {
-    return getGridHTML({
-        srcFn: getAlbumCover,
-        hrefFn: album => to('localized.album', album.directory),
-        detailsFn: details && (album => strings('misc.albumGridDetails', {
-            tracks: strings.count.tracks(album.tracks.length, {unit: true}),
-            time: strings.count.duration(getTotalDuration(album.tracks))
-        })),
-        ...props
-    });
-}
-
-function getFlashGridHTML({
-    getFlashCover, getGridHTML, to,
-    ...props
-}) {
-    return getGridHTML({
-        srcFn: getFlashCover,
-        hrefFn: flash => to('localized.flash', flash.directory),
-        ...props
-    });
-}
-
 function writeSymlinks() {
     return progressPromiseAll('Writing site symlinks.', [
         link(path.join(__dirname, UTILITY_DIRECTORY), 'shared.utilityRoot'),
@@ -2115,72 +2068,6 @@ function writeSharedFilesAndPages({strings, wikiData}) {
     ].filter(Boolean));
 }
 
-function getRevealStringFromWarnings(warnings, {strings}) {
-    return strings('misc.contentWarnings', {warnings}) + `<br><span class="reveal-interaction">${strings('misc.contentWarnings.reveal')}</span>`
-}
-
-function getRevealStringFromTags(tags, {strings}) {
-    return tags && tags.some(tag => tag.isCW) && (
-        getRevealStringFromWarnings(strings.list.unit(tags.filter(tag => tag.isCW).map(tag => tag.name)), {strings}));
-}
-
-function generateCoverLink({
-    link, strings, to, wikiData,
-    src,
-    path,
-    alt,
-    tags = []
-}) {
-    const { wikiInfo } = wikiData;
-
-    if (!src && path) {
-        src = to(...path);
-    }
-
-    if (!src) {
-        throw new Error(`Expected src or path`);
-    }
-
-    return fixWS`
-        <div id="cover-art-container">
-            ${img({
-                src,
-                alt,
-                thumb: 'medium',
-                id: 'cover-art',
-                link: true,
-                square: true,
-                reveal: getRevealStringFromTags(tags, {strings})
-            })}
-            ${wikiInfo.features.artTagUI && tags.filter(tag => !tag.isCW).length && fixWS`
-                <p class="tags">
-                    ${strings('releaseInfo.artTags')}
-                    ${(tags
-                        .filter(tag => !tag.isCW)
-                        .map(link.tag)
-                        .join(',\n'))}
-                </p>
-            `}
-        </div>
-    `;
-}
-
-function getAlbumStylesheet(album, {to}) {
-    return [
-        album.wallpaperArtists && fixWS`
-            body::before {
-                background-image: url("${to('media.albumWallpaper', album.directory)}");
-                ${album.wallpaperStyle}
-            }
-        `,
-        album.bannerStyle && fixWS`
-            #banner img {
-                ${album.bannerStyle}
-            }
-        `
-    ].filter(Boolean).join('\n');
-}
-
 function generateRedirectPage(title, target, {strings}) {
     return fixWS`
         <!DOCTYPE html>
@@ -2204,203 +2091,6 @@ function generateRedirectPage(title, target, {strings}) {
     `;
 }
 
-function getArtistString(artists, {
-    iconifyURL, link, strings,
-    showIcons = false,
-    showContrib = false
-}) {
-    return strings.list.and(artists.map(({ who, what }) => {
-        const { urls, directory, name } = who;
-        return [
-            link.artist(who),
-            showContrib && what && `(${what})`,
-            showIcons && urls.length && `<span class="icons">(${
-                strings.list.unit(urls.map(url => iconifyURL(url, {strings})))
-            })</span>`
-        ].filter(Boolean).join(' ');
-    }));
-}
-
-function getFlashDirectory(flash) {
-    // const kebab = getKebabCase(flash.name.replace('[S] ', ''));
-    // return flash.page + (kebab ? '-' + kebab : '');
-    // return '' + flash.page;
-    return '' + flash.directory;
-}
-
-function getTagDirectory({name}) {
-    return getKebabCase(name);
-}
-
-function fancifyURL(url, {strings, album = false} = {}) {
-    const domain = new URL(url).hostname;
-    return fixWS`<a href="${url}" class="nowrap">${
-        domain.includes('bandcamp.com') ? strings('misc.external.bandcamp') :
-        [
-            'music.solatrux.com'
-        ].includes(domain) ? strings('misc.external.bandcamp.domain', {domain}) :
-        [
-            'types.pl'
-        ].includes(domain) ? strings('misc.external.mastodon.domain', {domain}) :
-        domain.includes('youtu') ? (album
-            ? (url.includes('list=')
-                ? strings('misc.external.youtube.playlist')
-                : strings('misc.external.youtube.fullAlbum'))
-            : strings('misc.external.youtube')) :
-        domain.includes('soundcloud') ? strings('misc.external.soundcloud') :
-        domain.includes('tumblr.com') ? strings('misc.external.tumblr') :
-        domain.includes('twitter.com') ? strings('misc.external.twitter') :
-        domain.includes('deviantart.com') ? strings('misc.external.deviantart') :
-        domain.includes('wikipedia.org') ? strings('misc.external.wikipedia') :
-        domain.includes('poetryfoundation.org') ? strings('misc.external.poetryFoundation') :
-        domain.includes('instagram.com') ? strings('misc.external.instagram') :
-        domain.includes('patreon.com') ? strings('misc.external.patreon') :
-        domain
-    }</a>`;
-}
-
-function fancifyFlashURL(url, flash, {strings}) {
-    const link = fancifyURL(url, {strings});
-    return `<span class="nowrap">${
-        url.includes('homestuck.com') ? (isNaN(Number(flash.page))
-            ? strings('misc.external.flash.homestuck.secret', {link})
-            : strings('misc.external.flash.homestuck.page', {link, page: flash.page})) :
-        url.includes('bgreco.net') ? strings('misc.external.flash.bgreco', {link}) :
-        url.includes('youtu') ? strings('misc.external.flash.youtube', {link}) :
-        link
-    }</span>`;
-}
-
-function iconifyURL(url, {strings, to}) {
-    const domain = new URL(url).hostname;
-    const [ id, msg ] = (
-        domain.includes('bandcamp.com') ? ['bandcamp', strings('misc.external.bandcamp')] :
-        (
-            domain.includes('music.solatrus.com')
-        ) ? ['bandcamp', strings('misc.external.bandcamp.domain', {domain})] :
-        (
-            domain.includes('types.pl')
-        ) ? ['mastodon', strings('misc.external.mastodon.domain', {domain})] :
-        domain.includes('youtu') ? ['youtube', strings('misc.external.youtube')] :
-        domain.includes('soundcloud') ? ['soundcloud', strings('misc.external.soundcloud')] :
-        domain.includes('tumblr.com') ? ['tumblr', strings('misc.external.tumblr')] :
-        domain.includes('twitter.com') ? ['twitter', strings('misc.external.twitter')] :
-        domain.includes('deviantart.com') ? ['deviantart', strings('misc.external.deviantart')] :
-        domain.includes('instagram.com') ? ['instagram', strings('misc.external.bandcamp')] :
-        ['globe', strings('misc.external.domain', {domain})]
-    );
-    return fixWS`<a href="${url}" class="icon"><svg><title>${msg}</title><use href="${to('shared.staticFile', `icons.svg#icon-${id}`)}"></use></svg></a>`;
-}
-
-function generateChronologyLinks(currentThing, {
-    contribKey,
-    getThings,
-    headingString,
-    link,
-    strings,
-    wikiData
-}) {
-    const { albumData } = wikiData;
-
-    const contributions = currentThing[contribKey];
-    if (!contributions) {
-        return '';
-    }
-
-    if (contributions.length > 8) {
-        return `<div class="chronology">${strings('misc.chronology.seeArtistPages')}</div>`;
-    }
-
-    return contributions.map(({ who: artist }) => {
-        const things = sortByDate(unique(getThings(artist)));
-        const releasedThings = things.filter(thing => {
-            const album = albumData.includes(thing) ? thing : thing.album;
-            return !(album && album.directory === UNRELEASED_TRACKS_DIRECTORY);
-        });
-        const index = releasedThings.indexOf(currentThing);
-
-        if (index === -1) return '';
-
-        // TODO: This can pro8a8ly 8e made to use generatePreviousNextLinks?
-        // We'd need to make generatePreviousNextLinks use toAnythingMan tho.
-        const previous = releasedThings[index - 1];
-        const next = releasedThings[index + 1];
-        const parts = [
-            previous && linkAnythingMan(previous, {
-                link, wikiData,
-                color: false,
-                text: strings('misc.nav.previous')
-            }),
-            next && linkAnythingMan(next, {
-                link, wikiData,
-                color: false,
-                text: strings('misc.nav.next')
-            })
-        ].filter(Boolean);
-
-        const stringOpts = {
-            index: strings.count.index(index + 1, {strings}),
-            artist: link.artist(artist)
-        };
-
-        return fixWS`
-            <div class="chronology">
-                <span class="heading">${strings(headingString, stringOpts)}</span>
-                ${parts.length && `<span class="buttons">(${parts.join(', ')})</span>`}
-            </div>
-        `;
-    }).filter(Boolean).join('\n');
-}
-
-function generateInfoGalleryLinks(currentThing, isGallery, {
-    link, strings,
-    linkKeyGallery,
-    linkKeyInfo
-}) {
-    return [
-        link[linkKeyInfo](currentThing, {
-            class: isGallery ? '' : 'current',
-            text: strings('misc.nav.info')
-        }),
-        link[linkKeyGallery](currentThing, {
-            class: isGallery ? 'current' : '',
-            text: strings('misc.nav.gallery')
-        })
-    ].join(', ');
-}
-
-function generatePreviousNextLinks(current, {
-    data,
-    link,
-    linkKey,
-    strings
-}) {
-    const linkFn = link[linkKey];
-
-    const index = data.indexOf(current);
-    const previous = data[index - 1];
-    const next = data[index + 1];
-
-    return [
-        previous && linkFn(previous, {
-            attributes: {
-                id: 'previous-button',
-                title: previous.name
-            },
-            text: strings('misc.nav.previous'),
-            color: false
-        }),
-        next && linkFn(next, {
-            attributes: {
-                id: 'next-button',
-                title: next.name
-            },
-            text: strings('misc.nav.next'),
-            color: false
-        })
-    ].filter(Boolean).join(', ');
-}
-
 // RIP toAnythingMan (previously getHrefOfAnythingMan), 2020-05-25<>2021-05-14.
 // ........Yet the function 8reathes life anew as linkAnythingMan! ::::)
 function linkAnythingMan(anythingMan, {link, wikiData, ...opts}) {
@@ -2412,11 +2102,6 @@ function linkAnythingMan(anythingMan, {link, wikiData, ...opts}) {
     )
 }
 
-function classes(...args) {
-    const values = args.filter(Boolean);
-    return `class="${values.join(' ')}"`;
-}
-
 async function processLanguageFile(file, defaultStrings = null) {
     let contents;
     try {
@@ -3255,6 +2940,11 @@ async function main() {
                 bound.link = withEntries(unbound_link, entries => entries
                     .map(([ key, fn ]) => [key, bindOpts(fn, {to})]));
 
+                bound.linkAnythingMan = bindOpts(linkAnythingMan, {
+                    link: bound.link,
+                    wikiData
+                });
+
                 bound.parseAttributes = bindOpts(parseAttributes, {
                     to
                 });
@@ -3291,6 +2981,10 @@ async function main() {
                     strings
                 });
 
+                bound.getLinkThemeString = getLinkThemeString;
+
+                bound.getThemeString = getThemeString;
+
                 bound.getArtistString = bindOpts(getArtistString, {
                     iconifyURL: bound.iconifyURL,
                     link: bound.link,
@@ -3311,12 +3005,14 @@ async function main() {
 
                 bound.generateChronologyLinks = bindOpts(generateChronologyLinks, {
                     link: bound.link,
+                    linkAnythingMan: bound.linkAnythingMan,
                     strings,
                     wikiData
                 });
 
                 bound.generateCoverLink = bindOpts(generateCoverLink, {
                     [bindOpts.bindIndex]: 0,
+                    img,
                     link: bound.link,
                     strings,
                     to,
@@ -3336,6 +3032,8 @@ async function main() {
 
                 bound.getGridHTML = bindOpts(getGridHTML, {
                     [bindOpts.bindIndex]: 0,
+                    getLinkThemeString,
+                    img,
                     strings
                 });
 
@@ -3354,6 +3052,14 @@ async function main() {
                     to
                 });
 
+                bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, {
+                    strings
+                });
+
+                bound.getRevealStringFromWarnings = bindOpts(getRevealStringFromWarnings, {
+                    strings
+                });
+
                 bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, {
                     to
                 });
diff --git a/src/util/colors.js b/src/util/colors.js
index 01c55024..3a7ce8f3 100644
--- a/src/util/colors.js
+++ b/src/util/colors.js
@@ -19,31 +19,3 @@ export function getColors(primary) {
 
     return {primary, dim, bg};
 }
-
-export function getLinkThemeString(color) {
-    if (!color) return '';
-
-    const { primary, dim } = getColors(color);
-    return `--primary-color: ${primary}; --dim-color: ${dim}`;
-}
-
-export function getThemeString(color, additionalVariables = []) {
-    if (!color) return '';
-
-    const { primary, dim, bg } = getColors(color);
-
-    const variables = [
-        `--primary-color: ${primary}`,
-        `--dim-color: ${dim}`,
-        `--bg-color: ${bg}`,
-        ...additionalVariables
-    ].filter(Boolean);
-
-    if (!variables.length) return '';
-
-    return (
-        `:root {\n` +
-        variables.map(line => `    ` + line + ';\n').join('') +
-        `}`
-    );
-}
diff --git a/src/util/link.js b/src/util/link.js
index 107b35ff..7ed5fd8e 100644
--- a/src/util/link.js
+++ b/src/util/link.js
@@ -10,7 +10,14 @@
 // gener8ting just a8out any link on the site.
 
 import * as html from './html.js'
-import { getLinkThemeString } from './colors.js'
+import { getColors } from './colors.js'
+
+export function getLinkThemeString(color) {
+    if (!color) return '';
+
+    const { primary, dim } = getColors(color);
+    return `--primary-color: ${primary}; --dim-color: ${dim}`;
+}
 
 const linkHelper = (hrefFn, {color = true, attr = null} = {}) =>
     (thing, {