openvibers/openvibe.media Commits

Media: JSON-LD on the media index (WebSite, ItemList) (#92)

OP OpenVibers committed 12 Oct 2026 · 0afbfa312d · 2 files, +20 −0

The media index's first page (`/`, and `/?tab=…`) now carries JSON-LD: `WebSite` on the all-media page, and an `ItemList` of the cards shown, with absolute URLs. Later pages are noindex and carry none. The network SEO survey found openvibe.media without any.

Test: `public-pages.test.js` checks the WebSite and ItemList (absolute URLs) on `/`, and none on `/?page=2`.

Changes

ddiff --git a/server/public/browse.js b/server/public/browse.js
iindex e518ca6..1d6c3be 100644
---- a/server/public/browse.js
++++ b/server/public/browse.js
@@@ -189,6 +189,17 @@ async function tabCounts() {
// ── Rendering ────────────────────────────────────────────────
const KIND_ICON = { video: '🎬', clip: '✂️', thumbnail: '🏞️', file: '📦' };
++/** JSON-LD for the index's first page: the site, and the cards it lists (ItemList). Later pages are noindex. */
++function browseJsonLd(config, tab, cards) {
++ const seo = require('openvibe-shared/seo');
++ const abs = (u) => { try { return new URL(u, `${config.publicUrl}/`).toString(); } catch { return null; } };
++ const items = cards.map((c) => ({ name: c.title || 'Untitled', url: abs(c.view) })).filter((it) => it.url);
++ return [
++ tab === 'all' ? seo.jsonLdTag(seo.jsonLd.website({ name: 'OpenVibe.Media', url: `${config.publicUrl}/`, description: 'Every public file on the OpenVibe network: videos, clips, files and thumbnails.' })) : '',
++ items.length ? seo.jsonLdTag(seo.jsonLd.itemList(tab === 'all' ? 'Latest public media' : `Latest public ${tab}`, items, { url: `${config.publicUrl}/${tab === 'all' ? '' : `?tab=${tab}`}` })) : '',
++ ].filter(Boolean).join('\n');
++}
++
function renderCard(c) {
const preview = c.thumb
? `<img src="${esc(c.thumb)}" alt="" loading="lazy">`
@@@ -229,6 +240,7 @@ function renderPage(tab, page, data, counts) {
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="OpenVibe.Media — Media Index">
<meta name="twitter:image" content="${config.publicUrl}/og-image.png">
++${page === 1 ? browseJsonLd(config, tab, data.cards) : ''}
<!-- The shared theme-loader applies the user's theme to <html> before anything paints. -->
<script src="${require('openvibe-shared/serve').url('theme-loader.js')}" defer></script>
<style>
ddiff --git a/test/public-pages.test.js b/test/public-pages.test.js
iindex d8ae60d..ae3b358 100644
---- a/test/public-pages.test.js
++++ b/test/public-pages.test.js
@@@ -101,6 +101,14 @@ const http = require('http');
assert.strictEqual(r.status, 404, 'a <video> element never gets HTML');
r = await get('/p/slug1', nav);
assert.deepStrictEqual([r.status, r.headers.location], [301, 'https://openvibe.community/p/slug1'], 'the paste page is a redirect, no row needed');
++ r = await get('/', nav);
++ assert.strictEqual(r.status, 200);
++ const ld = [...r.body.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
++ assert.ok(ld.some((x) => x['@type'] === 'WebSite'), 'the index names the site');
++ const list = ld.find((x) => x['@type'] === 'ItemList');
++ assert.ok(list && list.itemListElement.length >= 1 && list.itemListElement.every((it) => /^https?:\/\//.test(it.url)), 'the listed cards, with absolute URLs');
++ r = await get('/?page=2', nav);
++ assert.ok(!r.body.includes('application/ld+json'), 'a later (noindex) page carries none');
r = await get('/og-image.png', {});
assert.strictEqual(r.status, 200);
assert.strictEqual(r.headers['content-type'], 'image/png');