openvibers/openvibe.report Commits
Mark Report's tables as an open Dataset in JSON-LD (#147)
Changes
@@@ -22,6 +22,23 @@ const rooms = require('openvibe-shared/rooms');const { asset } = require('../assets');const NAME = 'OpenVibe.Report';++++// The tables are an open dataset, not only a page: Dataset JSON-LD names the numbers and the JSON that serves them, so++// dataset search and AI retrieval can cite Report's tables instead of re-deriving them from prose.++function networkDataset() {++ return seo.jsonLd.dataset({++ name: 'OpenVibe network numbers',++ url: 'https://openvibe.report/stats',++ description: 'Page views, visitors, live streams, chat, OpenCoins and service speed across every OpenVibe site, counted by the sites themselves and published as open JSON.',++ creator: 'OpenVibe',++ keywords: ['OpenVibe', 'web analytics', 'live streaming', 'page views', 'service status', 'open data'],++ distribution: [++ { '@type': 'DataDownload', name: 'Network numbers', contentUrl: 'https://openvibe.report/api/v1/stats', encodingFormat: 'application/json' },++ { '@type': 'DataDownload', name: 'AI results and cost per task', contentUrl: 'https://openvibe.report/api/v1/ai/results', encodingFormat: 'application/json' },++ { '@type': 'DataDownload', name: 'The Rundown topics', contentUrl: 'https://openvibe.report/api/v1/rundown/topics', encodingFormat: 'application/json' },++ ],++ });++}const NETWORK = 'https://openvibe.network';const LIVE = 'https://openvibe.live';const BENCH = 'https://bench.openvibe.services';@@@ -273,7 +290,7 @@ function home({ stats, rundown, topics } = {}) {out = foldAround(out, '>Questions</h2>', 'faq', '<h2 id="rp-faq-h">Questions</h2>');return layout({title: null, body: out, active: '/', path: '/', agent: true,-- jsonLd: [seo.jsonLd.website({ name: 'OpenVibe.Report', url: 'https://openvibe.report/', description: "The network's numbers open to everyone, what streamers talked about in The Rundown, and every stream's after-show report." }), seo.jsonLd.organization()],++ jsonLd: [seo.jsonLd.website({ name: 'OpenVibe.Report', url: 'https://openvibe.report/', description: "The network's numbers open to everyone, what streamers talked about in The Rundown, and every stream's after-show report." }), seo.jsonLd.organization(), networkDataset()],// The rule that shows the picked topic of the pick list: one per topic, written for this page.});}@@@ -302,7 +319,7 @@ ${sites ? `<section aria-labelledby="rp-sites-h"><h2 id="rp-sites-h">Every site<${L ? `<section aria-labelledby="rp-live-h"><h2 id="rp-live-h">OpenVibe.Live</h2><div class="rp-minis">${[['Streamers', L.streamers], ['Streams', L.streams], ['Hours watched', L.hours_watched], ['Chat messages', L.messages], ['Recordings', L.vods], ['Clips', L.clips], ['AI moments', L.ai_moments]].filter(([, v]) => n(v)).map(([l, v]) => `<div><b>${fmt(v)}</b><span>${esc(l)}</span></div>`).join('')}</div></section>` : ''}${A && A.daily && A.daily.length > 1 ? `<section aria-labelledby="rp-acc-h"><h2 id="rp-acc-h">New accounts a day</h2>${bars(A.daily, 'New accounts a day', { h: 110 })}</section>` : ''}<section aria-labelledby="rp-how-h"><h2 id="rp-how-h">How these are counted</h2><p class="rp-fine">${esc(d.note || '')} The same numbers: <a href="/api/v1/stats?days=${esc(days)}"><code>/api/v1/stats</code></a>. How closely each site follows the design kit, read from its code: <a href="/design">Design health</a>.</p></section>`;-- return layout({ title: `Every site's numbers, ${days} days`, description: 'Page views, people, streams and how fast each OpenVibe service answers, open to everyone.', body, active: '/stats', path: `/stats${Number(days) === 30 ? '' : `?days=${Number(days)}`}`, agent: true });++ return layout({ title: `Every site's numbers, ${days} days`, description: 'Page views, people, streams and how fast each OpenVibe service answers, open to everyone.', body, active: '/stats', path: `/stats${Number(days) === 30 ? '' : `?days=${Number(days)}`}`, agent: true, jsonLd: [networkDataset()] });}function rundown({ rundown: R = {}, topics, reports } = {}) {@@@ -70,6 +70,19 @@ const { boot, check, done, defaults } = require('./helpers/boot');assert.ok(org && Array.isArray(org.sameAs) && org.sameAs.includes('https://github.com/OpenVibers'), 'Organization');});++ await check('the home and the stats page carry Dataset JSON-LD with the JSON endpoints', async () => {++ for (const path of ['/', '/stats']) {++ const r = await t.get(path);++ const ld = [...r.text.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1])).flatMap((b) => b['@graph'] || [b]);++ const dataset = ld.find((x) => x['@type'] === 'Dataset');++ assert.ok(dataset, `${path}: Dataset`);++ assert.strictEqual(dataset.url, 'https://openvibe.report/stats', `${path}: Dataset url`);++ const urls = (dataset.distribution || []).map((d) => d.contentUrl);++ assert.ok(urls.includes('https://openvibe.report/api/v1/stats'), `${path}: stats JSON`);++ assert.ok(urls.includes('https://openvibe.report/api/v1/ai/results'), `${path}: AI JSON`);++ }++ });++await check('The Rundown pages name OpenVibe.Live as canonical and mount the Frame for service report', async () => {const r = await t.get('/rundown/t/seattle');assert.ok(r.text.includes('<link rel="canonical" href="https://openvibe.live/rundown/t/seattle">'), 'the topic is Live\'s page');
@@ -22,6 +22,23 @@ const rooms = require('openvibe-shared/rooms');
@@ -22,6 +22,23 @@ const rooms = require('openvibe-shared/rooms');
const { asset } = require('../assets');
const { asset } = require('../assets');
const NAME = 'OpenVibe.Report';
const NAME = 'OpenVibe.Report';
+
+// The tables are an open dataset, not only a page: Dataset JSON-LD names the numbers and the JSON that serves them, so
+// dataset search and AI retrieval can cite Report's tables instead of re-deriving them from prose.
+function networkDataset() {
+ return seo.jsonLd.dataset({
+ name: 'OpenVibe network numbers',
+ url: 'https://openvibe.report/stats',
+ description: 'Page views, visitors, live streams, chat, OpenCoins and service speed across every OpenVibe site, counted by the sites themselves and published as open JSON.',
+ creator: 'OpenVibe',
+ keywords: ['OpenVibe', 'web analytics', 'live streaming', 'page views', 'service status', 'open data'],
+ distribution: [
+ { '@type': 'DataDownload', name: 'Network numbers', contentUrl: 'https://openvibe.report/api/v1/stats', encodingFormat: 'application/json' },
+ { '@type': 'DataDownload', name: 'AI results and cost per task', contentUrl: 'https://openvibe.report/api/v1/ai/results', encodingFormat: 'application/json' },
+ { '@type': 'DataDownload', name: 'The Rundown topics', contentUrl: 'https://openvibe.report/api/v1/rundown/topics', encodingFormat: 'application/json' },
+ ],
+ });
+}
const NETWORK = 'https://openvibe.network';
const NETWORK = 'https://openvibe.network';
const LIVE = 'https://openvibe.live';
const LIVE = 'https://openvibe.live';
const BENCH = 'https://bench.openvibe.services';
const BENCH = 'https://bench.openvibe.services';
@@ -273,7 +290,7 @@ function home({ stats, rundown, topics } = {}) {
@@ -273,7 +290,7 @@ function home({ stats, rundown, topics } = {}) {
out = foldAround(out, '>Questions</h2>', 'faq', '<h2 id="rp-faq-h">Questions</h2>');
out = foldAround(out, '>Questions</h2>', 'faq', '<h2 id="rp-faq-h">Questions</h2>');
return layout({
return layout({
title: null, body: out, active: '/', path: '/', agent: true,
title: null, body: out, active: '/', path: '/', agent: true,
- jsonLd: [seo.jsonLd.website({ name: 'OpenVibe.Report', url: 'https://openvibe.report/', description: "The network's numbers open to everyone, what streamers talked about in The Rundown, and every stream's after-show report." }), seo.jsonLd.organization()],
+ jsonLd: [seo.jsonLd.website({ name: 'OpenVibe.Report', url: 'https://openvibe.report/', description: "The network's numbers open to everyone, what streamers talked about in The Rundown, and every stream's after-show report." }), seo.jsonLd.organization(), networkDataset()],
// The rule that shows the picked topic of the pick list: one per topic, written for this page.
// The rule that shows the picked topic of the pick list: one per topic, written for this page.
});
});
}
}
@@ -302,7 +319,7 @@ ${sites ? `<section aria-labelledby="rp-sites-h"><h2 id="rp-sites-h">Every site<
@@ -302,7 +319,7 @@ ${sites ? `<section aria-labelledby="rp-sites-h"><h2 id="rp-sites-h">Every site<
${L ? `<section aria-labelledby="rp-live-h"><h2 id="rp-live-h">OpenVibe.Live</h2><div class="rp-minis">${[['Streamers', L.streamers], ['Streams', L.streams], ['Hours watched', L.hours_watched], ['Chat messages', L.messages], ['Recordings', L.vods], ['Clips', L.clips], ['AI moments', L.ai_moments]].filter(([, v]) => n(v)).map(([l, v]) => `<div><b>${fmt(v)}</b><span>${esc(l)}</span></div>`).join('')}</div></section>` : ''}
${L ? `<section aria-labelledby="rp-live-h"><h2 id="rp-live-h">OpenVibe.Live</h2><div class="rp-minis">${[['Streamers', L.streamers], ['Streams', L.streams], ['Hours watched', L.hours_watched], ['Chat messages', L.messages], ['Recordings', L.vods], ['Clips', L.clips], ['AI moments', L.ai_moments]].filter(([, v]) => n(v)).map(([l, v]) => `<div><b>${fmt(v)}</b><span>${esc(l)}</span></div>`).join('')}</div></section>` : ''}
${A && A.daily && A.daily.length > 1 ? `<section aria-labelledby="rp-acc-h"><h2 id="rp-acc-h">New accounts a day</h2>${bars(A.daily, 'New accounts a day', { h: 110 })}</section>` : ''}
${A && A.daily && A.daily.length > 1 ? `<section aria-labelledby="rp-acc-h"><h2 id="rp-acc-h">New accounts a day</h2>${bars(A.daily, 'New accounts a day', { h: 110 })}</section>` : ''}
<section aria-labelledby="rp-how-h"><h2 id="rp-how-h">How these are counted</h2><p class="rp-fine">${esc(d.note || '')} The same numbers: <a href="/api/v1/stats?days=${esc(days)}"><code>/api/v1/stats</code></a>. How closely each site follows the design kit, read from its code: <a href="/design">Design health</a>.</p></section>`;
<section aria-labelledby="rp-how-h"><h2 id="rp-how-h">How these are counted</h2><p class="rp-fine">${esc(d.note || '')} The same numbers: <a href="/api/v1/stats?days=${esc(days)}"><code>/api/v1/stats</code></a>. How closely each site follows the design kit, read from its code: <a href="/design">Design health</a>.</p></section>`;
- return layout({ title: `Every site's numbers, ${days} days`, description: 'Page views, people, streams and how fast each OpenVibe service answers, open to everyone.', body, active: '/stats', path: `/stats${Number(days) === 30 ? '' : `?days=${Number(days)}`}`, agent: true });
+ return layout({ title: `Every site's numbers, ${days} days`, description: 'Page views, people, streams and how fast each OpenVibe service answers, open to everyone.', body, active: '/stats', path: `/stats${Number(days) === 30 ? '' : `?days=${Number(days)}`}`, agent: true, jsonLd: [networkDataset()] });
}
}
function rundown({ rundown: R = {}, topics, reports } = {}) {
function rundown({ rundown: R = {}, topics, reports } = {}) {
@@ -70,6 +70,19 @@ const { boot, check, done, defaults } = require('./helpers/boot');
@@ -70,6 +70,19 @@ const { boot, check, done, defaults } = require('./helpers/boot');
assert.ok(org && Array.isArray(org.sameAs) && org.sameAs.includes('https://github.com/OpenVibers'), 'Organization');
assert.ok(org && Array.isArray(org.sameAs) && org.sameAs.includes('https://github.com/OpenVibers'), 'Organization');
});
});
+ await check('the home and the stats page carry Dataset JSON-LD with the JSON endpoints', async () => {
+ for (const path of ['/', '/stats']) {
+ const r = await t.get(path);
+ const ld = [...r.text.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1])).flatMap((b) => b['@graph'] || [b]);
+ const dataset = ld.find((x) => x['@type'] === 'Dataset');
+ assert.ok(dataset, `${path}: Dataset`);
+ assert.strictEqual(dataset.url, 'https://openvibe.report/stats', `${path}: Dataset url`);
+ const urls = (dataset.distribution || []).map((d) => d.contentUrl);
+ assert.ok(urls.includes('https://openvibe.report/api/v1/stats'), `${path}: stats JSON`);
+ assert.ok(urls.includes('https://openvibe.report/api/v1/ai/results'), `${path}: AI JSON`);
+ }
+ });
+
await check('The Rundown pages name OpenVibe.Live as canonical and mount the Frame for service report', async () => {
await check('The Rundown pages name OpenVibe.Live as canonical and mount the Frame for service report', async () => {
const r = await t.get('/rundown/t/seattle');
const r = await t.get('/rundown/t/seattle');
assert.ok(r.text.includes('<link rel="canonical" href="https://openvibe.live/rundown/t/seattle">'), 'the topic is Live\'s page');
assert.ok(r.text.includes('<link rel="canonical" href="https://openvibe.live/rundown/t/seattle">'), 'the topic is Live\'s page');