openvibers/openvibe.media Commits

Live's recordings and clips on Media's pages point at OpenVibe.Video: the watch page's canonical and the browse cards named openvibe.live/vod/<id>, a 301 since Live#143 (#145)

OP OpenVibers committed 24 days ago · 8c52bce430 · 4 files, +28 −17

Changes

ddiff --git a/server/public/browse.js b/server/public/browse.js
iindex 659b8cb..a8dede2 100644
---- a/server/public/browse.js
++++ b/server/public/browse.js
@@@ -19,7 +19,7 @@ const THUMB_DIR = path.resolve(config.thumbnails.path);
// Public base URL per app for "source" links — shared with the watch pages.
const pageFrame = require('./page-frame');
--const { appUrl, isoDate } = pageFrame;
++const { appUrl, watchPageUrl, isoDate } = pageFrame;
const cache = require('openvibe-shared/cache-policy');
const frame = require('openvibe-shared/frame');
const showcase = require('openvibe-shared/showcase');
@@@ -36,7 +36,7 @@ function vodCard(v) {
thumb: abs(v.thumbnail_url), duration: v.duration_seconds,
text: snip(v.description || v.ai_overview),
view: `${config.publicUrl}/v/${v.id}`,
-- source: `${appUrl(v.app_id)}/vod/${v.id}`, sourceLabel: 'VOD page',
++ source: watchPageUrl('vod', v.app_id, v.id), sourceLabel: 'VOD page',
};
}
function clipCard(c) {
@@@ -45,7 +45,7 @@ function clipCard(c) {
thumb: abs(c.thumbnail_url), duration: c.duration_seconds,
text: snip(c.description || c.ai_overview),
view: `${config.publicUrl}/c/${c.id}`,
-- source: `${appUrl(c.app_id)}/clip/${c.id}`, sourceLabel: 'Clip page',
++ source: watchPageUrl('clip', c.app_id, c.id), sourceLabel: 'Clip page',
};
}
function fileCard(f) {
@@@ -76,8 +76,8 @@ function thumbCard(name, mtimeMs) {
let source = null, sourceLabel = null;
if (m) {
const base = appUrl('live');
-- if (m[1] === 'vod') { source = `${base}/vod/${m[2]}`; sourceLabel = 'VOD page'; }
-- else if (m[1] === 'clip') { source = `${base}/clip/${m[2]}`; sourceLabel = 'Clip page'; }
++ if (m[1] === 'vod') { source = watchPageUrl('vod', 'live', m[2]); sourceLabel = 'VOD page'; }
++ else if (m[1] === 'clip') { source = watchPageUrl('clip', 'live', m[2]); sourceLabel = 'Clip page'; }
else { source = `${base}/stream/${m[2]}`; sourceLabel = 'Stream'; }
}
return {
ddiff --git a/server/public/page-frame.js b/server/public/page-frame.js
iindex c9a5909..a095f38 100644
---- a/server/public/page-frame.js
++++ b/server/public/page-frame.js
@@@ -36,6 +36,14 @@ const APP_PUBLIC_URLS = (() => {
return defaults;
})();
const appUrl = (appId) => APP_PUBLIC_URLS[appId] || APP_PUBLIC_URLS.live;
++// Live's recordings and clips are watched on OpenVibe.Video since 2026-10-19: Live's /vod/<id> and /clip/<id> 301 there.
++const VIDEO_PUBLIC_URL = String(process.env.VIDEO_PUBLIC_URL || 'https://openvibe.video').replace(/\/+$/, '');
++/** The canonical watch page of an app's VOD or clip: OpenVibe.Video's for Live's, the app's own /vod or /clip otherwise. */
++function watchPageUrl(kind, appId, id) {
++ const app = appId || 'live';
++ if (app === 'live') return `${VIDEO_PUBLIC_URL}/${kind === 'clip' ? 'c' : 'v'}/${id}`;
++ return `${appUrl(app)}/${kind === 'clip' ? 'clip' : 'vod'}/${id}`;
++}
const esc = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
const abs = (u) => (!u ? null : (/^https?:\/\//i.test(u) ? u : `${config.publicUrl}${u.startsWith('/') ? '' : '/'}${u}`));
@@@ -215,7 +223,7 @@ function page({ seo, css = '', body, history, footer, summary }) {
}
module.exports = {
-- NETWORK_URL, SITE_NAME, DEFAULT_OG_IMAGE, APP_PUBLIC_URLS, appUrl,
++ NETWORK_URL, SITE_NAME, DEFAULT_OG_IMAGE, APP_PUBLIC_URLS, appUrl, VIDEO_PUBLIC_URL, watchPageUrl,
esc, abs, snip, isoDate, isoDuration, fmtDuration, fmtDate, jsonForScript,
extraHead, baseCss, navOptions, footerOptions, defaultFooterLinks, page, DEFAULT_THEME_CSS,
};
ddiff --git a/server/public/pages.js b/server/public/pages.js
iindex 7163879..f179459 100644
---- a/server/public/pages.js
++++ b/server/public/pages.js
@@@ -17,7 +17,7 @@ const config = require('../config');
const pageFrame = require('./page-frame');
const showcase = require('openvibe-shared/showcase');
--const { esc, abs, snip, appUrl, isoDate, isoDuration, fmtDuration, fmtDate, SITE_NAME, NETWORK_URL } = pageFrame;
++const { esc, abs, snip, appUrl, watchPageUrl, isoDate, isoDuration, fmtDuration, fmtDate, SITE_NAME, NETWORK_URL } = pageFrame;
const VIDEO_MIME = { '.mp4': 'video/mp4', '.webm': 'video/webm', '.mkv': 'video/x-matroska' };
/** Link-preview and search crawlers: they want the page, never the bytes. */
@@@ -45,10 +45,9 @@ function creatorLine(record) {
return record.username || record.display_name || null;
}
--/** Where the owning app shows this item — Live's /vod and /clip pages. */
++/** Where this item is watched: OpenVibe.Video for Live's, the owning app's own page otherwise. */
function sourcePage(kind, record) {
-- const base = appUrl(record.app_id || 'live');
-- return `${base}/${kind === 'vod' ? 'vod' : 'clip'}/${record.id}`;
++ return watchPageUrl(kind, record.app_id, record.id);
}
/** A clip the owning app's automation cut (AI moments), not a person. */
@@@ -58,7 +57,7 @@ function isAiClip(kind, record) {
/**
* May search engines index this watch page? Only when it is public, Media is its canonical home (the
-- * owning app is not Live, which serves its own /vod and /clip pages), and a person made it: AI clips
++ * owning app is not Live, whose items are canonical on OpenVibe.Video), and a person made it: AI clips
* are noindex, as on Live. The sitemap lists exactly these pages (server/public/crawl.js).
*/
function watchIndexable(kind, record) {
ddiff --git a/test/public-pages.test.js b/test/public-pages.test.js
iindex 8e7e5d5..53869af 100644
---- a/test/public-pages.test.js
++++ b/test/public-pages.test.js
@@@ -34,7 +34,11 @@ const http = require('http');
};
const html = pages.renderWatchPage('vod', vod);
assert.ok(html.includes('<title>Late night &lt;build&gt; — OpenVibe.Media</title>'));
-- assert.ok(html.includes('<link rel="canonical" href="https://openvibe.live/vod/42">'), 'Live-owned VOD is canonical on Live');
++ assert.ok(html.includes('<link rel="canonical" href="https://openvibe.video/v/42">'), 'Live-owned VOD is canonical on OpenVibe.Video');
++ // The browse cards' "VOD page" and "Clip page" links use the same rule: Live's on OpenVibe.Video, another app's own.
++ assert.strictEqual(pageFrame.watchPageUrl('vod', 'live', 42), 'https://openvibe.video/v/42');
++ assert.strictEqual(pageFrame.watchPageUrl('clip', null, 9), 'https://openvibe.video/c/9');
++ assert.strictEqual(pageFrame.watchPageUrl('vod', 'games', 3), 'https://openvibe.games/vod/3');
assert.ok(html.includes('<meta name="robots" content="noindex, follow">'));
assert.ok(/\/shared\/theme-loader\.js\?v=[0-9a-f]{12}/.test(html), 'this site\'s own pinned Frame files');
// The shell loads the theme-loader eagerly (no defer) inside <head>, so it applies the user's
@@@ -59,7 +63,7 @@ const http = require('http');
assert.strictEqual(ld.duration, 'PT1H2M5S');
assert.strictEqual(ld.uploadDate, '2026-09-10T20:15:00.000Z');
assert.deepStrictEqual(ld.thumbnailUrl, ['https://media.test/t/vod-42-1.jpg']);
-- assert.strictEqual(ld.url, 'https://openvibe.live/vod/42');
++ assert.strictEqual(ld.url, 'https://openvibe.video/v/42');
assert.ok(!html.includes('</script>"'), 'no script-closing sequence smuggled through JSON');
const other = pages.renderWatchPage('clip', { ...vod, id: 7, app_id: 'games', title: 'Frag' });
@@@ -70,7 +74,7 @@ const http = require('http');
// The AI summary (openvibe-shared/seo pageSummary) on public watch pages: description, canonical, facts.
assert.ok(html.includes('<meta name="ai-summary" content="We built a thing &amp; it worked.">'), 'the watch page carries the AI summary');
assert.ok(html.includes('<li>Duration: 1:02:05</li>') && html.includes('<li>Views: 1,234</li>'), 'with its facts');
-- assert.ok(html.includes('"@type":"WebPage"') && html.includes('"url":"https://openvibe.live/vod/42"'), 'its WebPage names the canonical');
++ assert.ok(html.includes('"@type":"WebPage"') && html.includes('"url":"https://openvibe.video/v/42"'), 'its WebPage names the canonical');
assert.ok(!unlisted.includes('name="ai-summary"'), 'unlisted pages carry none');
// ── Next-step cards (Shared ADR 0002, D94): real, signed-out targets, ov_from=media, at most four ──
@@@ -88,16 +92,16 @@ const http = require('http');
};
// A playable public Live-owned VOD: the Live card (the canonical) and the index; no clip-cutting card on a VOD.
assert.ok(html.includes('class="sc-sec sc-next"') && html.includes('<ol class="sc-next-list">'), 'a playable public VOD page carries the next-step cards');
-- assert.deepStrictEqual(checkCards(html, 'Live VOD'), ['https://openvibe.live/vod/42?ov_from=media', 'https://media.test/?ov_from=media'],
++ assert.deepStrictEqual(checkCards(html, 'Live VOD'), ['https://openvibe.video/v/42?ov_from=media', 'https://media.test/?ov_from=media'],
'a Live-owned item\'s card is its canonical; the index card is always there');
// A VOD another app owns: only the index.
assert.deepStrictEqual(checkCards(pages.renderWatchPage('vod', { ...vod, id: 43, app_id: 'games' }), 'games VOD'), ['https://media.test/?ov_from=media']);
// A playable person-cut clip on Live: Live, cut your own, the index.
assert.deepStrictEqual(checkCards(pages.renderWatchPage('clip', { ...vod, id: 9 }), 'Live clip'),
-- ['https://openvibe.live/clip/9?ov_from=media', 'https://openvibe.live/?ov_from=media', 'https://media.test/?ov_from=media']);
++ ['https://openvibe.video/c/9?ov_from=media', 'https://openvibe.live/?ov_from=media', 'https://media.test/?ov_from=media']);
// An AI clip with a source VOD: every card applies, and four is the most that render.
const aiClip = pages.renderWatchPage('clip', { ...vod, id: 10, auto_generated: 1, vod_id: 42 });
-- assert.deepStrictEqual(checkCards(aiClip, 'AI clip'), ['https://openvibe.live/clip/10?ov_from=media', 'https://media.test/v/42?ov_from=media',
++ assert.deepStrictEqual(checkCards(aiClip, 'AI clip'), ['https://openvibe.video/c/10?ov_from=media', 'https://media.test/v/42?ov_from=media',
'https://openvibe.live/?ov_from=media', 'https://media.test/?ov_from=media'], 'a clip with a source VOD shows the VOD card');
assert.ok(aiClip.includes('See the VOD this clip came from'));
assert.strictEqual((aiClip.match(/class="sc-next-item"/g) || []).length, 4, 'at most four cards');