openvibers/openvibe.mediahub Commits
Video watch pages: the page title names the channel and the day ('Fortnite!' titled twelve different recordings' pages) (#93)
Changes
@@@ -146,6 +146,13 @@ function videoLd({ base, path, item, title, description, chapterList, partOf })};}++// A watch page's title: streamers reuse stream titles ("Fortnite!" titled twelve different recordings' pages on++// 2026-10-17), so the channel and the day make each page's title its own in search results and browser tabs.++function watchTitle(title, who, iso) {++ const parts = [/^[A-Za-z0-9_]{1,32}$/.test(String(who || '')) ? `@${who}` : null, dateLabel(iso) || null].filter(Boolean);++ return parts.length ? `${title} (${parts.join(', ')})` : title;++}++function dateLabel(iso) {if (!iso) return '';const d = new Date(iso);@@@ -784,7 +791,7 @@ ${uploaded.length ? html`<section class="ovv-section" aria-labelledby="h-uploadsindex: !unlisted && !ownOnly,noindex: unlisted || ownOnly,...(ownOnly ? { cache: cache.htmlHeaders({ private: true }) } : {}),-- title: vod.title || 'Stream recording',++ title: watchTitle(vod.title || 'Stream recording', vod.username, vod.created_at),description: vod.description || vod.ai_overview || '',...(ownOnly || vod.is_recording ? {} : {jsonLd: videoLd({ base, path: `/v/${id}`, item: vod, title: vod.title || 'Stream recording', description: vod.description || vod.ai_overview, chapterList: mems }),@@@ -861,7 +868,7 @@ ${clips.length ? html`<section class="ovv-panel" aria-labelledby="h-clips"><h2 iindex: !unlisted && !ownOnly && !aiClip,noindex: unlisted || ownOnly || aiClip,...(ownOnly ? { cache: cache.htmlHeaders({ private: true }) } : {}),-- title: clip.title || 'Clip',++ title: watchTitle(clip.title || 'Clip', clip.source_streamer_username, clip.created_at),description: clip.ai_overview || '',...(ownOnly || aiClip ? {} : {jsonLd: videoLd({ base, path: `/c/${id}`, item: clip, title: clip.title || 'Clip', description: clip.ai_overview || attribution, partOf: clip.vod_id ? `/v/${clip.vod_id}` : null }),@@@ -250,6 +250,7 @@ function countOccurrences(haystack, needle) {assert.ok(r.text.includes('<video'), 'a video element');assert.ok(r.text.includes('src="https://openvibe.media/v/101"'), 'the playback url');assert.match(r.text, /class="ovv-avatar"[^>]*>S<img src="[^"]*\/avatar\/streamer" alt="" loading="lazy">/, 'the streamer\'s picture over the letter');++ assert.match(r.text, /<title>[^<]*\(@streamer, Oct 12, 2026\)[^<]*<\/title>/, 'the title names the channel and the day (streamers reuse stream titles)');assert.ok(r.text.includes('preload="metadata"'));assert.ok(r.text.includes('playsinline'));assert.ok(r.text.includes('data-ovv-video'));
@@ -146,6 +146,13 @@ function videoLd({ base, path, item, title, description, chapterList, partOf })
@@ -146,6 +146,13 @@ function videoLd({ base, path, item, title, description, chapterList, partOf })
};
};
}
}
+// A watch page's title: streamers reuse stream titles ("Fortnite!" titled twelve different recordings' pages on
+// 2026-10-17), so the channel and the day make each page's title its own in search results and browser tabs.
+function watchTitle(title, who, iso) {
+ const parts = [/^[A-Za-z0-9_]{1,32}$/.test(String(who || '')) ? `@${who}` : null, dateLabel(iso) || null].filter(Boolean);
+ return parts.length ? `${title} (${parts.join(', ')})` : title;
+}
+
function dateLabel(iso) {
function dateLabel(iso) {
if (!iso) return '';
if (!iso) return '';
const d = new Date(iso);
const d = new Date(iso);
@@ -784,7 +791,7 @@ ${uploaded.length ? html`<section class="ovv-section" aria-labelledby="h-uploads
@@ -784,7 +791,7 @@ ${uploaded.length ? html`<section class="ovv-section" aria-labelledby="h-uploads
index: !unlisted && !ownOnly,
index: !unlisted && !ownOnly,
noindex: unlisted || ownOnly,
noindex: unlisted || ownOnly,
...(ownOnly ? { cache: cache.htmlHeaders({ private: true }) } : {}),
...(ownOnly ? { cache: cache.htmlHeaders({ private: true }) } : {}),
- title: vod.title || 'Stream recording',
+ title: watchTitle(vod.title || 'Stream recording', vod.username, vod.created_at),
description: vod.description || vod.ai_overview || '',
description: vod.description || vod.ai_overview || '',
...(ownOnly || vod.is_recording ? {} : {
...(ownOnly || vod.is_recording ? {} : {
jsonLd: videoLd({ base, path: `/v/${id}`, item: vod, title: vod.title || 'Stream recording', description: vod.description || vod.ai_overview, chapterList: mems }),
jsonLd: videoLd({ base, path: `/v/${id}`, item: vod, title: vod.title || 'Stream recording', description: vod.description || vod.ai_overview, chapterList: mems }),
@@ -861,7 +868,7 @@ ${clips.length ? html`<section class="ovv-panel" aria-labelledby="h-clips"><h2 i
@@ -861,7 +868,7 @@ ${clips.length ? html`<section class="ovv-panel" aria-labelledby="h-clips"><h2 i
index: !unlisted && !ownOnly && !aiClip,
index: !unlisted && !ownOnly && !aiClip,
noindex: unlisted || ownOnly || aiClip,
noindex: unlisted || ownOnly || aiClip,
...(ownOnly ? { cache: cache.htmlHeaders({ private: true }) } : {}),
...(ownOnly ? { cache: cache.htmlHeaders({ private: true }) } : {}),
- title: clip.title || 'Clip',
+ title: watchTitle(clip.title || 'Clip', clip.source_streamer_username, clip.created_at),
description: clip.ai_overview || '',
description: clip.ai_overview || '',
...(ownOnly || aiClip ? {} : {
...(ownOnly || aiClip ? {} : {
jsonLd: videoLd({ base, path: `/c/${id}`, item: clip, title: clip.title || 'Clip', description: clip.ai_overview || attribution, partOf: clip.vod_id ? `/v/${clip.vod_id}` : null }),
jsonLd: videoLd({ base, path: `/c/${id}`, item: clip, title: clip.title || 'Clip', description: clip.ai_overview || attribution, partOf: clip.vod_id ? `/v/${clip.vod_id}` : null }),
@@ -250,6 +250,7 @@ function countOccurrences(haystack, needle) {
@@ -250,6 +250,7 @@ function countOccurrences(haystack, needle) {
assert.ok(r.text.includes('<video'), 'a video element');
assert.ok(r.text.includes('<video'), 'a video element');
assert.ok(r.text.includes('src="https://openvibe.media/v/101"'), 'the playback url');
assert.ok(r.text.includes('src="https://openvibe.media/v/101"'), 'the playback url');
assert.match(r.text, /class="ovv-avatar"[^>]*>S<img src="[^"]*\/avatar\/streamer" alt="" loading="lazy">/, 'the streamer\'s picture over the letter');
assert.match(r.text, /class="ovv-avatar"[^>]*>S<img src="[^"]*\/avatar\/streamer" alt="" loading="lazy">/, 'the streamer\'s picture over the letter');
+ assert.match(r.text, /<title>[^<]*\(@streamer, Oct 12, 2026\)[^<]*<\/title>/, 'the title names the channel and the day (streamers reuse stream titles)');
assert.ok(r.text.includes('preload="metadata"'));
assert.ok(r.text.includes('preload="metadata"'));
assert.ok(r.text.includes('playsinline'));
assert.ok(r.text.includes('playsinline'));
assert.ok(r.text.includes('data-ovv-video'));
assert.ok(r.text.includes('data-ovv-video'));