commit e055b0ca7d0ff724792b9a0037b4f295ea5c6997
parent 0ec62439205a3ebe8d12394f0a3e96ee6d66a841
Author: David Eisinger <[email protected]>
Date: Sun, 11 Oct 2026 09:03:02 -0400
Use new player in posts
Diffstat:
5 files changed, 72 insertions(+), 7 deletions(-)
diff --git a/test/navigation.cjs b/test/navigation.cjs
@@ -22,20 +22,20 @@ function write(name, text) {
(async () => {
fs.cpSync(path.join(repo, 'themes/v2'), path.join(root, 'themes/v2'), { recursive: true });
- write('hugo.toml', 'baseURL="http://localhost/"\ntheme="v2"\ntitle="David Eisinger"\n');
+ write('hugo.toml', 'baseURL="http://localhost/"\ntheme="v2"\ntitle="David Eisinger"\n[markup.goldmark.renderer]\nunsafe=true\n');
write('layouts/partials/marky.html', '');
write('content/_index.md', 'A personal website.');
write('content/music/_index.md', '---\ntitle: Music\n---\n');
write('content/about/index.md', '---\ntitle: About\n---\n' +
'[Jump to contact](#contact)\n\n' + 'A paragraph for testing scroll restoration.\n\n'.repeat(90) +
'## Contact\n\n[Music](/music/)\n\n' + 'More content.\n\n'.repeat(20));
- write('content/journal/example/index.md', '---\ntitle: Example post\ndate: 2026-01-01\ntags: [dispatch]\n---\nHello.');
+ write('content/journal/example/index.md', '---\ntitle: Example post\ndate: 2026-01-01\ntags: [dispatch]\n---\nHello.\n\n<audio controls src="/journal/example/Night Moon.mp3"></audio>');
fs.copyFileSync(path.join(repo, 'content/journal/dispatch-44-october-2026/Night Moon.mp3'),
path.join(root, 'content/journal/example/Night Moon.mp3'));
- write('content/journal/older/index.md', '---\ntitle: Older post\ndate: 2025-01-01\n---\nAn earlier recording.');
+ write('content/journal/older/index.md', '---\ntitle: Older post\ndate: 2025-01-01\n---\nAn earlier recording.\n\n<audio controls src="/journal/older/Older Song.mp3"></audio>\n\nAnother copy.\n\n<audio controls src="/journal/older/Older Song.mp3"></audio>');
fs.copyFileSync(path.join(repo, 'content/journal/dispatch-44-october-2026/Night Moon.mp3'),
path.join(root, 'content/journal/older/Older Song.mp3'));
- write('content/elsewhere/example/index.md', '---\ntitle: Elsewhere example\ndate: 2025-01-01\ncanonical_url: https://example.org/original\n---\nOriginal article.');
+ write('content/elsewhere/example/index.md', '---\ntitle: Elsewhere example\ndate: 2025-01-01\ncanonical_url: https://example.org/original\nexclude_music: true\n---\nOriginal article.\n\n<audio controls src="/journal/example/Night Moon.mp3"></audio>');
server = http.createServer((request, response) => {
const isFetch = request.headers.accept === 'text/html';
if (isFetch && failNextFetch) {
@@ -196,6 +196,29 @@ function write(name, text) {
assert.equal(await page.locator('#site-header .track-title').textContent(), 'Night Moon');
assert.equal(await page.locator('#content audio').evaluateAll(players => players.every(audio => audio.paused)), true);
+ // Post embeds share playback and metadata, including multiple embeds and revisits.
+ await click('/journal/older/');
+ assert.equal(await page.locator('.track-toggle').count(), 2);
+ assert.equal(await page.locator('#content audio:visible').count(), 0);
+ await page.getByRole('button', { name: 'Play Older Song', exact: true }).first().click();
+ await page.waitForFunction(() => !originalAudio.paused && originalAudio.currentTime > 0.1
+ && originalAudio.src.includes('Older%20Song.mp3'));
+ assert.equal(await page.getByRole('button', { name: 'Pause Older Song', exact: true }).count(), 2);
+ assert.equal(await page.locator('#site-header .track-source a').textContent(), 'Older post');
+ await click('/music/');
+ await page.getByRole('button', { name: 'Pause Older Song', exact: true }).waitFor();
+ await click('/journal/older/');
+ assert.equal(await page.getByRole('button', { name: 'Pause Older Song', exact: true }).count(), 2);
+ await page.getByRole('button', { name: 'Pause Older Song', exact: true }).last().click();
+ assert.equal(await page.evaluate(() => originalAudio.paused), true);
+ await click('/elsewhere/example/');
+ assert.equal(await page.locator('.track-toggle').count(), 0);
+ assert.equal(await page.locator('#content audio:visible').count(), 1);
+ await click('/journal/example/');
+ await page.getByRole('button', { name: 'Play Night Moon', exact: true }).click();
+ await page.getByRole('button', { name: 'Pause Night Moon', exact: true }).waitFor();
+ assert.equal(await page.locator('#site-header .track-source a').textContent(), 'Example post');
+
// A failed enhanced request must retry with normal document navigation.
failNextFetch = true;
await click('/about/');
@@ -217,8 +240,18 @@ function write(name, text) {
await plain.getByRole('link', { name: 'More music →' }).click();
assert.equal(await plain.locator('#content audio:visible').count(), 2);
assert.equal(await plain.locator('.track-toggle').count(), 0);
+ await plain.goto(origin + '/journal/example/');
+ assert.equal(await plain.locator('#content audio:visible').count(), 1);
+ assert.equal(await plain.locator('.track-toggle').count(), 0);
await noJS.close();
+ // A direct post load enhances its embeds too.
+ await page.goto(origin + '/journal/older/');
+ assert.equal(await page.locator('.track-toggle').count(), 2);
+ await page.getByRole('button', { name: 'Play Older Song', exact: true }).first().click();
+ await page.getByRole('button', { name: 'Pause Older Song', exact: true }).first().waitFor();
+
+
await page.setViewportSize({ width: 390, height: 850 });
await page.goto(origin);
await click('/about/');
diff --git a/themes/v2/assets/css/style.scss b/themes/v2/assets/css/style.scss
@@ -422,7 +422,7 @@ pre {
align-items: center;
display: flex;
gap: 2ch;
- padding: 2ch 0;
+ padding: 1ch 0;
> div {
flex: 1;
diff --git a/themes/v2/assets/js/player.js b/themes/v2/assets/js/player.js
@@ -54,7 +54,35 @@
}
}
+ function preparePostTracks() {
+ document.querySelectorAll('#content [data-post-music]').forEach(post => {
+ post.querySelectorAll('audio[src]').forEach(audio => {
+ if (audio.closest('.music-track')) return;
+ const url = new URL(audio.src);
+ if (!url.pathname.toLowerCase().endsWith('.mp3')) return;
+ let name = url.pathname.split('/').pop().slice(0, -4);
+ try { name = decodeURIComponent(name); } catch { /* Keep malformed filenames readable. */ }
+ const row = document.createElement('div');
+ row.className = 'music-track';
+ const details = document.createElement('div');
+ const trackTitle = document.createElement('p');
+ trackTitle.className = 'track-title';
+ trackTitle.textContent = name;
+ const trackSource = document.createElement('p');
+ trackSource.className = 'track-source';
+ const link = document.createElement('a');
+ link.href = post.dataset.postUrl;
+ link.textContent = post.dataset.postTitle;
+ trackSource.append('From ', link);
+ details.append(trackTitle, trackSource);
+ audio.before(row);
+ row.append(audio, details);
+ });
+ });
+ }
+
function enhance() {
+ preparePostTracks();
controls = [];
document.querySelectorAll('#content .music-track').forEach(row => {
const audio = row.querySelector('audio');
diff --git a/themes/v2/layouts/elsewhere/single.html b/themes/v2/layouts/elsewhere/single.html
@@ -18,7 +18,9 @@
on <a href="{{ .Params.canonical_url }}">{{ $host }}</a>
</h3>
- {{ .Content }}
+ <div{{ if ne .Params.exclude_music true }} data-post-music{{ end }} data-post-title="{{ .Title }}" data-post-url="{{ .RelPermalink }}">
+ {{ .Content }}
+ </div>
{{- partial "references.html" . -}}
{{- partial "backlinks.html" . -}}
diff --git a/themes/v2/layouts/journal/single.html b/themes/v2/layouts/journal/single.html
@@ -18,7 +18,9 @@
{{ end }}
</h3>
- {{ .Content }}
+ <div{{ if ne .Params.exclude_music true }} data-post-music{{ end }} data-post-title="{{ .Title }}" data-post-url="{{ .RelPermalink }}">
+ {{ .Content }}
+ </div>
{{- partial "references.html" . -}}
{{- partial "backlinks.html" . -}}