« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/upd8.js
diff options
context:
space:
mode:
Diffstat (limited to 'upd8.js')
-rw-r--r--upd8.js1491
1 files changed, 928 insertions, 563 deletions
diff --git a/upd8.js b/upd8.js
index 439f6fb2..d86cc022 100644
--- a/upd8.js
+++ b/upd8.js
@@ -89,6 +89,8 @@ const readdir = util.promisify(fs.readdir);
 const readFile = util.promisify(fs.readFile);
 const writeFile = util.promisify(fs.writeFile);
 const access = util.promisify(fs.access);
+const symlink = util.promisify(fs.symlink);
+const unlink = util.promisify(fs.unlink);
 
 const {
     cacheOneArg,
@@ -101,68 +103,32 @@ const {
     th
 } = require('./upd8-util');
 
-const C = require('./common');
+const C = require('./common/common');
 
 // This can 8e changed if you want to output to some other directory. Just make
 // sure static files are copied into it too! (Which, ahem. Might 8e a todo.)
 // const C.SITE_DIRECTORY = '';
 
 const SITE_TITLE = 'Homestuck Music Wiki';
+const SITE_VERSION = 'autumnal polish haul';
+const SITE_RELEASE = '10 October 2020';
 
-const SITE_ABOUT = fixWS`
-    <p>Welcome to my fan-made Homestuck music wiki!</p>
-    <p><a href="https://www.homestuck.com/">Homestuck</a> has always been an incredible creative collaboration, and especially beloved by the community and critical in that collaboration is the webcomic and world's humongous soundtrack, comprising well over 500 tracks by dozens of musicians and artists. This wiki aims to be an interesting and useful resource for anyone interested in that music, as well as an archive for all things related.</p>
-    <p>Pertaining to the history of this site: it was originally made as a remake of Homestuck's official <a href="https://homestuck.bandcamp.com/">Bandcamp</a>, which saw its content particularly reduced on <a href="https://twitter.com/hamesatron/status/1187842783618297856">10/25/19</a>. This site aims to be a more reliable resource and reference: track art (conspicuously missing from the Bandcamp) is archived here, solo albums (among other missing albums, like [[album:Squiddles!]]) are all indexed in the one place, and URLs will always stay consistent. And of course, also included are links for listening on Bandcamp and other services.</p>
-    <p>The code for this website is open source (GPL-3.0), and can be explored or forked <a href="https://github.com/hsmusic/hsmusic.github.io/">here</a>. I don't actively keep track of issues or PRs raised there; if you want to get in touch with feature requests or comments on the code, my contact info is <a href="feedback/index.html">here</a>!</p>
-    <p><i>Resource &amp; Author Credits</i></p>
-    <ul>
-        <li>Florrie: that's me! I programmed most of the site, and put the whole thing together. <a href="feedback/index.html">Say hi</a>!</li>
-        <li><a href="https://homestuck.bandcamp.com/">Homestuck's Bandcamp</a>, the official host of Homestuck's music: I got almost all the official album listings and basic track info from here.</li>
-        <li>GiovanH's <a href="https://my.pcloud.com/publink/show?code=kZdJQ8kZNyIwh0Hn1ime6Ty7L2J87BE3E2ak">complete track art archive</a>: track art! A million thanks for putting this together and sharing this with me. (Prior to this, I used the <a href="https://web.archive.org/web/20190720035022/https://homestuck.bandcamp.com/music">Web Archive</a> to gather track art.)</li>
-        <li><a href="https://homestuck.net/music/references.html">NSND</a>: leitmotifs! Thanks to this site in combination with credits on the bandcamp and artists' own commentary, this wiki is a rather comprehensive resource for leitmotifs and other track references.</li>
-        <li><a href="https://www.bgreco.net/hsflash.html">bgreco.net (HQ Audio Flashes)</a>: thumbnail captures for the individual Flash animations! There were a couple captures missing that I took myself, but most Flash thumbnails are from here.</a></li>
-        <li>The <a href="https://homestuck-and-mspa-music.fandom.com/wiki/Homestuck_and_MSPA_Music_Wiki">Homestuck and MSPA Music Wiki</a> on Fandom: the inspiration for this wiki! I've wanted to make a more complete and explorable wiki ever since seeing it. The Fandom wiki has also been a very handy reference in putting this together, so much thanks to everyone who's worked on it!</li>
-        <li><a href="https://carrd.co/">carrd.co</a>: I stole your icons.svg file. It is mine now. :tobyfox_dog_sunglasses:</li>
-        <li>All organizers and contributors of the <a href="https://sollay-b.tumblr.com/post/188094230423/hello-a-couple-of-years-ago-allyssinian">Homestuck Vol. 5 Anthology</a> - community-made track art for [[album:Homestuck Vol. 5]]! All of this art is <i>excellent</i>. Each track credits its respective cover artist.</li>
-        <li>Likewise for the <a href="https://hsfanmusic.skaia.net/post/619761136023257089/unofficialmspafans-we-are-proud-to-announce-the">Beyond Canon Track Art Anthology</a> as well as <a href="https://alterniaart.tumblr.com/">Alternia/Bound</a>!</li>
-        <li>All comments on the site: I appreciate all feedback a lot! People have shared a ton of ideas and suggestions with me, and I <i>cannot</i> emphasize enough how motivating it is to share a project with like-minded folx interested in making it better with you.</li>
-    </ul>
-    <p><i>Feature Acknowledgements</i></p>
-    <ul>
-        <li><b>Thank you,</b> GiovanH, for linking me to a resource for higher quality cover art, and bringing to my attention the fact that clicking a cover art on Bandcamp to zoom in will often reveal a higher quality image.</li>
-        <li>cosmogonicalAuthor, for a variety of feature requests and comments! In particular: improving way the track list on author pages is sorted; expanding the introduction; expanding the introduction message to the website; and linking bonus art for Homestuck Vol. 5 - plus a few other good suggestions I haven't gotten to yet. Thanks!</li>
-        <li>Monckat, for suggesting the album Strife 2 before I'd begun adding fandom-created albums and unofficial releases to this wiki, and for working with an emailer to reupload the original cover art for [[track:the-thirteenth-hour]].</li>
-        <li>Kidpen, for suggesting the "Flashes that feature this track" feature.</li>
-        <li>an emailer, for suggesting the "Random track" feature.</li>
-        <li>foreverFlumoxed, for pointing out that [[flash:338]] contains reference to [[JOHN DO THE WINDY THING]] (this reminded me to add all the unreleased Flash tracks to the Unreleased Tracks album!), for recommending the restructure to [[album:Unreleased Tracks]], and for going to the massive effort of checking every track page and pointing out a bunch of missing cover arts and title typos!</li>
-        <li>Makin, for various initial help in data collection (especially commentary) and lifting the site off the ground by pinning it to the top of the /r/homestuck subreddit for a while, and for linking me the independent release of <a href="https://jamesdever.bandcamp.com/album/sburb">Sburb</a>.</li>
-        <li>an emailer, for sending a crop of the YT thumbnail art for [[After the Sun]] (plus the SoundCloud link for that track), for reporting the "Random" buttons being broken, and for linking a bunch of resources and various official uploads of tracks and albums.</li>
-        <li>Thanks for pointing out typos, errors in reference lists, and out of date details: cookiefonster, foreverFlummoxed, and an emailer.</li>
-    </ul>
-`;
-
-const SITE_CHANGELOG = fs.readFileSync('changelog.html').toString().trim(); // fight me bro
-
-const SITE_FEEDBACK = fixWS`
-    <p><strong>Feature requests? Noticed any errors?</strong> Please let me know! I appreciate feedback a lot, and always want to make this site better.</p>
-    <p>The best place to talk about this site is on its <a href="https://forum.homestuck.xyz/viewtopic.php?f=7&t=151">HomestuckXYZ forum thread</a>.</p>
-    <p>Or, if forums aren't really the thing for you, I've got an email too: towerofnix at gmail dot beans. (You know the domain.)</p>
-    <p>I used to have a Twitter account, but Twitter is bad and poofing from it was probably my greatest decision.</p>
-    <p>Thank you for sharing your feedback!</p>
-`;
-
-const SITE_JS_DISABLED = fixWS`
-    <p>Sorry, that link won't work unless you're running a web browser that supports relatively modern JavaScript.</p>
-    <p>Please press the back button to get where you were, or <a href="index.html">head back to the index</a>.</p>
-`;
+function readDataFile(file) {
+    // fight me bro
+    return fs.readFileSync(path.join(C.DATA_DIRECTORY, file)).toString().trim();
+}
+
+const SITE_ABOUT = readDataFile('about.html');
+const SITE_CHANGELOG = readDataFile('changelog.html');
+const SITE_FEEDBACK = readDataFile('feedback.html');
+const SITE_JS_DISABLED = readDataFile('js-disabled.html');
 
 // Might ena8le this later... we'll see! Eventually. May8e.
 const ENABLE_ARTIST_AVATARS = false;
 const ARTIST_AVATAR_DIRECTORY = 'artist-avatar';
 
-const ALBUM_DATA_FILE = 'album.txt';    // /album/*/$.txt
-const ARTIST_DATA_FILE = 'artists.txt'; // /$.txt
-const FLASH_DATA_FILE = 'flashes.txt';  // /$.txt
+const ARTIST_DATA_FILE = 'artists.txt';
+const FLASH_DATA_FILE = 'flashes.txt';
 
 const CSS_FILE = 'site.css';
 
@@ -192,33 +158,8 @@ let justEverythingSortedByArtDateMan;
 // missing track files (or track files which are not linked to al8ums). All a
 // 8unch of stuff that's a pain to deal with for no apparent 8enefit.
 async function findAlbumDataFiles() {
-    // Promises suck. This could pro8a8ly 8e written with async/await and an
-    // ordinary for loop, 8ut I'm using promises 8ecause they let all the
-    // folders get read simultaneously.
-    // ...Actually screw it, let's use async/await AND promises.
-    /*
-    return readdir(C.ALBUM_DIRECTORY)
-        .then(albums => Promise.all(albums
-            .map(album => readdir(path.join(C.ALBUM_DIRECTORY, album))
-                .then(files => files.includes(ALBUM_DATA_FILE) ? path.join(C.ALBUM_DIRECTORY, album, ALBUM_DATA_FILE) : null))))
-        .then(paths => paths.filter(Boolean));
-    */
-
-    const albums = await readdir(C.ALBUM_DIRECTORY);
-
-    const paths = await progressPromiseAll(`Searching for album files.`, albums.map(async album => {
-        // Argua8ly terri8le/am8iguous varia8le naming. Too 8ad!
-        const albumDirectory = path.join(C.ALBUM_DIRECTORY, album);
-        const files = await readdir(albumDirectory);
-        if (files.includes(ALBUM_DATA_FILE)) {
-            return path.join(albumDirectory, ALBUM_DATA_FILE);
-        }
-        // The old code returns null if the data file isn't present, 8ut that's
-        // not actually necessary. We just need some falsey value, and the
-        // implied undefined when you don't explicitly return anything works.
-    }));
-
-    return paths.filter(Boolean);
+    return (await readdir(path.join(C.DATA_DIRECTORY, C.DATA_ALBUM_DIRECTORY)))
+        .map(albumFile => path.join(C.DATA_DIRECTORY, C.DATA_ALBUM_DIRECTORY, albumFile));
 }
 
 function* getSections(lines) {
@@ -323,7 +264,7 @@ function transformInline(text) {
             const album = getLinkedAlbum(ref);
             if (album) {
                 return fixWS`
-                    <a href="${C.ALBUM_DIRECTORY}/${album.directory}/index.html" style="${getThemeString(album)}">${album.name}</a>
+                    <a href="${C.ALBUM_DIRECTORY}/${album.directory}/" style="${getThemeString(album)}">${album.name}</a>
                 `;
             } else {
                 console.warn(`\x1b[33mThe linked album ${match} does not exist!\x1b[0m`);
@@ -332,7 +273,7 @@ function transformInline(text) {
         } else if (category === 'artist:') {
             const artist = getLinkedArtist(ref);
             if (artist) {
-                return `<a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artist.name)}/index.html">${artist.name}</a>`;
+                return `<a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artist.name)}/">${artist.name}</a>`;
             } else {
                 console.warn(`\x1b[33mThe linked artist ${artist} does not exist!\x1b[0m`);
                 return ref;
@@ -344,7 +285,7 @@ function transformInline(text) {
                 const nextCharacter = text[offset + 1];
                 const lastCharacter = name[name.length - 1];
                 if (
-                    ![' ', '\n'].includes(nextCharacter) &&
+                    ![' ', '\n', '<'].includes(nextCharacter) &&
                     lastCharacter === '.'
                 ) {
                     name = name.slice(0, -1);
@@ -358,7 +299,7 @@ function transformInline(text) {
             const track = getLinkedTrack(ref);
             if (track) {
                 return fixWS`
-                    <a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a>
+                    <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
                 `;
             } else {
                 console.warn(`\x1b[33mThe linked track ${match} does not exist!\x1b[0m`);
@@ -374,7 +315,7 @@ function transformInline(text) {
                     name = track.name;
                 }
                 return fixWS`
-                    <a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${name}</a>
+                    <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${name}</a>
                 `;
             } else {
                 console.warn(`\x1b[33mThe linked track ${match} does not exist!\x1b[0m`);
@@ -384,6 +325,57 @@ function transformInline(text) {
     });
 }
 
+function parseAttributes(string) {
+    const attributes = Object.create(null);
+    const skipWhitespace = i => {
+        const ws = /\s/;
+        if (ws.test(string[i])) {
+            const match = string.slice(i).match(/[^\s]/);
+            if (match) {
+                return i + match.index;
+            } else {
+                return string.length;
+            }
+        } else {
+            return i;
+        }
+    };
+
+    for (let i = 0; i < string.length;) {
+        i = skipWhitespace(i);
+        const aStart = i;
+        const aEnd = i + string.slice(i).match(/[\s=]|$/).index;
+        const attribute = string.slice(aStart, aEnd);
+        i = skipWhitespace(aEnd);
+        if (string[i] === '=') {
+            i = skipWhitespace(i + 1);
+            let end, endOffset;
+            if (string[i] === '"' || string[i] === "'") {
+                end = string[i];
+                endOffset = 1;
+                i++;
+            } else {
+                end = '\\s';
+                endOffset = 0;
+            }
+            const vStart = i;
+            const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index;
+            const value = string.slice(vStart, vEnd);
+            i = vEnd + endOffset;
+            attributes[attribute] = value;
+        } else {
+            attributes[attribute] = attribute;
+        }
+    }
+    return Object.fromEntries(Object.entries(attributes).map(([ key, val ]) => [
+        key,
+        val === 'true' ? true :
+        val === 'false' ? false :
+        val === key ? true :
+        val
+    ]));
+}
+
 function transformMultiline(text, treatAsDocument=false) {
     // Heck yes, HTML magics.
 
@@ -397,7 +389,11 @@ function transformMultiline(text, treatAsDocument=false) {
 
     let inList = false;
     for (let line of text.split(/\r|\n|\r\n/)) {
-        line = line.replace(/<img src="(.*?)">/g, '<a href="$1">$&</a>');
+        line = line.replace(/<img (.*?)>/g, (match, attributes) => img({
+            lazy: true,
+            link: true,
+            ...parseAttributes(attributes)
+        }));
         if (line.startsWith('- ')) {
             if (!inList) {
                 outLines.push('<ul>');
@@ -546,7 +542,7 @@ async function processAlbumDataFile(file) {
         const groupName = getBasicField(section, 'Group');
         if (groupName) {
             group = groupName;
-            groupColor = getBasicField(section, 'FG');
+            groupColor = getBasicField(section, 'FG') || album.color;
             album.usesGroups = true;
             continue;
         }
@@ -792,6 +788,8 @@ function getTotalDuration(tracks) {
     return tracks.reduce((duration, track) => duration + track.duration, 0);
 }
 
+const stringifyIndent = 0;
+
 function stringifyAlbumData() {
     return JSON.stringify(albumData, (key, value) => {
         if (['album', 'commentary'].includes(key)) {
@@ -799,7 +797,7 @@ function stringifyAlbumData() {
         }
 
         return value;
-    }, 1);
+    }, stringifyIndent);
 }
 
 function stringifyFlashData() {
@@ -809,16 +807,16 @@ function stringifyFlashData() {
         }
 
         return value;
-    }, 1);
+    }, stringifyIndent);
 }
 
 function stringifyArtistData() {
-    return JSON.stringify(artistData, null, 1);
+    return JSON.stringify(artistData, null, stringifyIndent);
 }
 
 // 8asic function for writing any site page. Handles all the 8asename,
 // directory, and site-template shenanigans!
-async function writePage(directoryParts, titleOrHead, body) {
+async function OLD_writePage(directoryParts, titleOrHead, body) {
     const directory = path.join(C.SITE_DIRECTORY, ...directoryParts);
     await mkdirp(directory);
     // This is sort of hard-coded, i.e. we don't do path.join(C.ROOT_DIRECTORY).
@@ -837,7 +835,7 @@ async function writePage(directoryParts, titleOrHead, body) {
                     // directory !== C.SITE_DIRECTORY &&
                     // directory !== '.' &&
                     // `<base href="${path.relative(directory, C.SITE_DIRECTORY)}">`,
-                    `<link rel="stylesheet" href="${CSS_FILE}">`,
+                    `<link rel="stylesheet" href="${C.STATIC_DIRECTORY}/${CSS_FILE}">`,
                     // Apply JavaScript directly to the HTML <head>.
                     // (This is unfortun8, 8ut necessary, 8ecause the entire
                     // <body> tag is passed to this function; if we wanted to
@@ -845,9 +843,12 @@ async function writePage(directoryParts, titleOrHead, body) {
                     // string, well........ we don't want to go there.
                     // To deal with this, we use the "defer" property, which
                     // means the code only runs once the body has 8een loaded.)
-                    `<script src="common.js"></script>`,
+                    `<script src="${C.COMMON_DIRECTORY}/common.js"></script>`,
                     `<script src="data.js"></script>`,
-                    `<script defer src="client.js"></script>`
+                    `<script defer src="${C.STATIC_DIRECTORY}/lazy-show.js"></script>`,
+                    `<script defer src="${C.STATIC_DIRECTORY}/lazy-loading.js"></script>`,
+                    `<script defer src="${C.STATIC_DIRECTORY}/lazy-fallback.js"></script>`,
+                    `<script defer src="${C.STATIC_DIRECTORY}/client.js"></script>`
                 ].filter(Boolean).join('\n')}
             </head>
             ${body}
@@ -855,69 +856,383 @@ async function writePage(directoryParts, titleOrHead, body) {
     `));
 }
 
+function escapeAttributeValue(value) {
+    return value.toString().replace(/"/g, '&quot;');
+}
+
+function attributes(attribs) {
+    return Object.entries(attribs)
+        .filter(([ key, val ]) => val !== '')
+        .map(([ key, val ]) => `${key}="${escapeAttributeValue(val)}"`)
+        .join(' ');
+}
+
+function img({
+    src = '',
+    alt = '',
+    id = '',
+    width = '',
+    height = '',
+    link = false,
+    lazy = false,
+    square = false
+}) {
+    const willSquare = square;
+    const willLink = typeof link === 'string' || link;
+
+    const imgAttributes = attributes({
+        id: link ? '' : id,
+        alt,
+        width,
+        height
+    });
+
+    const nonlazyHTML = wrap(`<img src="${src}" ${imgAttributes}>`);
+    const lazyHTML = lazy && wrap(`<img class="lazy" data-original="${src}" ${imgAttributes}>`, true);
+
+    if (lazy) {
+        return fixWS`
+            <noscript>${nonlazyHTML}</noscript>
+            ${lazyHTML}
+        `;
+    } else {
+        return nonlazyHTML;
+    }
+
+    function wrap(html, hide = false) {
+        if (willSquare) {
+            html = fixWS`<div ${classes('square', hide && !willLink && 'js-hide')}><div class="square-content">${html}</div></div>`;
+        }
+
+        if (willLink) {
+            html = `<a ${classes('box', hide && 'js-hide')} ${attributes({
+                id,
+                href: typeof link === 'string' ? link : src
+            })}>${html}</a>`;
+        }
+
+        return html;
+    }
+}
+
+async function writePage(directoryParts, {
+    title = '',
+    meta = {},
+
+    body = {
+        style: ''
+    },
+
+    main = {
+        classes: [],
+        content: ''
+    },
+
+    sidebar = {
+        collapse: true,
+        classes: [],
+        content: ''
+    },
+
+    nav = {
+        links: [],
+        classes: [],
+        content: ''
+    }
+}) {
+    const directory = path.join(C.SITE_DIRECTORY, ...directoryParts);
+    const file = path.join(directory, 'index.html');
+    const href = path.join(...directoryParts, 'index.html');
+
+    const mainHTML = main.content && fixWS`
+        <main id="content" ${classes(...main.classes || [])}>
+            ${main.content}
+        </main>
+    `;
+
+    const {
+        collapse = true,
+        wide = false
+    } = sidebar;
+
+    const sidebarHTML = sidebar.content && fixWS`
+        <div id="sidebar" ${classes(
+            wide && 'wide',
+            !collapse && 'no-hide',
+            ...sidebar.classes || []
+        )}>
+            ${sidebar.content}
+        </div>
+    `;
+
+    if (nav.simple) {
+        nav.links = [
+            ['./', 'Home'],
+            [href, title]
+        ]
+    }
+
+    const navContentHTML = [
+        nav.links && fixWS`
+            <h2 class="highlight-last-link">
+                ${nav.links
+                    .map(([ href, title ], i) => (href
+                        ? (i > 0 ? '/ ' : '') + `<a href="${href}">${title}</a>`
+                        : `<span>${title}</span>`))
+                    .join('\n')}
+            </h2>
+        `,
+        nav.content
+    ].filter(Boolean).join('\n');
+
+    const navHTML = navContentHTML && fixWS`
+        <nav id="header" ${classes(...nav.classes || [])}>
+            ${navContentHTML}
+        </nav>
+    `;
+
+    const layoutHTML = [
+        navHTML,
+        sidebar.content ? fixWS`
+            <div ${classes('columns', !collapse && 'vertical-when-thin')}>
+                ${sidebarHTML}
+                ${mainHTML}
+            </div>
+        ` : mainHTML
+    ].filter(Boolean).join('\n');
+
+    await mkdirp(directory);
+    await writeFile(file, rebaseURLs(directory, fixWS`
+        <!DOCTYPE html>
+        <html data-rebase="${path.relative(directory, C.SITE_DIRECTORY)}">
+            <head>
+                <title>${title}</title>
+                <meta charset="utf-8">
+                <meta name="viewport" content="width=device-width, initial-scale=1">
+                ${Object.entries(meta).map(([ key, value ]) => `<meta ${key}="${escapeAttributeValue(value)}">`).join('\n')}
+                <link rel="stylesheet" href="${C.STATIC_DIRECTORY}/site.css">
+            </head>
+            <body ${attributes({style: body.style})}>
+                ${layoutHTML}
+                <script src="${C.STATIC_DIRECTORY}/lazy-show.js"></script>
+                <script src="${C.STATIC_DIRECTORY}/lazy-loading.js"></script>
+                <script src="${C.STATIC_DIRECTORY}/lazy-fallback.js"></script>
+                <script src="${C.COMMON_DIRECTORY}/common.js"></script>
+                <script src="data.js"></script>
+                <script src="${C.STATIC_DIRECTORY}/client.js"></script>
+            </body>
+        </html>
+    `));
+}
+
+function getGridHTML({
+    entries,
+    srcFn,
+    hrefFn,
+    altFn,
+    lazy = true
+}) {
+    return entries.map(({ large, item }, i) => fixWS`
+        <a ${classes('grid-item', 'box', large && 'large-grid-item')} href="${hrefFn(item)}" style="${getThemeString(item)}">
+            ${img({
+                src: srcFn(item),
+                alt: altFn(item),
+                lazy: (typeof lazy === 'number' ? i >= lazy : lazy),
+                square: true
+            })}
+            <span>${item.name}</span>
+        </a>
+    `).join('\n');
+}
+
+function getAlbumGridHTML(props) {
+    return getGridHTML({
+        srcFn: getAlbumCover,
+        hrefFn: album => `${C.ALBUM_DIRECTORY}/${album.directory}/`,
+        altFn: () => 'album cover',
+        ...props
+    });
+}
+
+function getAlbumGridHTML(props) {
+    return getGridHTML({
+        srcFn: getAlbumCover,
+        hrefFn: album => `${C.ALBUM_DIRECTORY}/${album.directory}/`,
+        altFn: () => 'album cover',
+        ...props
+    });
+}
+
+function getFlashGridHTML(props) {
+    return getGridHTML({
+        srcFn: getFlashCover,
+        hrefFn: flash => `${C.FLASH_DIRECTORY}/${flash.directory}/`,
+        altFn: () => 'flash art',
+        ...props
+    });
+}
+
+function getNewReleases(numReleases) {
+    const latestFirst = albumData.slice().reverse();
+
+    // TODO: Major fan albums
+    const majorReleases = [];
+    majorReleases.push(latestFirst.find(album => album.isOfficial));
+
+    const otherReleases = latestFirst
+        .filter(album => !majorReleases.includes(album))
+        .slice(0, numReleases - majorReleases.length);
+
+    return [
+        ...majorReleases.map(album => ({large: true, item: album})),
+        ...otherReleases.map(album => ({large: false, item: album}))
+    ];
+}
+
+function writeSymlinks() {
+    return progressPromiseAll('Building site symlinks.', [
+        link(C.COMMON_DIRECTORY),
+        link(C.STATIC_DIRECTORY),
+        link(C.MEDIA_DIRECTORY)
+    ]);
+
+    async function link(directory) {
+        const file = path.join(C.SITE_DIRECTORY, directory);
+        try {
+            await unlink(file);
+        } catch (error) {
+            if (error.code !== 'ENOENT') {
+                throw error;
+            }
+        }
+        await symlink(path.join('..', directory), file);
+    }
+}
+
 function writeMiscellaneousPages() {
     return progressPromiseAll('Writing miscellaneous pages.', [
-        writePage([], fixWS`
-            <title>${SITE_TITLE}</title>
-            <meta name="description" content="Expansive resource for anyone interested in fan- and official music alike; an archive for all things related.">
-        `, fixWS`
-            <body id="top-index">
-                <div id="content">
+        writePage([], {
+            title: SITE_TITLE,
+            meta: {
+                description: "Expansive resource for anyone interested in fan-made and official Homestuck music alike; an archive for all things related."
+            },
+            main: {
+                classes: ['top-index'],
+                content: fixWS`
                     <h1>${SITE_TITLE}</h1>
-                    <div id="intro-menu">
-                        <p>Explore the site!</p>
-                        <a href="${C.LISTING_DIRECTORY}/index.html">Listings</a>
-                        <a href="${C.FLASH_DIRECTORY}/index.html">Flashes &amp; Games</a>
-                        <a href="${C.ABOUT_DIRECTORY}/index.html">About &amp; Credits</a>
-                        <a href="${C.FEEDBACK_DIRECTORY}/index.html">Feedback &amp; Suggestions</a>
-                        <a href="${C.CHANGELOG_DIRECTORY}/index.html">Changelog</a>
-                        <p>...or choose an album:</p>
-                    </div>
-                    <h2>Beyond Canon</h2>
-                    <h3>The future of Homestuck music, today.<br>Albums by the Homestuck^2 Music Team. 2020+.</h2>
+                    <h2>New Releases</h2>
                     <div class="grid-listing">
-                        ${albumData.filter(album => album.isBeyond).reverse().map(album => fixWS`
-                            <a class="grid-item" href="${C.ALBUM_DIRECTORY}/${album.directory}/index.html" style="${getThemeString(album)}">
-                                <img src="${getAlbumCover(album)}" alt="cover art">
-                                <span>${album.name}</span>
-                            </a>
-                        `).join('\n')}
+                        ${getAlbumGridHTML({
+                            entries: getNewReleases(4),
+                            lazy: false
+                        })}
                     </div>
                     <h2>Fandom</h2>
-                    <h3>A look into Homestuck's world of music and art created&mdash;and organized&mdash;by fans.<br>The beginning of time, through the end.</h3>
                     <div class="grid-listing">
-                        ${albumData.filter(album => album.isFanon).reverse().map(album => fixWS`
-                            <a class="grid-item" href="${C.ALBUM_DIRECTORY}/${album.directory}/index.html" style="${getThemeString(album)}">
-                                <img src="${getAlbumCover(album)}" alt="cover art">
-                                <span>${album.name}</span>
-                            </a>
-                        `).join('\n')}
-                        <a class="grid-item" href="${C.FEEDBACK_DIRECTORY}/index.html" style="--fg-color: #ffffff">...and more to be added at your request</a>
+                        ${getAlbumGridHTML({
+                            entries: (albumData
+                                .filter(album => album.isFanon)
+                                .reverse()
+                                .slice(0, 6)
+                                .concat([albumData.find(album => album.directory === C.UNRELEASED_TRACKS_DIRECTORY)])
+                                .map(album => ({item: album}))),
+                            lazy: true
+                        })}
+                        <div class="grid-actions">
+                            <a class="box grid-item" href="albums/fandom/" style="--fg-color: #ffffff">Explore Fandom!</a>
+                            <a class="box grid-item" href="${C.FEEDBACK_DIRECTORY}/" style="--fg-color: #ffffff">Request an album!</a>
+                        </div>
                     </div>
                     <h2>Official</h2>
-                    <h3>The original discography: a replica of the Homestuck Bandcamp prior to the enmergening.<br>Albums organized by What Pumpkin. 2009&ndash;2019.</h3>
                     <div class="grid-listing">
-                        ${albumData.filter(album => album.isCanon).reverse().map(album => fixWS`
-                            <a class="grid-item" href="${C.ALBUM_DIRECTORY}/${album.directory}/index.html" style="${getThemeString(album)}">
-                                <img src="${getAlbumCover(album)}" alt="cover art">
-                                <span>${album.name}</span>
-                            </a>
-                        `).join('\n')}
+                        ${getAlbumGridHTML({
+                            entries: (albumData
+                                .filter(album => album.isOfficial)
+                                .reverse()
+                                .slice(0, 11)
+                                .map(album => ({item: album}))),
+                            lazy: true
+                        })}
+                        <div class="grid-actions">
+                            <a class="box grid-item" href="albums/official/" style="--fg-color: #ffffff">Explore Official!</a>
+                        </div>
                     </div>
-                </div>
-            </body>
-        `),
-        writePage([C.FLASH_DIRECTORY], `Flashes & Games`, fixWS`
-            <body id="top-index">
-                <div id="content">
-                    <h1>Flashes &amp; Games</h1>
-                    <div id="intro-menu">
-                        <a href="index.html">Home</a>
-                        <a href="${C.LISTING_DIRECTORY}/index.html">Listings</a>
-                        <a href="${C.ABOUT_DIRECTORY}/index.html">About &amp; Credits</a>
-                        <a href="${C.FEEDBACK_DIRECTORY}/index.html">Feedback &amp; Suggestions</a>
-                        <a href="${C.CHANGELOG_DIRECTORY}/index.html">Changelog</a>
+                `
+            },
+            sidebar: {
+                collapse: false,
+                wide: true,
+                content: fixWS`
+                    <h1>Get involved!</h1>
+                    <ul>
+                        <li><a href="${C.FEEDBACK_DIRECTORY}/">Request features or send feedback!</a></li>
+                        <li><a href="donate/">Donate????</a></li>
+                    </ul>
+                    <hr>
+                    <h1>News</h1>
+                    <p>Todo.</p>
+                `
+            },
+            nav: {
+                content: fixWS`
+                    <h2 class="dot-between-spans">
+                        <span><a class="current" href="./">Home</a></span>
+                        <span><a href="${C.LISTING_DIRECTORY}/">Listings</a></span>
+                        <span><a href="${C.FLASH_DIRECTORY}/">Flashes &amp; Games</a></span>
+                        <span><a href="${C.ABOUT_DIRECTORY}/">About &amp; Credits</a></span>
+                        <span><a href="${C.FEEDBACK_DIRECTORY}/">Feedback &amp; Suggestions</a></span>
+                        <span><a href="${C.CHANGELOG_DIRECTORY}/">Changelog</a> (${SITE_RELEASE}: ${SITE_VERSION})</span>
+                    </h2>
+                `
+            }
+        }),
+
+        writePage(['albums', 'fandom'], {
+            title: `Albums - Fandom`,
+            main: {
+                classes: ['top-index'],
+                content: fixWS`
+                    <h1>Albums - Fandom</h1>
+                    <div class="grid-listing">
+                        ${getAlbumGridHTML({
+                            entries: (albumData
+                                .filter(album => album.isFanon)
+                                .reverse()
+                                .map(album => ({item: album}))),
+                            lazy: 4
+                        })}
+                    </div>
+                `
+            },
+            nav: {simple: true}
+        }),
+
+        writePage(['albums', 'official'], {
+            title: `Albums - Official`,
+            main: {
+                classes: ['top-index'],
+                content: fixWS`
+                    <h1>Albums - Official</h1>
+                    <div class="grid-listing">
+                        ${getAlbumGridHTML({
+                            entries: (albumData
+                                .filter(album => album.isOfficial)
+                                .reverse()
+                                .map(album => ({item: album}))),
+                            lazy: 4
+                        })}
                     </div>
+                `
+            },
+            nav: {simple: true}
+        }),
+
+        writePage([C.FLASH_DIRECTORY], {
+            title: `Flashes & Games`,
+            main: {
+                classes: ['flash-index'],
+                content: fixWS`
+                    <h1>Flashes &amp; Games</h1>
                     <div class="long-content">
                         <p>Also check out:</p>
                         <ul>
@@ -925,61 +1240,73 @@ function writeMiscellaneousPages() {
                             <li>bgreco.net's <a href="https://www.bgreco.net/hsflash.html">Homestuck HQ Audio Flashes</a>: an index of all HS Flash animations with Bandcamp-quality audio built in. (Also the source for many thumbnails below!)</li>
                         </ul>
                     </div>
-                    <div class="grid-listing">
-                        ${flashData.map(flash => flash.act8r8k ? fixWS`
-                            <h2 style="${getThemeString(flash)}"><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act === flash.act))}/index.html">${flash.act}</a></h2>
-                        ` : fixWS`
-                            <a class="grid-item" href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flash)}/index.html" style="${getThemeString(flash)}">
-                                <img src="${getFlashCover(flash)}" alt="cover art">
-                                <span>${flash.name}</span>
-                            </a>
-                        `).join('\n')}
-                    </div>
-                </div>
-            </body>
-        `),
-        writePage([C.ABOUT_DIRECTORY], 'About &amp; Credits', fixWS`
-            <body>
-                <div id="content">
+                    ${flashData.filter(flash => flash.act8r8k).map((act, i) => fixWS`
+                        <h2 style="${getThemeString(act)}"><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act === act.act))}/">${act.act}</a></h2>
+                        <div class="grid-listing">
+                            ${getFlashGridHTML({
+                                entries: (flashData
+                                    .filter(flash => !flash.act8r8k && flash.act === act.act)
+                                    .map(flash => ({item: flash}))),
+                                lazy: i === 0 ? 4 : true
+                            })}
+                        </div>
+                    `).join('\n')}
+                `
+            },
+            nav: {simple: true}
+        }),
+
+        writePage([C.ABOUT_DIRECTORY], {
+            title: `About &amp; Credits`,
+            main: {
+                content: fixWS`
                     <div class="long-content">
                         <h1>${SITE_TITLE}</h1>
-                        <p><a href="index.html">(Home)</a></p>
                         ${transformMultiline(SITE_ABOUT, true)}
                     </div>
-                </div>
-            </body>
-        `),
-        writePage([C.CHANGELOG_DIRECTORY], `Changelog`, fixWS`
-            <body>
-                <div id="content">
+                `
+            },
+            nav: {simple: true}
+        }),
+
+        writePage([C.CHANGELOG_DIRECTORY], {
+            title: `Changelog`,
+            main: {
+                content: fixWS`
                     <div class="long-content">
                         <h1>Changelog</h1>
-                        <p><a href="index.html">(Home)</a></p>
                         ${transformMultiline(SITE_CHANGELOG, true)}
                     </div>
-                </div>
-            </body>
-        `),
-        writePage([C.FEEDBACK_DIRECTORY], 'Feedback &amp; Suggestions!', fixWS`
-            <body>
-                <div id="content">
+                `
+            },
+            nav: {simple: true}
+        }),
+
+        writePage([C.FEEDBACK_DIRECTORY], {
+            title: `Feedback &amp; Suggestions!`,
+            main: {
+                content: fixWS`
                     <div class="long-content">
                         <h1>Feedback &amp; Suggestions!</h1>
-                        <p><a href="index.html">(Home)</a></p>
                         ${SITE_FEEDBACK}
                     </div>
-                </div>
-            </body>
-        `),
-        writePage([C.JS_DISABLED_DIRECTORY], 'JavaScript Disabled', fixWS`
-            <body>
-                <div id="content">
+                `
+            },
+            nav: {simple: true}
+        }),
+
+        writePage([C.JS_DISABLED_DIRECTORY], {
+            title: 'JavaScript Disabled',
+            main: {
+                content: fixWS`
                     <h1>JavaScript Disabled (or out of date)</h1>
                     ${SITE_JS_DISABLED}
-                </div>
-            </body>
-        `),
-        writeFile('data.js', fixWS`
+                `
+            },
+            nav: {simple: true}
+        }),
+
+        writeFile(path.join(C.SITE_DIRECTORY, 'data.js'), fixWS`
             // Yo, this file is gener8ted. Don't mess around with it!
             window.albumData = ${stringifyAlbumData()};
             window.flashData = ${stringifyFlashData()};
@@ -1001,162 +1328,197 @@ async function writeAlbumPage(album) {
     const trackToListItem = track => fixWS`
         <li style="${getThemeString(track)}">
             (${getDurationString(track.duration)})
-            <a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html">${track.name}</a>
+            <a href="${C.TRACK_DIRECTORY}/${track.directory}/">${track.name}</a>
             ${track.artists !== album.artists && fixWS`
                 <span class="by">by ${getArtistString(track.artists)}</span>
             ` || `<!-- (here: Track-specific musician credits) -->`}
         </li>
     `;
     const listTag = getAlbumListTag(album);
-    await writePage([C.ALBUM_DIRECTORY, album.directory], album.name, fixWS`
-        <body style="${getThemeString(album)}; --album-directory: ${album.directory}">
-            <div id="header">
-                ${generateHeaderForAlbum(album)}
-            </div>
-            <div class="columns">
-                <div id="sidebar">
-                    ${generateSidebarForAlbum(album)}
-                </div>
-                <div id="content">
-                    <a id="cover-art" href="${getAlbumCover(album)}"><img src="${getAlbumCover(album)}" alt="cover art"></a>
-                    <h1>${album.name}</h1>
-                    <p>
-                        ${album.artists && `By ${getArtistString(album.artists, true)}.<br>` || `<!-- (here: Full-album musician credits) -->`}
-                        ${album.coverArtists &&  `Cover art by ${getArtistString(album.coverArtists, true)}.<br>` || `<!-- (here: Cover art credits) -->`}
-                        Released ${getDateString(album)}.
-                        ${+album.artDate !== +album.date && `<br>Art released ${getDateString({date: album.artDate})}.` || `<!-- (here: Cover art release date) -->`}
-                        <br>Duration: ~${getDurationString(getTotalDuration(album.tracks))}.</p>
-                    </p>
-                    ${album.urls.length && `<p>Listen on ${joinNoOxford(album.urls.map(url => fancifyURL(url, {album: true})), 'or')}.</p>` || `<!-- (here: Listen on...) -->`}
-                    ${album.usesGroups ? fixWS`
-                        <p>This album listing is divided into groups:</p>
-                        <dl class="album-group-list">
-                            ${album.tracks.flatMap((track, i, arr) => [
-                                (i > 0 && track.group !== arr[i - 1].group) && `</${listTag}></dd>`,
-                                (i === 0 || track.group !== arr[i - 1].group) && fixWS`
-                                    <dt>${track.group}:</dt>
-                                    <dd><${listTag}>
-                                `,
-                                trackToListItem(track),
-                                i === arr.length && `</${listTag}></dd>`
-                            ].filter(Boolean)).join('\n')}
-                        </dl>
-                    ` : fixWS`
-                        <${listTag}>
-                            ${album.tracks.map(trackToListItem).join('\n')}
-                        </${listTag}>
-                    `}
-                    ${album.commentary && fixWS`
-                        <p>Artist commentary:</p>
-                        <blockquote>
-                            ${transformMultiline(album.commentary)}
-                        </blockquote>
-                    ` || `<!-- (here: Full-album commentary) -->`}
+    await writePage([C.ALBUM_DIRECTORY, album.directory], {
+        title: album.name,
+        body: {
+            style: `${getThemeString(album)}; --album-directory: ${album.directory}`
+        },
+        main: {
+            content: fixWS`
+                ${img({
+                    src: getAlbumCover(album),
+                    alt: 'album cover',
+                    id: 'cover-art',
+                    link: true,
+                    square: true
+                })}
+                <h1>${album.name}</h1>
+                <p>
+                    ${album.artists && `By ${getArtistString(album.artists, true)}.<br>` || `<!-- (here: Full-album musician credits) -->`}
+                    ${album.coverArtists &&  `Cover art by ${getArtistString(album.coverArtists, true)}.<br>` || `<!-- (here: Cover art credits) -->`}
+                    Released ${getDateString(album)}.
+                    ${+album.artDate !== +album.date && `<br>Art released ${getDateString({date: album.artDate})}.` || `<!-- (here: Cover art release date) -->`}
+                    <br>Duration: ~${getDurationString(getTotalDuration(album.tracks))}.</p>
+                </p>
+                ${album.urls.length && `<p>Listen on ${joinNoOxford(album.urls.map(url => fancifyURL(url, {album: true})), 'or')}.</p>` || `<!-- (here: Listen on...) -->`}
+                ${album.usesGroups ? fixWS`
+                    <dl class="album-group-list">
+                        ${album.tracks.flatMap((track, i, arr) => [
+                            (i > 0 && track.group !== arr[i - 1].group) && `</${listTag}></dd>`,
+                            (i === 0 || track.group !== arr[i - 1].group) && fixWS`
+                                ${track.group && `<dt>${track.group}:</dt>`}
+                                <dd><${listTag === 'ol' ? `ol start="${i + 1}"` : listTag}>
+                            `,
+                            trackToListItem(track),
+                            i === arr.length && `</${listTag}></dd>`
+                        ].filter(Boolean)).join('\n')}
+                    </dl>
+                ` : fixWS`
+                    <${listTag}>
+                        ${album.tracks.map(trackToListItem).join('\n')}
+                    </${listTag}>
+                `}
+                ${album.commentary && fixWS`
+                    <p>Artist commentary:</p>
+                    <blockquote>
+                        ${transformMultiline(album.commentary)}
+                    </blockquote>
+                ` || `<!-- (here: Full-album commentary) -->`}
+            `
+        },
+        sidebar: {
+            content: generateSidebarForAlbum(album)
+        },
+        nav: {
+            links: [
+                ['./', 'Home'],
+                [`${C.ALBUM_DIRECTORY}/${album.directory}/`, album.name],
+                [null, generateAlbumNavLinks(album)]
+            ],
+            content: fixWS`
+                <div>
+                    ${generateAlbumChronologyLinks(album)}
                 </div>
-            </div>
-        </body>
-    `);
+            `
+        }
+    });
 }
 
 async function writeTrackPage(track) {
+    const { album } = track;
     const tracksThatReference = getTracksThatReference(track);
     const ttrFanon = tracksThatReference.filter(t => t.album.isFanon);
     const ttrOfficial = tracksThatReference.filter(t => t.album.isOfficial);
     const tracksReferenced = getTracksReferencedBy(track);
     const flashesThatFeature = getFlashesThatFeature(track);
-    await writePage([C.TRACK_DIRECTORY, track.directory], track.name, fixWS`
-        <body style="${getThemeString(track)}; --album-directory: ${track.album.directory}; --track-directory: ${track.directory}">
-            <div id="header">
-                ${generateHeaderForAlbum(track.album, track)}
-            </div>
-            <div class="columns">
-                <div id="sidebar">
-                    ${generateSidebarForAlbum(track.album, track)}
-                </div>
-                <div id="content">
-                    <a href="${getTrackCover(track)}" id="cover-art"><img src="${getTrackCover(track)}" alt="cover art"></a>
-                    <h1>${track.name}</h1>
-                    <p>
-                        By ${getArtistString(track.artists, true)}.
-                        ${track.coverArtists &&  `<br>Cover art by ${getArtistString(track.coverArtists, true)}.` || `<!-- (here: Cover art credits) -->`}
-                        ${track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY && `<br>Released ${getDateString(track)}.` || `<!-- (here: Track release date) -->`}
-                        ${+track.artDate !== +track.date && `<br>Art released ${getDateString({date: track.artDate})}.` || `<!-- (here: Cover art release date, if it differs) -->`}
-                        ${track.duration && `<br>Duration: ${getDurationString(track.duration)}.` || `<!-- (here: Track duration) -->`}
-                    </p>
-                    ${track.urls.length ? fixWS`
-                        <p>Listen on ${joinNoOxford(track.urls.map(fancifyURL), 'or')}.</p>
-                    ` : fixWS`
-                        <p>This track has no URLs at which it can be listened.</p>
-                    `}
-                    ${track.contributors.textContent && fixWS`
-                        <p>Contributors:<br>${transformInline(track.contributors.textContent)}</p>
-                    `}
-                    ${track.contributors.length && fixWS`
-                        <p>Contributors:</p>
-                        <ul>
-                            ${track.contributors.map(contrib => `<li>${getArtistString([contrib], true)}</li>`).join('\n')}
-                        </ul>
-                    ` || `<!-- (here: Track contributor credits) -->`}
-                    ${tracksReferenced.length && fixWS`
-                        <p>Tracks that <i>${track.name}</i> references:</p>
-                        <ul>
-                            ${tracksReferenced.map(track => fixWS`
-                                <li>
-                                    <a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a>
-                                    <span class="by">by ${getArtistString(track.artists)}</span>
-                                </li>
-                            `).join('\n')}
-                        </ul>
-                    ` || `<!-- (here: List of tracks referenced) -->`}
-                    ${tracksThatReference.length && fixWS`
-                        <p>Tracks that reference <i>${track.name}</i>:</p>
-                        <dl>
-                            ${ttrOfficial.length && fixWS`
-                                <dt>Official:</dt>
-                                <dd><ul>
-                                    ${ttrOfficial.map(track => fixWS`
-                                        <li>
-                                            <a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a>
-                                            <span class="by">by ${getArtistString(track.artists)}</span>
-                                        </li>
-                                    `).join('\n')}
-                                </ul></dd>
-                            ` || `<!-- (here: Official tracks) -->`}
-                            ${ttrFanon.length && fixWS`
-                                <dt>Fandom:</dt>
-                                <dd><ul>
-                                    ${ttrFanon.map(track => fixWS`
-                                        <li>
-                                            <a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a>
-                                            <span class="by">by ${getArtistString(track.artists)}</span>
-                                        </li>
-                                    `).join('\n')}
-                                </ul></dd>
-                            ` || `<!-- (here: Fandom tracks) -->`}
-                        </dl>
-                    ` || `<!-- (here: Tracks that reference this track) -->`}
-                    ${flashesThatFeature.length && fixWS`
-                        <p>Flashes &amp; games that feature <i>${track.name}</i>:</p>
-                        <ul>
-                            ${flashesThatFeature.map(flash => `<li>${getFlashLinkHTML(flash)}</li>`).join('\n')}
-                        </ul>
-                    ` || `<!-- (here: Flashes that feature this track) -->`}
-                    ${track.lyrics && fixWS`
-                        <p>Lyrics:</p>
-                        <blockquote>
-                            ${transformMultiline(track.lyrics)}
-                        </blockquote>
-                    ` || `<!-- (here: Track lyrics) -->`}
-                    ${track.commentary && fixWS`
-                        <p>Artist commentary:</p>
-                        <blockquote>
-                            ${transformMultiline(track.commentary)}
-                        </blockquote>
-                    ` || `<!-- (here: Track commentary) -->`}
+    await writePage([C.TRACK_DIRECTORY, track.directory], {
+        title: track.name,
+        body: {
+            style: `${getThemeString(track)}; --album-directory: ${album.directory}; --track-directory: ${track.directory}`
+        },
+        sidebar: {
+            content: generateSidebarForAlbum(album, track)
+        },
+        nav: {
+            links: [
+                ['./', 'Home'],
+                [`${C.ALBUM_DIRECTORY}/${album.directory}/`, album.name],
+                [`${C.TRACK_DIRECTORY}/${track.directory}/`, track.name],
+                [null, generateAlbumNavLinks(album, track)]
+            ],
+            content: fixWS`
+                <div>
+                    ${generateAlbumChronologyLinks(album, track)}
                 </div>
-            </div>
-        </body>
-    `);
+            `
+        },
+        main: {
+            content: fixWS`
+                ${img({
+                    src: getTrackCover(track),
+                    alt: 'track cover',
+                    id: 'cover-art',
+                    link: true,
+                    square: true
+                })}
+                <h1>${track.name}</h1>
+                <p>
+                    By ${getArtistString(track.artists, true)}.
+                    ${track.coverArtists &&  `<br>Cover art by ${getArtistString(track.coverArtists, true)}.` || `<!-- (here: Cover art credits) -->`}
+                    ${album.directory !== C.UNRELEASED_TRACKS_DIRECTORY && `<br>Released ${getDateString(track)}.` || `<!-- (here: Track release date) -->`}
+                    ${+track.artDate !== +track.date && `<br>Art released ${getDateString({date: track.artDate})}.` || `<!-- (here: Cover art release date, if it differs) -->`}
+                    ${track.duration && `<br>Duration: ${getDurationString(track.duration)}.` || `<!-- (here: Track duration) -->`}
+                </p>
+                ${track.urls.length ? fixWS`
+                    <p>Listen on ${joinNoOxford(track.urls.map(fancifyURL), 'or')}.</p>
+                ` : fixWS`
+                    <p>This track has no URLs at which it can be listened.</p>
+                `}
+                ${track.contributors.textContent && fixWS`
+                    <p>Contributors:<br>${transformInline(track.contributors.textContent)}</p>
+                `}
+                ${track.contributors.length && fixWS`
+                    <p>Contributors:</p>
+                    <ul>
+                        ${track.contributors.map(contrib => `<li>${getArtistString([contrib], true)}</li>`).join('\n')}
+                    </ul>
+                ` || `<!-- (here: Track contributor credits) -->`}
+                ${tracksReferenced.length && fixWS`
+                    <p>Tracks that <i>${track.name}</i> references:</p>
+                    <ul>
+                        ${tracksReferenced.map(track => fixWS`
+                            <li>
+                                <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
+                                <span class="by">by ${getArtistString(track.artists)}</span>
+                            </li>
+                        `).join('\n')}
+                    </ul>
+                ` || `<!-- (here: List of tracks referenced) -->`}
+                ${tracksThatReference.length && fixWS`
+                    <p>Tracks that reference <i>${track.name}</i>:</p>
+                    <dl>
+                        ${ttrOfficial.length && fixWS`
+                            <dt>Official:</dt>
+                            <dd><ul>
+                                ${ttrOfficial.map(track => fixWS`
+                                    <li>
+                                        <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
+                                        <span class="by">by ${getArtistString(track.artists)}</span>
+                                    </li>
+                                `).join('\n')}
+                            </ul></dd>
+                        ` || `<!-- (here: Official tracks) -->`}
+                        ${ttrFanon.length && fixWS`
+                            <dt>Fandom:</dt>
+                            <dd><ul>
+                                ${ttrFanon.map(track => fixWS`
+                                    <li>
+                                        <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
+                                        <span class="by">by ${getArtistString(track.artists)}</span>
+                                    </li>
+                                `).join('\n')}
+                            </ul></dd>
+                        ` || `<!-- (here: Fandom tracks) -->`}
+                    </dl>
+                ` || `<!-- (here: Tracks that reference this track) -->`}
+                ${flashesThatFeature.length && fixWS`
+                    <p>Flashes &amp; games that feature <i>${track.name}</i>:</p>
+                    <ul>
+                        ${flashesThatFeature.map(flash => `<li>${getFlashLinkHTML(flash)}</li>`).join('\n')}
+                    </ul>
+                ` || `<!-- (here: Flashes that feature this track) -->`}
+                ${track.lyrics && fixWS`
+                    <p>Lyrics:</p>
+                    <blockquote>
+                        ${transformMultiline(track.lyrics)}
+                    </blockquote>
+                ` || `<!-- (here: Track lyrics) -->`}
+                ${track.commentary && fixWS`
+                    <p>Artist commentary:</p>
+                    <blockquote>
+                        ${transformMultiline(track.commentary)}
+                    </blockquote>
+                ` || `<!-- (here: Track commentary) -->`}
+            `
+        }
+    });
 }
 
 async function writeArtistPages() {
@@ -1191,7 +1553,7 @@ async function writeArtistPage(artistName) {
         return fixWS`
             <li title="${th(i + 1)} track by ${artistName}; ${th(track.album.tracks.indexOf(track) + 1)} in ${track.album.name}">
                 ${track.duration && `(${getDurationString(track.duration)})` || `<!-- (here: Duration) -->`}
-                <a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a>
+                <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
                 ${track.artists.includes(artistName) && track.artists.length > 1 && `<span class="contributed">(with ${getArtistString(track.artists.filter(a => a !== artistName))})</span>` || `<!-- (here: Co-artist credits) -->`}
                 ${contrib.what && `<span class="contributed">(${getContributionString(contrib) || 'contributed'})</span>` || `<!-- (here: Contribution details) -->`}
                 ${flashes.length && `<br><span class="flashes">(Featured in ${joinNoOxford(flashes.map(getFlashLinkHTML))})</span></br>` || `<!-- (here: Flashes featuring this track) -->`}
@@ -1201,8 +1563,8 @@ async function writeArtistPage(artistName) {
 
     // Shish!
     const kebab = C.getArtistDirectory(artistName);
-    const index = `${C.ARTIST_DIRECTORY}/${kebab}/index.html`;
-    await writePage([C.ARTIST_DIRECTORY, kebab], artistName, fixWS`
+    const index = `${C.ARTIST_DIRECTORY}/${kebab}/`;
+    await OLD_writePage([C.ARTIST_DIRECTORY, kebab], artistName, fixWS`
         <body>
             <div id="content">
                 ${ENABLE_ARTIST_AVATARS && await access(path.join(C.ARTIST_AVATAR_DIRECTORY, kebab + '.jpg')).then(() => true, () => false) && fixWS`
@@ -1237,7 +1599,7 @@ async function writeArtistPage(artistName) {
                         return fixWS`
                             <li title="${th(i + 1)} art by ${artistName}${thing.album && `; ${th(thing.album.tracks.indexOf(thing) + 1)} track in ${thing.album.name}`}">
                                 ${thing.album ? fixWS`
-                                    <a href="${C.TRACK_DIRECTORY}/${thing.directory}/index.html" style="${getThemeString(thing)}">${thing.name}</a>
+                                    <a href="${C.TRACK_DIRECTORY}/${thing.directory}/" style="${getThemeString(thing)}">${thing.name}</a>
                                 ` : '<i>(cover art)</i>'}
                                 ${thing.coverArtists.length > 1 && `<span class="contributed">(with ${getArtistString(thing.coverArtists.filter(({ who }) => who !== artistName))})</span>`}
                                 ${contrib.what && `<span class="contributed">(${getContributionString(contrib)})</span>`}
@@ -1251,7 +1613,7 @@ async function writeArtistPage(artistName) {
                         const contributionString = flash.contributors.filter(({ who }) => who === artistName).map(getContributionString).join(' ');
                         return fixWS`
                             <li>
-                                <a href="${C.FLASH_DIRECTORY}/${flash.directory}/index.html" style="${getThemeString(flash)}">${flash.name}</a>
+                                <a href="${C.FLASH_DIRECTORY}/${flash.directory}/" style="${getThemeString(flash)}">${flash.name}</a>
                                 ${contributionString && `<span class="contributed">(${contributionString})</span>`}
                                 (${getDateString({date: flash.date})})
                             </li>
@@ -1265,7 +1627,7 @@ async function writeArtistPage(artistName) {
                         return fixWS`
                             <li>
                                 ${thing.album ? fixWS`
-                                    <a href="${C.TRACK_DIRECTORY}/${thing.directory}/index.html" style="${getThemeString(thing)}">${thing.name}</a>
+                                    <a href="${C.TRACK_DIRECTORY}/${thing.directory}/" style="${getThemeString(thing)}">${thing.name}</a>
                                 ` : '(album commentary)'}
                                 ${flashes.length && `<br><span class="flashes">(Featured in ${joinNoOxford(flashes.map(getFlashLinkHTML))})</span></br>`}
                             </li>
@@ -1289,7 +1651,7 @@ function albumChunkedList(tracks, getLI, showDate = true, dateProperty = 'date')
                 if (i === 0 || album !== getAlbum(previous) || (showDate && +thing[dateProperty] !== +previous[dateProperty])) {
                     const heading = fixWS`
                         <dt>
-                            <a href="${C.ALBUM_DIRECTORY}/${getAlbum(thing).directory}/index.html" style="${getThemeString(getAlbum(thing))}">${getAlbum(thing).name}</a>
+                            <a href="${C.ALBUM_DIRECTORY}/${getAlbum(thing).directory}/" style="${getThemeString(getAlbum(thing))}">${getAlbum(thing).name}</a>
                             ${showDate && `(${getDateString({date: thing[dateProperty]})})`}
                         </dt>
                         <dd><ul>
@@ -1317,7 +1679,7 @@ function actChunkedList(flashes, getLI, showDate = true, dateProperty = 'date')
                 if (i === 0 || act !== previous.act) {
                     const heading = fixWS`
                         <dt>
-                            <a href="${C.FLASH_DIRECTORY}/${sorted.find(flash => !flash.act8r8k && flash.act === act).directory}/index.html" style="${getThemeString(flash)}">${flash.act}</a>
+                            <a href="${C.FLASH_DIRECTORY}/${sorted.find(flash => !flash.act8r8k && flash.act === act).directory}/" style="${getThemeString(flash)}">${flash.act}</a>
                         </dt>
                         <dd><ul>
                     `;
@@ -1358,17 +1720,110 @@ async function writeFlashPage(flash) {
         next && `<a href="${getHrefOfAnythingMan(next)}" id="next-button" title="${next.name}">Next</a>`
     ].filter(Boolean);
 
-    await writePage([C.FLASH_DIRECTORY, kebab], flash.name, fixWS`
-        <body style="${getThemeString(flash)}; --flash-directory: ${flash.directory}">
-            <div id="header">
-                <h2>
-                    <a href="index.html">Home</a>
-                    / <a href="${C.FLASH_DIRECTORY}/index.html">Flashes &amp; Games</a>
-                    / <a href="${C.FLASH_DIRECTORY}/${kebab}/index.html">${flash.name}</a>
-                    ${parts.length && fixWS`
-                        <span>(${parts.join(', ')})</span>
-                    ` || `<!-- (here: Flash navigation links) -->`}
-                </h2>
+    await writePage([C.FLASH_DIRECTORY, kebab], {
+        title: flash.name,
+        body: {
+            style: `${getThemeString(flash)}; --flash-directory: ${flash.directory}`
+        },
+        main: {
+            content: fixWS`
+                <h1>${flash.name}</h1>
+                <a id="cover-art" href="${getFlashCover(flash)}"><img src="${getFlashCover(flash)}" alt="cover art"></a>
+                <p>Released ${getDateString(flash)}.</p>
+                ${(flash.page || flash.urls.length) && `<p>Play on ${joinNoOxford(
+                    [
+                        flash.page && getFlashLink(flash),
+                        ...flash.urls
+                    ].map(url => `<span class="nowrap"><a href="${url}">${fancifyURL(url)}</a>` + (
+                        url.includes('homestuck.com') ? ` (${isNaN(Number(flash.page)) ? 'secret page' : `page ${flash.page}`})` :
+                        url.includes('bgreco.net') ? ` (HQ audio)` :
+                        url.includes('youtu') ? ` (on any device)` :
+                        ''
+                    ) + `</span>`), 'or')}.</p>` || `<!-- (here: Play-online links) -->`}
+                ${flash.contributors.textContent && fixWS`
+                    <p>Contributors:<br>${transformInline(flash.contributors.textContent)}</p>
+                `}
+                ${flash.tracks.length && fixWS`
+                    <p>Tracks featured in <i>${flash.name.replace(/\.$/, '')}</i>:</p>
+                    <ul>
+                        ${flash.tracks.map(ref => {
+                            const track = getLinkedTrack(ref);
+                            const neighm = ref.match(/(.*?\S):/) || [ref, ref];
+                            if (track) {
+                                const neeeighm = neighm[1].replace('$$$$', ':');
+                                return fixWS`
+                                    <li>
+                                        <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${neeeighm}</a>
+                                        <span class="by">by ${getArtistString(track.artists)}</span>
+                                    </li>
+                                `;
+                            } else {
+                                const by = ref.match(/\(by .*\)/);
+                                if (by) {
+                                    const name = ref.replace(by, '').trim();
+                                    const contribs = by[0].replace(/\(by |\)/g, '').split(',').map(w => ({who: w.trim()}));
+                                    return `<li>${name} <span class="by">by ${getArtistString(contribs)}</span></li>`;
+                                } else {
+                                    return `<li>${ref}</li>`;
+                                }
+                            }
+                        }).join('\n')}
+                    </ul>
+                ` || `<!-- (here: Flash track listing) -->`}
+                ${flash.contributors.length && fixWS`
+                    <p>Contributors:</p>
+                    <ul>
+                        ${flash.contributors.map(({ who, what }) => fixWS`
+                            <li>${artistNames.includes(who)
+                                ? `<a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(who)}/">${who}</a>`
+                                : who
+                            }${what && ` (${getContributionString({what})})`}</li>
+                        `).join('\n')}
+                    </ul>
+                ` || `<!-- (here: Flash contributor details) -->`}
+            `
+        },
+        sidebar: {
+            content: fixWS`
+                <h1><a href="${C.FLASH_DIRECTORY}/">Flashes &amp; Games</a></h1>
+                <dl>
+                    ${flashData.filter(f => f.act8r8k).filter(({ act }) =>
+                        act.startsWith('Act 1') ||
+                        act.startsWith('Act 6 Act 1') ||
+                        act.startsWith('Hiveswap') ||
+                        (
+                            flashData.findIndex(f => f.act === act) < act6 ? side === 1 :
+                            flashData.findIndex(f => f.act === act) < outsideCanon ? side === 2 :
+                            true
+                        )
+                    ).flatMap(({ act, color }) => [
+                        act.startsWith('Act 1') && `<dt ${classes('side', side === 1 && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act.startsWith('Act 1')))}/" style="--fg-color: #4ac925">Side 1 (Acts 1-5)</a></dt>`
+                        || act.startsWith('Act 6 Act 1') && `<dt ${classes('side', side === 2 && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act.startsWith('Act 6')))}/" style="--fg-color: #1076a2">Side 2 (Acts 6-7)</a></dt>`
+                        || act.startsWith('Hiveswap') && `<dt ${classes('side', side === 0 && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act.startsWith('Hiveswap')))}/" style="--fg-color: #008282">Outside Canon (Misc. Games)</a></dt>`,
+                        (
+                            flashData.findIndex(f => f.act === act) < act6 ? side === 1 :
+                            flashData.findIndex(f => f.act === act) < outsideCanon ? side === 2 :
+                            true
+                        ) && `<dt ${classes(act === flash.act && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act === act))}/" style="${getThemeString({color})}">${act}</a></dt>`,
+                        act === flash.act && fixWS`
+                            <dd><ul>
+                                ${flashData.filter(f => !f.act8r8k && f.act === act).map(f => fixWS`
+                                    <li ${classes(f === flash && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(f)}/" style="${getThemeString(f)}">${f.name}</a></li>
+                                `).join('\n')}
+                            </ul></dd>
+                        `
+                    ]).filter(Boolean).join('\n')}
+                </dl>
+            `
+        },
+        nav: {
+            links: [
+                ['./', 'Home'],
+                [`${C.FLASH_DIRECTORY}/`, `Flashes &amp; Games`],
+                [`${C.FLASH_DIRECTORY}/${kebab}/`, flash.name],
+                parts.length && [null, parts.join(', ')]
+            ].filter(Boolean),
+            content: fixWS`
                 <div>
                     ${chronologyLinks(flash, {
                         headingWord: 'flash/game',
@@ -1381,98 +1836,9 @@ async function writeFlashPage(flash) {
                         ]
                     }) || `<!-- (here: Contributor chronology links) -->`}
                 </div>
-            </div>
-            <div class="columns">
-                <div id="sidebar">
-                    <h1><a href="${C.FLASH_DIRECTORY}/index.html">Flashes &amp; Games</a></h1>
-                    <dl>
-                        ${flashData.filter(f => f.act8r8k).filter(({ act }) =>
-                            act.startsWith('Act 1') ||
-                            act.startsWith('Act 6 Act 1') ||
-                            act.startsWith('Hiveswap') ||
-                            (
-                                flashData.findIndex(f => f.act === act) < act6 ? side === 1 :
-                                flashData.findIndex(f => f.act === act) < outsideCanon ? side === 2 :
-                                true
-                            )
-                        ).flatMap(({ act, color }) => [
-                            act.startsWith('Act 1') && `<dt${classes('side', side === 1 && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act.startsWith('Act 1')))}/index.html" style="--fg-color: #4ac925">Side 1 (Acts 1-5)</a></dt>`
-                            || act.startsWith('Act 6 Act 1') && `<dt${classes('side', side === 2 && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act.startsWith('Act 6')))}/index.html" style="--fg-color: #1076a2">Side 2 (Acts 6-7)</a></dt>`
-                            || act.startsWith('Hiveswap') && `<dt${classes('side', side === 0 && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act.startsWith('Hiveswap')))}/index.html" style="--fg-color: #008282">Outside Canon (Misc. Games)</a></dt>`,
-                            (
-                                flashData.findIndex(f => f.act === act) < act6 ? side === 1 :
-                                flashData.findIndex(f => f.act === act) < outsideCanon ? side === 2 :
-                                true
-                            ) && `<dt${classes(act === flash.act && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act === act))}/index.html" style="${getThemeString({color})}">${act}</a></dt>`,
-                            act === flash.act && fixWS`
-                                <dd><ul>
-                                    ${flashData.filter(f => !f.act8r8k && f.act === act).map(f => fixWS`
-                                        <li${classes(f === flash && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(f)}/index.html" style="${getThemeString(f)}">${f.name}</a></li>
-                                    `).join('\n')}
-                                </ul></dd>
-                            `
-                        ]).filter(Boolean).join('\n')}
-                    </dl>
-                </div>
-                <div id="content">
-                    <h1>${flash.name}</h1>
-                    <a id="cover-art" href="${getFlashCover(flash)}"><img src="${getFlashCover(flash)}" alt="cover art"></a>
-                    <p>Released ${getDateString(flash)}.</p>
-                    ${(flash.page || flash.urls.length) && `<p>Play on ${joinNoOxford(
-                        [
-                            flash.page && getFlashLink(flash),
-                            ...flash.urls
-                        ].map(url => `<span class="nowrap"><a href="${url}">${fancifyURL(url)}</a>` + (
-                            url.includes('homestuck.com') ? ` (${isNaN(Number(flash.page)) ? 'secret page' : `page ${flash.page}`})` :
-                            url.includes('bgreco.net') ? ` (HQ audio)` :
-                            url.includes('youtu') ? ` (on any device)` :
-                            ''
-                        ) + `</span>`), 'or')}.</p>` || `<!-- (here: Play-online links) -->`}
-                    ${flash.contributors.textContent && fixWS`
-                        <p>Contributors:<br>${transformInline(flash.contributors.textContent)}</p>
-                    `}
-                    ${flash.tracks.length && fixWS`
-                        <p>Tracks featured in <i>${flash.name.replace(/\.$/, '')}</i>:</p>
-                        <ul>
-                            ${flash.tracks.map(ref => {
-                                const track = getLinkedTrack(ref);
-                                const neighm = ref.match(/(.*?\S):/) || [ref, ref];
-                                if (track) {
-                                    const neeeighm = neighm[1].replace('$$$$', ':');
-                                    return fixWS`
-                                        <li>
-                                            <a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${neeeighm}</a>
-                                            <span class="by">by ${getArtistString(track.artists)}</span>
-                                        </li>
-                                    `;
-                                } else {
-                                    const by = ref.match(/\(by .*\)/);
-                                    if (by) {
-                                        const name = ref.replace(by, '').trim();
-                                        const contribs = by[0].replace(/\(by |\)/g, '').split(',').map(w => ({who: w.trim()}));
-                                        return `<li>${name} <span class="by">by ${getArtistString(contribs)}</span></li>`;
-                                    } else {
-                                        return `<li>${ref}</li>`;
-                                    }
-                                }
-                            }).join('\n')}
-                        </ul>
-                    ` || `<!-- (here: Flash track listing) -->`}
-                    ${flash.contributors.length && fixWS`
-                        <p>Contributors:</p>
-                        <ul>
-                            ${flash.contributors.map(({ who, what }) => fixWS`
-                                <li>${artistNames.includes(who)
-                                    ? `<a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(who)}/index.html">${who}</a>`
-                                    : who
-                                }${what && ` (${getContributionString({what})})`}</li>
-                            `).join('\n')}
-                        </ul>
-                    ` || `<!-- (here: Flash contributor details) -->`}
-                </div>
-            </div>
-        </body>
-    `);
+            `
+        }
+    });
 }
 
 function writeListingPages() {
@@ -1480,14 +1846,14 @@ function writeListingPages() {
 
     const getAlbumLI = (album, extraText = '') => fixWS`
         <li>
-            <a href="${C.ALBUM_DIRECTORY}/${album.directory}/index.html" style="${getThemeString(album)}">${album.name}</a>
+            <a href="${C.ALBUM_DIRECTORY}/${album.directory}/" style="${getThemeString(album)}">${album.name}</a>
             ${extraText}
         </li>
     `;
 
     const getArtistLI = artistName => fixWS`
         <li>
-            <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artistName)}/index.html">${artistName}</a>
+            <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artistName)}/">${artistName}</a>
             (${C.getArtistNumContributions(artistName, {allTracks, albumData, flashData})} <abbr title="contributions (to music, art, and flashes)">c.</abbr>)
         </li>
     `;
@@ -1522,7 +1888,7 @@ function writeListingPages() {
             .sort((a, b) => b.commentary - a.commentary)
             .map(({ name, commentary }) => fixWS`
                 <li>
-                    <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}/index.html#commentary">${name}</a>
+                    <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}/#commentary">${name}</a>
                     (${commentary} ${commentary === 1 ? 'entry' : 'entries'})
                 </li>
             `)],
@@ -1539,7 +1905,7 @@ function writeListingPages() {
             .sort((a, b) => b.duration - a.duration)
             .map(({ name, duration }) => fixWS`
                 <li>
-                    <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}/index.html#tracks">${name}</a>
+                    <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}/#tracks">${name}</a>
                     (~${getDurationString(duration)})
                 </li>
             `)],
@@ -1553,22 +1919,22 @@ function writeListingPages() {
             .reverse()
             .map(({ name, date }) => fixWS`
                 <li>
-                    <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}/index.html">${name}</a>
+                    <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}/">${name}</a>
                     (${getDateString({date})})
                 </li>
             `)],
         [['tracks', 'by-name'], `Tracks - by Name`, allTracks.slice()
             .sort(sortByName)
             .map(track => fixWS`
-                <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a></li>
+                <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>
             `)],
         [['tracks', 'by-album'], `Tracks - by Album`, fixWS`
                 <dl>
                     ${albumData.map(album => fixWS`
-                        <dt><a href="${C.ALBUM_DIRECTORY}/${album.directory}/index.html" style="${getThemeString(album)}">${album.name}</a></dt>
+                        <dt><a href="${C.ALBUM_DIRECTORY}/${album.directory}/" style="${getThemeString(album)}">${album.name}</a></dt>
                         <dd><ol>
                             ${album.tracks.map(track => fixWS`
-                                <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a></li>
+                                <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>
                             `).join('\n')}
                         </ol></dd>
                     `).join('\n')}
@@ -1577,14 +1943,14 @@ function writeListingPages() {
         [['tracks', 'by-date'], `Tracks - by Date`, albumChunkedList(
             C.sortByDate(allTracks.filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY)),
             track => fixWS`
-                <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a></li>
+                <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>
             `)],
         [['tracks', 'by-duration'], `Tracks - by Duration`, C.sortByDate(allTracks.slice())
             .filter(track => track.duration > 0)
             .sort((a, b) => b.duration - a.duration)
             .map(track => fixWS`
                 <li>
-                    <a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a>
+                    <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
                     (${getDurationString(track.duration)})
                 </li>
             `)],
@@ -1596,7 +1962,7 @@ function writeListingPages() {
             )),
             track => fixWS`
                 <li>
-                    <a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a>
+                    <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
                     (${getDurationString(track.duration)})
                 </li>
             `,
@@ -1607,22 +1973,22 @@ function writeListingPages() {
             .sort((a, b) => getTracksThatReference(b).length - getTracksThatReference(a).length)
             .map(track => fixWS`
                 <li>
-                    <a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a>
+                    <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
                     (${s(getTracksThatReference(track).length, 'time')} referenced)
                 </li>
             `)],
         [['tracks', 'in-flashes', 'by-album'], `Tracks - in Flashes &amp; Games (by Album)`, albumChunkedList(
             C.sortByDate(allTracks.slice()).filter(track => getFlashesThatFeature(track).length > 0),
-            track => `<li><a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a></li>`)],
+            track => `<li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>`)],
         [['tracks', 'in-flashes', 'by-flash'], `Tracks - in Flashes &amp; Games (by First Feature)`,
             Array.from(new Set(flashData.filter(flash => !flash.act8r8k).flatMap(flash => getTracksFeaturedByFlash(flash))))
             .filter(Boolean)
-            .map(track => `<li><a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a></li>`)],
+            .map(track => `<li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>`)],
         [['tracks', 'with-lyrics'], `Tracks - with Lyrics`, albumChunkedList(
             C.sortByDate(allTracks.slice())
             .filter(track => track.lyrics),
             track => fixWS`
-                <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(track)}">${track.name}</a></li>
+                <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>
             `)]
     ];
 
@@ -1632,7 +1998,7 @@ function writeListingPages() {
     };
 
     return progressPromiseAll(`Writing listing pages.`, [
-        writePage([C.LISTING_DIRECTORY], `Listings Index`, fixWS`
+        OLD_writePage([C.LISTING_DIRECTORY], `Listings Index`, fixWS`
             <body>
                 <div id="header">
                     ${generateHeaderForListings(listingDescriptors)}
@@ -1649,7 +2015,7 @@ function writeListingPages() {
                 </div>
             </body>
         `),
-        writePage([C.LISTING_DIRECTORY, 'all-commentary'], 'All Commentary', fixWS`
+        OLD_writePage([C.LISTING_DIRECTORY, 'all-commentary'], 'All Commentary', fixWS`
             <body>
                 <div id="header">
                     ${generateHeaderForListings(listingDescriptors, 'all-commentary')}
@@ -1666,7 +2032,7 @@ function writeListingPages() {
                                 .filter(album => [album, ...album.tracks].some(x => x.commentary))
                                 .map(album => fixWS`
                                     <li>
-                                        <a href="${C.LISTING_DIRECTORY}/all-commentary/index.html#${album.directory}" style="${getThemeString(album)}">${album.name}</a>
+                                        <a href="${C.LISTING_DIRECTORY}/all-commentary/#${album.directory}" style="${getThemeString(album)}">${album.name}</a>
                                         (${(() => {
                                             const things = [album, ...album.tracks];
                                             const cThings = things.filter(x => x.commentary);
@@ -1683,14 +2049,14 @@ function writeListingPages() {
                             .map(album => [album, ...album.tracks])
                             .filter(x => x.some(y => y.commentary))
                             .map(([ album, ...tracks ]) => fixWS`
-                                <h2 id="${album.directory}"><a href="${C.ALBUM_DIRECTORY}/${album.directory}/index.html" style="${getThemeString(album)}">${album.name}</a></h2>
+                                <h2 id="${album.directory}"><a href="${C.ALBUM_DIRECTORY}/${album.directory}/" style="${getThemeString(album)}">${album.name}</a></h2>
                                 ${album.commentary && fixWS`
                                     <blockquote style="${getThemeString(album)}">
                                         ${transformMultiline(album.commentary)}
                                     </blockquote>
                                 ` || `<!-- (here: Full-album commentary) -->`}
                                 ${tracks.filter(t => t.commentary).map(track => fixWS`
-                                    <h3 id="${track.directory}"><a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html" style="${getThemeString(album)}">${track.name}</a></h3>
+                                    <h3 id="${track.directory}"><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(album)}">${track.name}</a></h3>
                                     <blockquote style="${getThemeString(album)}">
                                         ${transformMultiline(track.commentary)}
                                     </blockquote>
@@ -1702,7 +2068,7 @@ function writeListingPages() {
                 </div>
             </body>
         `),
-        writePage([C.LISTING_DIRECTORY, 'random'], 'Random Pages', fixWS`
+        OLD_writePage([C.LISTING_DIRECTORY, 'random'], 'Random Pages', fixWS`
             <body>
                 <div id="header">
                     ${generateHeaderForListings(listingDescriptors, 'random')}
@@ -1718,19 +2084,19 @@ function writeListingPages() {
                             <dt>Miscellaneous:</dt>
                             <dd><ul>
                                 <li>
-                                    <a href="${C.JS_DISABLED_DIRECTORY}/index.html" data-random="artist">Random Artist</a>
-                                    (<a href="${C.JS_DISABLED_DIRECTORY}/index.html" data-random="artist-more-than-one-contrib">&gt;1 contribution</a>)
+                                    <a href="${C.JS_DISABLED_DIRECTORY}/" data-random="artist">Random Artist</a>
+                                    (<a href="${C.JS_DISABLED_DIRECTORY}/" data-random="artist-more-than-one-contrib">&gt;1 contribution</a>)
                                 </li>
-                                <li><a href="${C.JS_DISABLED_DIRECTORY}/index.html" data-random="album">Random Album (whole site)</a></li>
-                                <li><a href="${C.JS_DISABLED_DIRECTORY}/index.html" data-random="track">Random Track (whole site)</a></li>
+                                <li><a href="${C.JS_DISABLED_DIRECTORY}/" data-random="album">Random Album (whole site)</a></li>
+                                <li><a href="${C.JS_DISABLED_DIRECTORY}/" data-random="track">Random Track (whole site)</a></li>
                             </ul></dd>
                             ${[
                                 {name: 'Official', albumData: officialAlbumData, code: 'official'},
                                 {name: 'Fandom', albumData: fandomAlbumData, code: 'fandom'}
                             ].map(category => fixWS`
-                                <dt>${category.name}: (<a href="${C.JS_DISABLED_DIRECTORY}/index.html" data-random="album-in-${category.code}">Random Album</a>, <a href="${C.JS_DISABLED_DIRECTORY}/index.html" data-random="track-in-${category.code}">Random Track</a>)</dt>
+                                <dt>${category.name}: (<a href="${C.JS_DISABLED_DIRECTORY}/" data-random="album-in-${category.code}">Random Album</a>, <a href="${C.JS_DISABLED_DIRECTORY}/" data-random="track-in-${category.code}">Random Track</a>)</dt>
                                 <dd><ul>${category.albumData.map(album => fixWS`
-                                    <li><a style="${getThemeString(album)}; --album-directory: ${album.directory}" href="${C.JS_DISABLED_DIRECTORY}/index.html" data-random="track-in-album">${album.name}</a></li>
+                                    <li><a style="${getThemeString(album)}; --album-directory: ${album.directory}" href="${C.JS_DISABLED_DIRECTORY}/" data-random="track-in-album">${album.name}</a></li>
                                 `).join('\n')}</ul></dd>
                             `).join('\n')}
                         </dl>
@@ -1743,7 +2109,7 @@ function writeListingPages() {
 }
 
 function writeListingPage(directoryParts, title, items, listingDescriptors) {
-    return writePage([C.LISTING_DIRECTORY, ...directoryParts], title, fixWS`
+    return OLD_writePage([C.LISTING_DIRECTORY, ...directoryParts], title, fixWS`
         <body>
             <div id="header">
                 ${generateHeaderForListings(listingDescriptors, directoryParts)}
@@ -1767,14 +2133,14 @@ function writeListingPage(directoryParts, title, items, listingDescriptors) {
 
 function generateHeaderForListings(listingDescriptors, currentDirectoryParts) {
     return fixWS`
-        <h2>
-            <a href="index.html">Home</a>
-            / <a href="${C.LISTING_DIRECTORY}/index.html">Listings</a>
+        <h2 class="highlight-last-link">
+            <a href="./">Home</a>
+            / <a href="${C.LISTING_DIRECTORY}/">Listings</a>
             ${currentDirectoryParts && `/ <a href="${C.LISTING_DIRECTORY}/${
                 Array.isArray(currentDirectoryParts)
                 ? currentDirectoryParts.join('/')
                 : currentDirectoryParts
-            }/index.html">` + (
+            }/">` + (
                 currentDirectoryParts === 'all-commentary' ? `All Commentary` :
                 currentDirectoryParts === 'random' ? `Random Pages` :
                 listingDescriptors.find(([ ldDirectoryParts ]) => ldDirectoryParts === currentDirectoryParts)[1]
@@ -1785,7 +2151,7 @@ function generateHeaderForListings(listingDescriptors, currentDirectoryParts) {
 
 function generateSidebarForListings(listingDescriptors, currentDirectoryParts) {
     return fixWS`
-        <h1><a href="${C.LISTING_DIRECTORY}/index.html">Listings</a></h1>
+        <h1><a href="${C.LISTING_DIRECTORY}/">Listings</a></h1>
         ${generateLinkIndexForListings(listingDescriptors, currentDirectoryParts)}
     `;
 }
@@ -1794,15 +2160,15 @@ function generateLinkIndexForListings(listingDescriptors, currentDirectoryParts)
     return fixWS`
         <ul>
             ${listingDescriptors.map(([ ldDirectoryParts, ldTitle ]) => fixWS`
-                <li${classes(currentDirectoryParts === ldDirectoryParts && 'current')}>
-                    <a href="${C.LISTING_DIRECTORY}/${ldDirectoryParts.join('/')}/index.html">${ldTitle}</a>
+                <li ${classes(currentDirectoryParts === ldDirectoryParts && 'current')}>
+                    <a href="${C.LISTING_DIRECTORY}/${ldDirectoryParts.join('/')}/">${ldTitle}</a>
                 </li>
             `).join('\n')}
-            <li${classes(currentDirectoryParts === 'all-commentary' && 'current')}>
-                <a href="${C.LISTING_DIRECTORY}/all-commentary/index.html">All Commentary</a>
+            <li ${classes(currentDirectoryParts === 'all-commentary' && 'current')}>
+                <a href="${C.LISTING_DIRECTORY}/all-commentary/">All Commentary</a>
             </li>
-            <li${classes(currentDirectoryParts === 'random' && 'current')}>
-                <a href="${C.LISTING_DIRECTORY}/random/index.html">Random Pages</a>
+            <li ${classes(currentDirectoryParts === 'random' && 'current')}>
+                <a href="${C.LISTING_DIRECTORY}/random/">Random Pages</a>
             </li>
         </ul>
     `;
@@ -1813,7 +2179,7 @@ function getContributionString({ what }) {
     return what
         ? what.replace(/\[(.*?)\]/g, (match, name) =>
             allTracks.some(track => track.name === name)
-                ? `<i><a href="${C.TRACK_DIRECTORY}/${allTracks.find(track => track.name === name).directory}/index.html">${name}</a></i>`
+                ? `<i><a href="${C.TRACK_DIRECTORY}/${allTracks.find(track => track.name === name).directory}/">${name}</a></i>`
                 : `<i>${name}</i>`)
         : '';
 }
@@ -1923,7 +2289,7 @@ function getArtistString(artists, showIcons = false) {
     return joinNoOxford(artists.map(({ who, what }) => {
         const { urls = [] } = artistData.find(({ name }) => name === who) || {};
         return (
-            `<a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(who)}/index.html">${who}</a>` +
+            `<a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(who)}/">${who}</a>` +
             (what ? ` (${getContributionString({what})})` : '') +
             (showIcons && urls.length ? ` <span class="icons">(${urls.map(iconifyURL).join(', ')})</span>` : '')
         );
@@ -1940,9 +2306,24 @@ function getThemeString({fg, bg, theme}) {
 }
 */
 
+// Graciously stolen from https://stackoverflow.com/a/54071699! ::::)
+// in: r,g,b in [0,1], out: h in [0,360) and s,l in [0,1]
+function rgb2hsl(r,g,b) {
+    let a=Math.max(r,g,b), n=a-Math.min(r,g,b), f=(1-Math.abs(a+a-n-1));
+    let h= n && ((a==r) ? (g-b)/n : ((a==g) ? 2+(b-r)/n : 4+(r-g)/n));
+    return [60*(h<0?h+6:h), f ? n/f : 0, (a+a-n)/2];
+}
+
 function getThemeString({color}) {
+    const [ r, g, b ] = color.slice(1)
+        .match(/[0-9a-fA-F]{2,2}/g)
+        .slice(0, 3)
+        .map(val => parseInt(val, 16) / 255);
+    const [ h, s, l ] = rgb2hsl(r, g, b);
+    const dim = `hsl(${Math.round(h)}deg, ${Math.round(s * 50)}%, ${Math.round(l * 80)}%)`;
+
     if (color) {
-        return `--fg-color: ${color}`;
+        return `--fg-color: ${color}; --dim-color: ${dim}`;
     } else {
         return ``;
     }
@@ -1978,6 +2359,7 @@ function fancifyURL(url, {album = false} = {}) {
         url.includes('deviantart.com') ? 'DeviantArt' :
         url.includes('wikipedia.org') ? 'Wikipedia' :
         url.includes('poetryfoundation.org') ? 'Poetry Foundation' :
+        url.includes('instagram.com') ? 'Instagram' :
         new URL(url).hostname
     }</a>`;
 }
@@ -1993,9 +2375,10 @@ function iconifyURL(url) {
         url.includes('tumblr.com') ? ['tumblr', 'Tumblr'] :
         url.includes('twitter.com') ? ['twitter', 'Twitter'] :
         url.includes('deviantart.com') ? ['deviantart', 'DeviantArt'] :
+        url.includes('instagram.com') ? ['instagram', 'Instagram'] :
         ['globe', `External (${new URL(url).hostname})`]
     );
-    return fixWS`<a href="${url}" class="icon"><svg><title>${msg}</title><use href="icons.svg#icon-${id}"></use></svg></a>`;
+    return fixWS`<a href="${url}" class="icon"><svg><title>${msg}</title><use href="${C.STATIC_DIRECTORY}/icons.svg#icon-${id}"></use></svg></a>`;
 }
 
 function chronologyLinks(currentTrack, {
@@ -2032,7 +2415,7 @@ function chronologyLinks(currentTrack, {
             next && `<a href="${getHrefOfAnythingMan(next)}" title="${next.name}">Next</a>`
         ].filter(Boolean);
 
-        const heading = `${th(index + 1)} ${headingWord} by <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artist)}/index.html">${artist}</a>`;
+        const heading = `${th(index + 1)} ${headingWord} by <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artist)}/">${artist}</a>`;
 
         return fixWS`
             <div class="chronology">
@@ -2043,66 +2426,72 @@ function chronologyLinks(currentTrack, {
     }).filter(Boolean).join('\n');
 }
 
-function generateHeaderForAlbum(album, currentTrack = null) {
+function generateAlbumNavLinks(album, currentTrack = null) {
     const index = currentTrack && album.tracks.indexOf(currentTrack)
     const previous = currentTrack && album.tracks[index - 1]
     const next = currentTrack && album.tracks[index + 1]
-    return fixWS`
-        <h2>
-            <a href="index.html">Home</a>
-            / <a href="${C.ALBUM_DIRECTORY}/${album.directory}/index.html">${album.name}</a>
-            ${currentTrack && `/ <a href="${C.TRACK_DIRECTORY}/${currentTrack.directory}/index.html">${currentTrack.name}</a>` || `<!-- (here: Link to current track) --> `}
-            ${album.tracks.length > 1 && fixWS`
-                <span>(${[
-                    previous && `<a href="${C.TRACK_DIRECTORY}/${previous.directory}/index.html" id="previous-button" title="${previous.name}">Previous</a>`,
-                    next && `<a href="${C.TRACK_DIRECTORY}/${next.directory}/index.html" id="next-button" title="${next.name}">Next</a>`,
-                    `<a href="${C.JS_DISABLED_DIRECTORY}/index.html" data-random="track-in-album" id="random-button">${currentTrack ? 'Random' : 'Random Track'}</a>`
-                ].filter(Boolean).join(', ')})</span>
-            ` || `<!-- (here: Album navigation links) -->`}
-        </h2>
-        <div>
-            ${currentTrack && chronologyLinks(currentTrack, {
-                headingWord: 'track',
-                sourceData: allTracks,
-                filters: [
-                    {
-                        mapProperty: 'artists',
-                        toArtist: artist => artist
-                    },
-                    {
-                        mapProperty: 'contributors',
-                        toArtist: ({ who }) => who
-                    }
-                ]
-            }) || `<!-- (here: Musician & contributors chronology links) -->`}
-            ${chronologyLinks(currentTrack || album, {
-                headingWord: 'cover art',
-                sourceData: justEverythingSortedByArtDateMan,
-                filters: [
-                    {
-                        mapProperty: 'coverArtists',
-                        toArtist: ({ who }) => who
-                    }
-                ]
-            }) || `<!-- (here: Cover art chronology links) -->`}
-        </div>
-    `;
+
+    const [ previousLine, nextLine, randomLine ] = [
+        previous && `<a href="${C.TRACK_DIRECTORY}/${previous.directory}/" id="previous-button" title="${previous.name}">Previous</a>`,
+        next && `<a href="${C.TRACK_DIRECTORY}/${next.directory}/" id="next-button" title="${next.name}">Next</a>`,
+        `<a href="${C.JS_DISABLED_DIRECTORY}/" data-random="track-in-album" id="random-button">${currentTrack ? 'Random' : 'Random Track'}</a>`
+    ];
+
+    if (previousLine || nextLine) {
+        return `(${[previousLine, nextLine].filter(Boolean).join(', ')}<span class="js-hide">, ${randomLine}</span>)`;
+    } else {
+        return `<span class="js-hide">(${randomLine})</span>`;
+    }
+}
+
+function generateAlbumChronologyLinks(album, currentTrack = null) {
+    return [
+        currentTrack && chronologyLinks(currentTrack, {
+            headingWord: 'track',
+            sourceData: allTracks,
+            filters: [
+                {
+                    mapProperty: 'artists',
+                    toArtist: ({ who }) => who
+                },
+                {
+                    mapProperty: 'contributors',
+                    toArtist: ({ who }) => who
+                }
+            ]
+        }),
+        chronologyLinks(currentTrack || album, {
+            headingWord: 'cover art',
+            sourceData: justEverythingSortedByArtDateMan,
+            filters: [
+                {
+                    mapProperty: 'coverArtists',
+                    toArtist: ({ who }) => who
+                }
+            ]
+        })
+    ].filter(Boolean).join('\n');
 }
 
 function generateSidebarForAlbum(album, currentTrack = null) {
-    const trackToListItem = track => `<li${classes(track === currentTrack && 'current')}><a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html">${track.name}</a></li>`;
+    const trackToListItem = track => `<li ${classes(track === currentTrack && 'current')}><a href="${C.TRACK_DIRECTORY}/${track.directory}/">${track.name}</a></li>`;
     const listTag = getAlbumListTag(album);
     return fixWS`
-        <h1><a href="${C.ALBUM_DIRECTORY}/${album.directory}/index.html">${album.name}</a></h1>
+        <h1><a href="${C.ALBUM_DIRECTORY}/${album.directory}/">${album.name}</a></h1>
         ${album.usesGroups ? fixWS`
             <dl>
                 ${album.tracks.flatMap((track, i, arr) => [
                     (i > 0 && track.group !== arr[i - 1].group) && `</${listTag}></dd>`,
                     (i === 0 || track.group !== arr[i - 1].group) && fixWS`
-                        <dt style="${getThemeString(track)}"${classes(currentTrack && track.group === currentTrack.group && 'current')}><a href="${C.TRACK_DIRECTORY}/${track.directory}/index.html">${track.group}</a></dt>
-                        <dd><${listTag}>
+                        ${track.group && fixWS`
+                            <dt style="${getThemeString(track)}" ${classes(currentTrack && track.group === currentTrack.group && 'current')}>
+                                <a href="${C.TRACK_DIRECTORY}/${track.directory}/">${track.group}</a>
+                                ${listTag === 'ol' ? `(${i + 1}&ndash;${arr.length - arr.slice().reverse().findIndex(t => t.group === track.group)})` : `<!-- (here: track number range) -->`}
+                            </dt>
+                        `}
+                        <dd style="${getThemeString(track)}"><${listTag === 'ol' ? `ol start="${i + 1}"` : listTag}>
                     `,
-                    (currentTrack && track.group === currentTrack.group) && trackToListItem(track),
+                    (!currentTrack || track.group === currentTrack.group) && trackToListItem(track),
                     i === arr.length && `</${listTag}></dd>`
                 ].filter(Boolean)).join('\n')}
             </dl>
@@ -2114,35 +2503,6 @@ function generateSidebarForAlbum(album, currentTrack = null) {
     `
 }
 
-// These two functions are sort of hard-coded ways to quickly gra8 the path to
-// cover arts, for em8edding witin the HTML. They're actually 8ig hacks,
-// 8ecause they assume the track and al8um directories are adjacent to each
-// other. I get to make that assumption on the responsi8ility that I la8el
-// these functions "hard-coded", which 8asically just means my future self and
-// anyone else trying to mess with this code can't 8lame me for my terri8le
-// decisions / laziness in figuring out a 8etter solution. That said, note to
-// future self: these only work from two levels a8ove the root directory.
-// "O8viously," if you look at their implementation, 8ut if you don't... yeah.
-// You won't 8e a8le to call these for use in the lower level files.
-// ACTUALLY this means I really should just use a <base> element, which yes, I
-// have done 8efore (on my 8log). That way all HTML files have the same root
-// for referenced files, and these functions work anywhere. The catch, then, is
-// that you have to have a "8ase directory" constant, and keep that accurate on
-// 8oth your development machine and the server you pu8lish this too. So, it's
-// a trade-off. 8ut it does mean much cleaner, more general-use functions.
-// Which is kind of the goal here, I suppose. --- Actually, hold on, I took a
-// look at the document8tion and apparently relative URLs are totally okay!
-// Com8ine that with path.relative and I think that should work as a way to
-// skip a 8ase directory constant. Neat!
-/*
-function getAlbumCover(album) {
-    return `../../${C.ALBUM_DIRECTORY}/${album.directory}/cover.png`;
-}
-function getTrackCover(track) {
-    return `../../${C.ALBUM_DIRECTORY}/${track.album.directory}/${track.directory}.png`;
-}
-*/
-
 function getHrefOfAnythingMan(anythingMan) {
     return (
         albumData.includes(anythingMan) ? C.ALBUM_DIRECTORY :
@@ -2152,11 +2512,12 @@ function getHrefOfAnythingMan(anythingMan) {
     ) + '/' + (
         flashData.includes(anythingMan) ? getFlashDirectory(anythingMan) :
         anythingMan.directory
-    ) + '/index.html';
+    ) + '/';
 }
 
 function getAlbumCover(album) {
-    return `${C.ALBUM_DIRECTORY}/${album.directory}/cover.jpg`;
+    const file = 'cover.jpg';
+    return `${C.MEDIA_DIRECTORY}/${C.MEDIA_ALBUM_ART_DIRECTORY}/${album.directory}/${file}`;
 }
 function getTrackCover(track) {
     // Some al8ums don't have any track art at all, and in those, every track
@@ -2164,11 +2525,13 @@ function getTrackCover(track) {
     if (track.coverArtists === null) {
         return getAlbumCover(track.album);
     } else {
-        return `${C.ALBUM_DIRECTORY}/${track.album.directory}/${track.directory}.jpg`;
+        const file = `${track.directory}.jpg`;
+        return `${C.MEDIA_DIRECTORY}/${C.MEDIA_ALBUM_ART_DIRECTORY}/${track.album.directory}/${file}`;
     }
 }
 function getFlashCover(flash) {
-    return `${C.FLASH_DIRECTORY}/${getFlashDirectory(flash)}.${flash.jiff === 'Yeah' ? 'gif' : 'png'}`;
+    const file = `${getFlashDirectory(flash)}.${flash.jiff === 'Yeah' ? 'gif' : 'png'}`;
+    return `${C.MEDIA_DIRECTORY}/${C.MEDIA_FLASH_ART_DIRECTORY}/${file}`;
 }
 
 function getFlashLink(flash) {
@@ -2179,14 +2542,14 @@ function getFlashLinkHTML(flash, name = null) {
     if (!name) {
         name = flash.name;
     }
-    return `<a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flash)}/index.html" title="Page ${flash.page}" style="${getThemeString(flash)}">${name}</a>`;
+    return `<a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flash)}/" title="Page ${flash.page}" style="${getThemeString(flash)}">${name}</a>`;
 }
 
 function rebaseURLs(directory, html) {
     if (directory === '') {
         return html;
     }
-    return html.replace(/(href|src)="(.*?)"/g, (match, attr, url) => {
+    return html.replace(/(href|src|data-original)="(.*?)"/g, (match, attr, url) => {
         if (url.startsWith('#')) {
             return `${attr}="${url}"`;
         }
@@ -2196,7 +2559,7 @@ function rebaseURLs(directory, html) {
             // no error: it's a full url
         } catch (error) {
             // caught an error: it's a component!
-            url = path.relative(directory, url);
+            url = path.relative(directory, path.join(C.SITE_DIRECTORY, url));
         }
         return `${attr}="${url}"`;
     });
@@ -2204,7 +2567,8 @@ function rebaseURLs(directory, html) {
 
 function classes(...args) {
     const values = args.filter(Boolean);
-    return values.length ? ` class="${values.join(' ')}"` : '';
+    // return values.length ? ` class="${values.join(' ')}"` : '';
+    return `class="${values.join(' ')}"`;
 }
 
 async function main() {
@@ -2253,13 +2617,13 @@ async function main() {
     // but for now we dont do any significant error throwing
     // (not any that wouldnt be caught elsewhere, later)
     // so i guess its not a big deal???? :o
-    artistData = await processArtistDataFile(ARTIST_DATA_FILE);
+    artistData = await processArtistDataFile(path.join(C.DATA_DIRECTORY, ARTIST_DATA_FILE));
     if (artistData.error) {
         console.log(`\x1b[31;1m${artistData.error}\x1b[0m`);
         return;
     }
 
-    flashData = await processFlashDataFile(FLASH_DATA_FILE);
+    flashData = await processFlashDataFile(path.join(C.DATA_DIRECTORY, FLASH_DATA_FILE));
     if (flashData.error) {
         console.log(`\x1b[31;1m${flashData.error}\x1b[0m`);
         return;
@@ -2402,6 +2766,7 @@ async function main() {
         }
     }
 
+    await writeSymlinks();
     await writeMiscellaneousPages();
     await writeListingPages();
     await progressPromiseAll(`Writing album & track pages.`, queue(albumData.map(album => writeIndexAndTrackPagesForAlbum(album)).reduce((a, b) => a.concat(b))));