openvibers/openvibe.live Commits

A live channel's shared link shows the stream as it is now and says "Live now" (it showed the avatar); the person's structured data keeps the avatar and bio (#274)

OP OpenVibers committed 24 days ago · acd0eafc1f · 2 files, +23 −4

Changes

ddiff --git a/server/seo/pages.js b/server/seo/pages.js
iindex ad796af0..0337562f 100644
---- a/server/seo/pages.js
++++ b/server/seo/pages.js
@@@ -286,12 +286,17 @@ async function _channelMeta(username, page = 1) {
const canonicalPath = page > 1 && !pastEnd ? `${basePath}?page=${page}` : basePath;
const pageLabel = page > 1 ? ` — videos, page ${page}` : '';
-- const desc = clean(bio || aiShort || `${name} (${handle}) streams live on ${SITE_NAME}. Watch their live streams, VODs and clips.`, 200);
-- const image = acct.avatarUrl ? abs(acct.avatarUrl) : DEFAULT_OG_IMAGE;
++ const about = clean(bio || aiShort || `${name} (${handle}) streams live on ${SITE_NAME}. Watch their live streams, VODs and clips.`, 200);
++ const avatar = acct.avatarUrl ? abs(acct.avatarUrl) : null;
++ // A live channel's shared link shows the stream as it is now (16:9) and says so; offline, the avatar. The page is
++ // kept a minute (CHANNEL_CACHE_MS), so the card follows the stream going live or ending.
++ const liveNow = live.length ? live[0] : null;
++ const image = (liveNow && liveNow.thumbnail_url ? abs(liveNow.thumbnail_url) : null) || avatar || DEFAULT_OG_IMAGE;
++ const desc = liveNow ? clean(`Live now: ${liveNow.title || `${name} is streaming`}. ${about}`, 200) : about;
const person = {
'@type': 'Person', name: clean(name, 80), alternateName: handle, url: abs(basePath),
-- image: image !== DEFAULT_OG_IMAGE ? image : undefined,
-- description: desc,
++ image: avatar || undefined,
++ description: about,
interactionStatistic: followers ? { '@type': 'InteractionCounter', interactionType: 'https://schema.org/FollowAction', userInteractionCount: followers } : undefined,
};
const profile = { ...seo.jsonLd.webPage({ type: 'ProfilePage', name: `${name} (${handle})`, url: abs(canonicalPath) }), mainEntity: person };
ddiff --git a/test/seo-ssr.test.js b/test/seo-ssr.test.js
iindex eb4810c2..a752d49d 100644
---- a/test/seo-ssr.test.js
++++ b/test/seo-ssr.test.js
@@@ -244,6 +244,20 @@ const server = http.createServer(app).listen(0, '127.0.0.1', async () => {
}
});
++ await check('a live channel\'s shared link shows the stream as it is now and says so; the person keeps their avatar', async () => {
++ await addUser(6, 'carol', 'Carol', 'I paint.');
++ await db.run("UPDATE users SET avatar_url = 'https://openvibe.media/avatars/carol.png' WHERE id = 6");
++ await db.run("INSERT INTO streams (user_id, title, protocol, is_live, thumbnail_url, started_at, last_heartbeat) VALUES (6, 'Painting a sunset', 'webrtc', 1, '/api/thumbnails/stream-9-1.jpg', ov_now(), ov_now())");
++ const html = (await get('/@carol')).body;
++ assert.ok(head(html).includes('<meta property="og:image" content="https://openvibe.live/api/thumbnails/stream-9-1.jpg">'), 'the live picture');
++ assert.match(head(html), /<meta property="og:description" content="Live now: Painting a sunset\. I paint\.">/);
++ const person = ldOf(html).flatMap((n) => (n['@graph'] || [n])).find((n) => n['@type'] === 'Person' || (n.mainEntity && n.mainEntity['@type'] === 'Person'));
++ const p = person && (person['@type'] === 'Person' ? person : person.mainEntity);
++ assert.ok(p, 'a Person node');
++ assert.strictEqual(p.image, 'https://openvibe.media/avatars/carol.png', 'the person is still pictured by their avatar');
++ assert.strictEqual(p.description, 'I paint.', 'and described by their bio');
++ });
++
await check('the home page\'s JSON-LD lists are named by the snapshot\'s visible headings', async () => {
const home = (await get('/')).body;
const lists = ldOf(home).filter((n) => n['@type'] === 'ItemList');