openvibers/openvibe.live Commits

Share cards: every page's preview is the network's card (Go live from your browser), with page cards for The Rundown, Content, AI Moments and Chat, instead of the old purple /og-image.png (openvibe-shared v4.6.0) (#271)

OP OpenVibers committed 24 days ago · 06a51b11b8 · 13 files, +120 −17

Changes

ddiff --git a/README.md b/README.md
iindex 8ca7eb29..1621d018 100644
---- a/README.md
++++ b/README.md
@@@ -176,5 +176,5 @@ Minimum `.env`: `BASE_URL`, `JWT_SECRET`, `OV_NETWORK_URL`, `OV_NETWORK_INTERNAL
<!-- versions:start -->
- openvibe-contracts: v0.203.0
- openvibe-sdk: v0.50.1
--- openvibe-shared: v4.4.0
++- openvibe-shared: v4.6.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex c933c0c1..308f7b7e 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,7 +7,7 @@
"domain": "openvibe.live (www, ingest, whip)",
"contracts": "openvibe-contracts v0.203.0",
"sdk": "openvibe-sdk v0.50.1",
-- "shared": "openvibe-shared v4.4.0",
++ "shared": "openvibe-shared v4.6.0",
"capabilities": [
"live.chat_context.read",
"live.chat_effects.write",
ddiff --git a/package-lock.json b/package-lock.json
iindex b2484d42..deab1bf2 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -25,7 +25,7 @@
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.203.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.4.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.6.0",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
@@@ -2688,9 +2688,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.4.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.4.0",
-- "integrity": "sha512-zhC3wzRdI3HG+TzLEEXTzZquBoHJHzE/67kGgmrYCXmouqcugKA+RZZ3gFQXMsIx79IQNrbE/tlg0hqiyUCJmw==",
++ "version": "4.6.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.6.0",
++ "integrity": "sha512-fWYacR2Oc4/A6LT8ebnfGnPn2Ip0S5BQNWnw/QN/iDoAW55ql7CaiSlOBEmJkinITf1pCcIJKQi382CdgLDHTg==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex 7d996585..e66cb45d 100644
---- a/package.json
++++ b/package.json
@@@ -44,7 +44,7 @@
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.203.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.4.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.6.0",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
ddiff --git a/public/dmca.html b/public/dmca.html
iindex c507fb71..eb6cb58b 100644
---- a/public/dmca.html
++++ b/public/dmca.html
@@@ -11,7 +11,7 @@
<meta property="og:title" content="DMCA Policy — OpenVibe">
<meta property="og:description" content="DMCA and copyright takedown policy for OpenVibe: OpenVibe.Live, openvibe.tools and openvibe.games. How to send a notice and a counter-notice.">
<meta property="og:url" content="https://openvibe.live/dmca">
-- <meta property="og:image" content="https://openvibe.live/og-image.png">
++ <meta property="og:image" content="https://openvibe.live/shared/social/live.jpg">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
ddiff --git a/public/index.html b/public/index.html
iindex ed830508..51d75d47 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -15,7 +15,7 @@
<meta property="og:url" content="https://openvibe.live/">
<meta property="og:title" content="OpenVibe.Live — Go Live From Your Browser, No OBS Needed">
<meta property="og:description" content="Press Go Live, allow your camera and mic, and you're live. No OBS, no downloads, no follower minimum. Chat, VODs, clips and restreaming to Twitch, YouTube and Kick.">
-- <meta property="og:image" content="https://openvibe.live/og-image.png">
++ <meta property="og:image" content="https://openvibe.live/shared/social/live.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="OpenVibe.Live - Live Streaming Platform">
@@@ -27,7 +27,7 @@
<meta name="twitter:url" content="https://openvibe.live/">
<meta name="twitter:title" content="OpenVibe.Live — Go Live From Your Browser, No OBS Needed">
<meta name="twitter:description" content="Go live from your browser in seconds. No OBS, no downloads, no follower minimum. Open source and community run.">
-- <meta name="twitter:image" content="https://openvibe.live/og-image.png">
++ <meta name="twitter:image" content="https://openvibe.live/shared/social/live.jpg">
<meta name="twitter:image:alt" content="OpenVibe.Live Platform">
<!-- Icons & Preconnect -->
ddiff --git a/public/privacy.html b/public/privacy.html
iindex d7b431ef..7690751e 100644
---- a/public/privacy.html
++++ b/public/privacy.html
@@@ -11,7 +11,7 @@
<meta property="og:title" content="Privacy Policy — OpenVibe">
<meta property="og:description" content="Privacy Policy for OpenVibe: OpenVibe.Live, openvibe.tools, openvibe.games. We collect the least we can, we don't sell it, and transcription runs locally.">
<meta property="og:url" content="https://openvibe.live/privacy">
-- <meta property="og:image" content="https://openvibe.live/og-image.png">
++ <meta property="og:image" content="https://openvibe.live/shared/social/live.jpg">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
ddiff --git a/public/tos.html b/public/tos.html
iindex 84b7aab7..5076c08f 100644
---- a/public/tos.html
++++ b/public/tos.html
@@@ -11,7 +11,7 @@
<meta property="og:title" content="Terms of Service — OpenVibe">
<meta property="og:description" content="Terms of Service for OpenVibe: OpenVibe.Live, openvibe.tools, openvibe.games. Minimal rules, maximum freedom.">
<meta property="og:url" content="https://openvibe.live/tos">
-- <meta property="og:image" content="https://openvibe.live/og-image.png">
++ <meta property="og:image" content="https://openvibe.live/shared/social/live.jpg">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
ddiff --git a/server/index.js b/server/index.js
iindex 07c40943..1d448899 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -573,6 +573,8 @@ console.log(`[Server] /shared: serving ${SHARED_BROWSER_FILES.length} browser fi
res.setHeader('Cache-Control', 'no-cache');
res.sendFile(path.join(sharedServePath, 'openvibe-sw.js'));
});
++ // The share cards (og:image on every page), before the browser-file allowlist below.
++ require('./web/share-cards').register(app);
// Serve only the whitelisted browser files — don't expose the entire package
const sharedFileSet = new Set(SHARED_BROWSER_FILES);
app.use('/shared', (req, res, next) => {
ddiff --git a/server/seo/pages.js b/server/seo/pages.js
iindex 8457a68d..ad796af0 100644
---- a/server/seo/pages.js
++++ b/server/seo/pages.js
@@@ -152,7 +152,13 @@ const pasteHref = (slug) => `${PASTES_BASE}/p/${encodeURIComponent(slug)}`;
const VIDEO_META = `<meta name="ov-video-base" content="${esc(VIDEO_BASE)}">`;
const SITE_NAME = 'OpenVibe.Live';
--const DEFAULT_OG_IMAGE = '/og-image.png';
++// Share cards (openvibe-shared/social, served at /shared/social/<id>.jpg by server/index.js): the site's card, and a page
++// card for the big pages. They replaced /og-image.png (a logo on purple, "Free & Open Live Streaming") on 2026-10-20,
++// after a friend said openvibe.quest's preview "looks way cooler than openvibe.live".
++const social = require('openvibe-shared/social');
++const shareCard = (id) => social.url(id) || social.url('live') || '/og-image.png';
++const DEFAULT_OG_IMAGE = shareCard('live');
++const LIST_CARDS = { content: 'live-content', vods: 'live-content', clips: 'live-content', pastes: 'live-content', moments: 'live-moments' };
function baseUrl() {
let b = (config && config.baseUrl) || 'https://openvibe.live';
@@@ -211,7 +217,7 @@ async function _pageMeta(routePath, { page = 1 } = {}) {
// Search results (public/js/app-search.js) are never indexed; the page itself is findable.
if (p === '/search') return { title: `Search | ${SITE_NAME}`, description: 'Search channels, VODs and clips on OpenVibe.Live.', canonicalPath: '/search', ogType: 'website', robots: 'noindex,follow', jsonLd: [] };
// The global chat room: its own title and canonical (the shell left it with neither; browser check, WS-Q task 3).
-- if (p === '/chat') return { title: `Chat | ${SITE_NAME}`, description: 'The global chat room of OpenVibe.Live, open to everyone on the network.', canonicalPath: '/chat', ogType: 'website', robots: 'index,follow', jsonLd: [] };
++ if (p === '/chat') return { title: `Chat | ${SITE_NAME}`, description: 'The global chat room of OpenVibe.Live, open to everyone on the network.', canonicalPath: '/chat', image: shareCard('live-chat'), ogType: 'website', robots: 'index,follow', jsonLd: [] };
// The API docs and the deploy log had the home page's title and description and no canonical: duplicates of "/".
if (p === '/documentation') return { title: `API docs | ${SITE_NAME}`, description: 'The OpenVibe.Live API: sign-in and tokens, streams and channels, WebRTC and WHIP broadcasting, chat bots, robots, AI, VODs, clips and webhooks.', canonicalPath: '/documentation', ogType: 'website', robots: 'index,follow', jsonLd: [] };
if (p === '/updates') return { title: `Updates | ${SITE_NAME}`, description: 'Every change deployed to OpenVibe.Live, newest first: each line is a commit from its open-source repository on GitHub.', canonicalPath: '/updates', ogType: 'website', robots: 'index,follow', jsonLd: [] };
@@@ -354,7 +360,7 @@ async function _clipperOf(c) { return c.display_name || c.username || _nameOf(aw
async function _rundownMeta(p) {
const read = require('../rundown/read');
const crumbs = [{ name: 'Home', url: '/' }, { name: 'The Rundown', url: '/rundown' }];
-- const base = { image: DEFAULT_OG_IMAGE, ogType: 'website' };
++ const base = { image: shareCard('live-rundown'), ogType: 'website' };
let m;
if (p === '/rundown') {
let home = null;
@@@ -570,7 +576,7 @@ async function _listMeta(slug, label, description, itemsFn, { page = 1 } = {}) {
if (more !== null && (page > 1 || more)) snapshot += _pager(basePath, page, null, more, label.toLowerCase());
return {
title: `${label}${pageLabel} — ${SITE_NAME}`, description, canonicalPath: selfPath,
-- image: DEFAULT_OG_IMAGE, ogType: 'website', robots: 'index,follow',
++ image: shareCard(LIST_CARDS[slug] || 'live'), ogType: 'website', robots: 'index,follow',
jsonLd: [list, _breadcrumb([{ name: 'Home', url: '/' }, { name: label, url: basePath }, ...(page > 1 ? [{ name: `Page ${page}`, url: selfPath }] : [])])],
snapshot,
};
ddiff --git a/server/web/share-cards.js b/server/web/share-cards.js
nnew file mode 100644
iindex 00000000..2f5af315
---- /dev/null
++++ b/server/web/share-cards.js
@@@ -0,0 +1,24 @@
++'use strict';
++/**
++ * The share cards: the og:image of every page (server/seo/pages.js), openvibe-shared/social's 1200×630 JPEGs served from
++ * the pinned copy at /shared/social/<id>.jpg. `live` is the site's card; `live-rundown`, `live-content`, `live-moments`
++ * and `live-chat` are page cards. Only a ?v= that matches the card's own hash is immutable; anything else that is not a
++ * card falls through (to /shared's own 404).
++ */
++const social = require('openvibe-shared/social');
++const cache = require('openvibe-shared/cache-policy');
++
++function register(app) {
++ app.get('/shared/social/:file', (req, res, next) => {
++ const name = `social/${req.params.file}`;
++ if (!social.isCard(name)) return next();
++ const hashed = Boolean(req.query.v) && social.url(req.params.file.replace(/\.jpg$/, '')) === `/shared/${name}?v=${req.query.v}`;
++ res.setHeader('Access-Control-Allow-Origin', '*');
++ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
++ res.setHeader('Cache-Control', cache.assetHeaders(req.originalUrl, { hashed }));
++ if (hashed) res.setHeader('CDN-Cache-Control', cache.IMMUTABLE);
++ res.type('jpg').sendFile(social.path(name));
++ });
++}
++
++module.exports = { register };
ddiff --git a/test/seo-ssr.test.js b/test/seo-ssr.test.js
iindex a1c5c8ed..eb4810c2 100644
---- a/test/seo-ssr.test.js
++++ b/test/seo-ssr.test.js
@@@ -239,7 +239,7 @@ const server = http.createServer(app).listen(0, '127.0.0.1', async () => {
const html = fs.readFileSync(path.join(__dirname, '..', 'public', `${name}.html`), 'utf8');
assert.ok(html.includes(`<link rel="canonical" href="https://openvibe.live/${name}">`), name);
assert.ok(html.includes(`<meta property="og:url" content="https://openvibe.live/${name}">`), name);
-- assert.ok(/<meta property="og:title" content="[^"]+">/.test(html) && html.includes('<meta property="og:image" content="https://openvibe.live/og-image.png">'), name);
++ assert.ok(/<meta property="og:title" content="[^"]+">/.test(html) && html.includes('<meta property="og:image" content="https://openvibe.live/shared/social/live.jpg">'), name);
assert.ok(!/for the OpenVibe\b/.test(html.slice(0, html.indexOf('</head>'))), `${name}: "for the OpenVibe" reads wrong`);
}
});
ddiff --git a/test/share-cards.test.js b/test/share-cards.test.js
nnew file mode 100644
iindex 00000000..932d0b6c
---- /dev/null
++++ b/test/share-cards.test.js
@@@ -0,0 +1,71 @@
++/**
++ * Share cards (server/web/share-cards.js, server/seo/pages.js): every page's og:image is an openvibe-shared/social card
++ * served from Live's pinned copy, not the old /og-image.png. The site's card by default; The Rundown, the content lists,
++ * AI Moments and Chat each have a page card. A hashed URL is immutable, an unknown card falls through.
++ *
++ * node test/share-cards.test.js
++ */
++'use strict';
++const assert = require('assert');
++const fs = require('fs');
++const http = require('http');
++const path = require('path');
++const express = require('express');
++const social = require('openvibe-shared/social');
++
++(async () => {
++ let failures = 0;
++ const check = async (name, fn) => { try { await fn(); console.log(` ✓ ${name}`); } catch (err) { failures++; console.log(` ✗ ${name}\n ${err.stack}`); } };
++ const app = express();
++ require('../server/web/share-cards').register(app);
++ app.use((req, res) => res.status(404).send('shared 404'));
++ const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });
++ const get = (p) => new Promise((resolve, reject) => {
++ http.get({ host: '127.0.0.1', port: server.address().port, path: p }, (res) => {
++ const chunks = [];
++ res.on('data', (c) => chunks.push(c));
++ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) }));
++ }).on('error', reject);
++ });
++
++ await check('the site card and the page cards are in the pinned openvibe-shared', () => {
++ for (const id of ['live', 'live-rundown', 'live-content', 'live-moments', 'live-chat']) assert.ok(social.has(id), id);
++ });
++
++ await check('/shared/social/<id>.jpg answers the JPEG; its own hash is immutable, anything else is not', async () => {
++ const url = social.url('live-rundown');
++ const r = await get(url);
++ assert.strictEqual(r.status, 200);
++ assert.strictEqual(r.headers['content-type'], 'image/jpeg');
++ assert.strictEqual(r.body.readUInt16BE(0), 0xffd8, 'a JPEG');
++ assert.match(r.headers['cache-control'], /immutable/);
++ assert.ok(r.headers['cdn-cache-control'], 'the CDN keeps it too');
++ const plain = await get('/shared/social/live.jpg');
++ assert.strictEqual(plain.status, 200);
++ assert.ok(!/immutable/.test(plain.headers['cache-control']), 'no hash: revalidated');
++ assert.ok(!/immutable/.test((await get('/shared/social/live.jpg?v=000000000000')).headers['cache-control']), 'a wrong hash is not immutable');
++ });
++
++ await check('what is not a card falls through', async () => {
++ assert.strictEqual((await get('/shared/social/nope.jpg')).status, 404);
++ assert.strictEqual((await get('/shared/social/..%2Fpackage.json')).status, 404);
++ assert.strictEqual((await get('/shared/social/live.png')).status, 404);
++ });
++
++ await check('pages name the cards: the site card by default, page cards on the big pages', () => {
++ const src = fs.readFileSync(path.join(__dirname, '..', 'server', 'seo', 'pages.js'), 'utf8');
++ assert.match(src, /const DEFAULT_OG_IMAGE = shareCard\('live'\);/);
++ assert.match(src, /const base = \{ image: shareCard\('live-rundown'\)/, 'The Rundown');
++ assert.match(src, /canonicalPath: '\/chat', image: shareCard\('live-chat'\)/, 'Chat');
++ assert.match(src, /LIST_CARDS = \{ content: 'live-content', vods: 'live-content', clips: 'live-content', pastes: 'live-content', moments: 'live-moments' \}/);
++ for (const f of ['index.html', 'tos.html', 'privacy.html', 'dmca.html']) {
++ const html = fs.readFileSync(path.join(__dirname, '..', 'public', f), 'utf8');
++ assert.ok(!html.includes('og-image.png'), `${f} still names /og-image.png`);
++ assert.ok(html.includes('content="https://openvibe.live/shared/social/live.jpg"'), f);
++ }
++ });
++
++ server.close();
++ console.log(failures ? `\n${failures} check(s) failed` : '\nshare cards: all checks passed');
++ if (failures) process.exitCode = 1;
++})();