openvibers/openvibe.report Commits
The Rundown canonicalises to OpenVibe.Live; its pages leave Report's sitemap (#124)
Changes
@@@ -77,10 +77,10 @@ All server-rendered in `server/views/index.js`; every dynamic value goes through|---|---|| `/` | the 7-day open numbers, the Rundown's counts and lead, and the trending topics || `/stats?days=7\|30\|90` | the network totals and a first look at every site (anything else is 30) |--| `/rundown` | on air, the lead, the latest streams and every topic |--| `/rundown/t/:slug` | one topic, its takes and related topics |--| `/rundown/e/:streamId` | one stream as an episode, with its segments |--| `/rundown/s/:username` | one streamer's episodes and topics |++| `/rundown` | on air, the lead, the latest streams and every topic; canonical is OpenVibe.Live's `/rundown` |++| `/rundown/t/:slug` | one topic, its takes and related topics; canonical is Live's `/rundown/t/:slug` |++| `/rundown/e/:streamId` | one stream as an episode, with its segments; canonical is Live's `/rundown/e/:streamId` |++| `/rundown/s/:username` | one streamer's episodes and topics; canonical is Live's `/rundown/:username` || `/after-show/:streamId` | one after-show report || `/c/:username` | one channel's latest after-show reports || `/ai` | benchmark kinds grouped by room, with leaders and game counts |@@@ -16,7 +16,6 @@ function createAgentPages({ publicPages, bench }) {if (path === '/') page = await publicPages.home();else if (path === '/developers') page = publicPages.developers();else if (path === '/design') page = publicPages.design();-- else if (path === '/rundown') page = await publicPages.rundown();else if (path === '/stats') page = await publicPages.stats(normaliseDays(query && query.get('days')));else if (path === '/ai') page = await publicPages.aiIndex();else if (model) page = await publicPages.aiModel(decodeURIComponent(model[1]));@@@ -7,8 +7,8 @@* GET /sitemap.xml /, /stats, /design, /rundown and every /rundown/t/<slug> from the Rundown's topics* GET /llms.txt a markdown map of the site and its machine-readable answers*-- * The sitemap reads topics through the same cached client as the pages, so a crawler is no heavier than a reader-- * and a source that does not answer leaves the three static pages in place.++ * The sitemap lists only the pages this site owns; The Rundown pages carry rel=canonical to openvibe.live and are left++ * out, so the two sites never present the same page as two canonical URLs.*/const fs = require('fs');const path = require('path');@@@ -46,25 +46,20 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null }) {});r.get('/sitemap.xml', async (_req, res) => {-- const [data, ai, reports] = await Promise.all([live.topics(), bench.kinds(), sessionReports ? sessionReports.listPublic({ limit: 500 }) : []]);-- const topics = (data && Array.isArray(data.topics)) ? data.topics : [];++ const [ai, reports] = await Promise.all([bench.kinds(), sessionReports ? sessionReports.listPublic({ limit: 500 }) : []]);const kinds = (ai && Array.isArray(ai.data.kinds)) ? ai.data.kinds : [];const lastmod = siteUpdated();++ // Only the pages this site owns. /rundown and its topic, episode and speaker pages carry rel=canonical to++ // OpenVibe.Live's Rundown, so listing them here would advertise a duplicate.const pages = [{ loc: abs('/'), changefreq: 'daily', priority: 1.0 },{ loc: abs('/stats'), changefreq: 'daily', priority: 0.9 },{ loc: abs('/design'), changefreq: 'weekly', priority: 0.6 },-- { loc: abs('/rundown'), changefreq: 'daily', priority: 0.9 },{ loc: abs('/ai'), changefreq: 'daily', priority: 0.9 },...reports.map((rep) => ({ loc: abs('/s/' + encodeURIComponent(rep.id)), changefreq: 'daily', priority: 0.6 })),...kinds.filter((k) => k && bench.isKindId(k.id)).map((k) => ({loc: abs(`/ai/${encodeURIComponent(k.id)}`), changefreq: 'daily', priority: 0.7,})),-- ...topics.filter((t) => t && typeof t.slug === 'string').map((t) => ({-- loc: abs(`/rundown/t/${encodeURIComponent(t.slug)}`),-- changefreq: 'weekly',-- priority: 0.7,-- })),].map((p) => ({ ...p, ...(lastmod ? { lastmod } : {}) }));res.type('application/xml').set('Cache-Control', CACHE).send(seo.sitemapXml(pages));});@@@ -81,7 +76,7 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null }) {{ title: 'OpenVibe.Report', url: abs('/'), note: 'the Report room' },{ title: 'Open numbers', url: abs('/stats'), note: 'page views, accounts and traffic for every OpenVibe site' },{ title: 'Design health', url: abs('/design'), note: 'how closely each site follows the shared design kit, read from its code; per site at /design/<site>' },-- { title: 'The Rundown', url: abs('/rundown'), note: 'what streamers talked about, topic by topic' },++ { title: 'The Rundown', url: 'https://openvibe.live/rundown', note: 'what streamers talked about, topic by topic (OpenVibe.Live is its canonical home)' },{ title: 'AI boards', url: abs('/ai'), note: 'Bench ratings, quality and cost across rooms' },] },// A JSON API is what an agent came for, not something to skip (AGT-5 review).@@@ -87,6 +87,15 @@ function createPageRoutes(ctx) {const { live, bench, sessionReports } = ctx;const r = express.Router();const publicPages = ctx.publicPages || createPublicPages(ctx);++ // The Rundown's pages canonicalise to OpenVibe.Live, and so does its Markdown twin: the generic agent layer++ // (server/agent-pages.js) would sign the twin with this site's own address. Answer it here instead, before the++ // .md hand-off, with Live's canonical.++ r.get('/rundown.md', async (_req, res) => {++ const { markdownTwin } = require('openvibe-shared/agent-layer');++ const page = await publicPages.rundown();++ const body = markdownTwin({ html: page.html, canonical: 'https://openvibe.live/rundown' });++ res.set('Content-Type', 'text/markdown; charset=utf-8').set('Cache-Control', CACHE).send(body);++ });// A path ending in .md is a Markdown twin (server/agent-pages.js), never a page parameter.r.use((req, res, next) => (req.path.endsWith('.md') ? next('router') : next()));@@@ -23,6 +23,7 @@ const { asset } = require('../assets');const NAME = 'OpenVibe.Report';const NETWORK = 'https://openvibe.network';++const LIVE = 'https://openvibe.live';const BENCH = 'https://bench.openvibe.services';const ROOM = rooms.get('report');const esc = kit.esc;@@@ -317,7 +318,7 @@ ${all.length ? `<section aria-labelledby="rp-t-h"><h2 id="rp-t-h">Topics</h2><ul${voices.length ? `<section aria-labelledby="rp-v-h"><h2 id="rp-v-h">Voices</h2><ul class="rp-voices">${voices.map((v) => `<li><a href="/rundown/s/${encodeURIComponent((v.user || {}).username || '')}">${kit.mark({ name: who(v.user), src: v.user && v.user.avatar_url, size: 40 })}<span><b>${esc(who(v.user))}</b><small>${[n(v.topics) ? `${exact(v.topics)} topic${Number(v.topics) === 1 ? '' : 's'}` : '', n(v.segments) ? `${exact(v.segments)} take${Number(v.segments) === 1 ? '' : 's'}` : ''].filter(Boolean).join(' · ')}</small></span></a></li>`).join('')}</ul></section>` : ''}${streams.length ? kit.rail({ id: 'episodes', title: 'Recent streams', items: streams }) : ''}${reports && reports.length ? reportsSection(reports) : ''}`;-- return layout({ title: 'The Rundown: what streamers talked about', description: 'Topics, takes and crossovers from what streamers said on OpenVibe, written up by AI from the streams themselves.', body, active: '/rundown', path: '/rundown' });++ return layout({ title: 'The Rundown: what streamers talked about', description: 'Topics, takes and crossovers from what streamers said on OpenVibe, written up by AI from the streams themselves.', body, active: '/rundown', path: '/rundown', canonical: `${LIVE}/rundown` });}@@@ -462,7 +463,7 @@ ${t.headline ? `<p class="rp-lede">${esc(t.headline)}</p>` : ''}${t.summary ? `<${takes.length ? `<section aria-labelledby="rp-tk-h"><h2 id="rp-tk-h">Takes</h2><ol class="rp-takes">${takes.map(takeRow).join('')}</ol></section>` : ''}${citesHTML}${(d.related || []).length ? `<section aria-labelledby="rp-rel-h"><h2 id="rp-rel-h">Related</h2><ul class="rp-chips">${d.related.slice(0, 12).map((r) => `<li><a href="/rundown/t/${encodeURIComponent(r.slug)}">${esc(r.title)}</a></li>`).join('')}</ul></section>` : ''}`;-- return layout({ title: `${t.title}: The Rundown`, description: t.headline || `What streamers said about ${t.title}.`, body, active: '/rundown', path: `/rundown/t/${encodeURIComponent(t.slug || '')}` });++ return layout({ title: `${t.title}: The Rundown`, description: t.headline || `What streamers said about ${t.title}.`, body, active: '/rundown', path: `/rundown/t/${encodeURIComponent(t.slug || '')}`, canonical: `${LIVE}/rundown/t/${encodeURIComponent(t.slug || '')}` });}/** A chapter's moment in the recording on OpenVibe.Video (the same link Live's own Rundown draws). */@@@ -489,17 +490,17 @@ function episode({ episode: d, citations = [] } = {}) {${chaptersHTML}${segs.length ? `<section aria-labelledby="rp-sg-h"><h2 id="rp-sg-h">What happened</h2><ol class="rp-takes rp-timeline">${segs.map(takeRow).join('')}</ol></section>` : ''}${(d.more || []).length ? `<section aria-labelledby="rp-mo-h"><h2 id="rp-mo-h">More from ${esc(who(s.user) || 'this streamer')}</h2><ul class="rp-chips">${d.more.slice(0, 8).map((m) => `<li><a href="/rundown/e/${encodeURIComponent(m.stream_id)}">${esc(m.title)}</a></li>`).join('')}</ul></section>` : ''}`;-- return layout({ title: `${e.title || s.title}: The Rundown`, description: e.logline || '', body, active: '/rundown', path: `/rundown/e/${encodeURIComponent(s.id || e.stream_id || '')}` });++ return layout({ title: `${e.title || s.title}: The Rundown`, description: e.logline || '', body, active: '/rundown', path: `/rundown/e/${encodeURIComponent(s.id || e.stream_id || '')}`, canonical: `${LIVE}/rundown/e/${encodeURIComponent(s.id || e.stream_id || '')}` });}function speaker({ speaker: d, username } = {}) {const u = d.user || { username };-- if (d.opted_out) return layout({ title: `${who(u)}: The Rundown`, robots: 'noindex, follow', body: `<header class="rp-page-h"><h1>${esc(who(u))}</h1><p class="rp-lede">${esc(who(u))} has opted out of The Rundown: nothing they say on stream is written up here.</p></header>`, active: '/rundown' });++ if (d.opted_out) return layout({ title: `${who(u)}: The Rundown`, robots: 'noindex, follow', body: `<header class="rp-page-h"><h1>${esc(who(u))}</h1><p class="rp-lede">${esc(who(u))} has opted out of The Rundown: nothing they say on stream is written up here.</p></header>`, active: '/rundown', canonical: `${LIVE}/rundown/${encodeURIComponent(u.username || username || '')}` });const body = `<header class="rp-page-h rp-person">${kit.mark({ name: who(u), src: u.avatar_url, size: 64 })}<div><p class="rp-kicker">The Rundown · voice</p><h1>${esc(who(u))}</h1><p class="rp-meta"><a href="https://openvibe.live/@${encodeURIComponent(u.username || username)}">Channel on OpenVibe.Live</a> · <a href="/c/${encodeURIComponent(u.username || username)}">After-show reports</a></p></div></header>${(d.topics || []).length ? `<section aria-labelledby="rp-st-h"><h2 id="rp-st-h">Topics</h2><ul class="rp-topics">${d.topics.slice(0, 12).map(topicCard).join('')}</ul></section>` : ''}${(d.lines || []).length ? `<section aria-labelledby="rp-sl-h"><h2 id="rp-sl-h">Takes</h2><ol class="rp-takes">${d.lines.slice(0, 20).map(takeRow).join('')}</ol><p class="rp-meta">${AI}</p></section>` : ''}${(d.episodes || []).length ? `<section aria-labelledby="rp-se-h"><h2 id="rp-se-h">Episodes</h2><ul class="rp-chips">${d.episodes.slice(0, 12).map((m) => `<li><a href="/rundown/e/${encodeURIComponent(m.stream_id)}">${esc(m.title)}</a></li>`).join('')}</ul></section>` : ''}`;-- return layout({ title: `${who(u)}: The Rundown`, description: `What ${who(u)} talked about on stream, topic by topic.`, body, active: '/rundown', path: `/rundown/s/${encodeURIComponent(u.username || username || '')}` });++ return layout({ title: `${who(u)}: The Rundown`, description: `What ${who(u)} talked about on stream, topic by topic.`, body, active: '/rundown', path: `/rundown/s/${encodeURIComponent(u.username || username || '')}`, canonical: `${LIVE}/rundown/${encodeURIComponent(u.username || username || '')}` });}function recap({ recap: d } = {}) {@@@ -1,9 +1,9 @@'use strict';/**-- * The crawl artifacts: robots.txt allows the public pages and disallows the API; sitemap.xml is the home, /stats,-- * /rundown and every topic from the Rundown; llms.txt names the site and its machine-readable answers. All three-- * are public for an hour.++ * The crawl artifacts: robots.txt allows the public pages and disallows the API; sitemap.xml is the pages this site++ * owns (not The Rundown, which canonicalises to OpenVibe.Live); llms.txt names the site and its machine-readable++ * answers. All three are public for an hour.*/const assert = require('assert');const fs = require('fs');@@@ -26,14 +26,17 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'STfor (const p of ['/stats', '/rundown']) assert.ok(!r.text.includes(`Disallow: ${p}`), `${p} must stay crawlable`);});-- await check('sitemap.xml: the home, /stats, /rundown and every topic, with lastmod from STATUS.json', async () => {++ await check('sitemap.xml: the site\'s own pages, never The Rundown (Live is its canonical), with lastmod from STATUS.json', async () => {const r = await t.get('/sitemap.xml');assert.strictEqual(r.status, 200);assert.match(r.headers.get('content-type') || '', /^application\/xml/);assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600');-- for (const p of ['/', '/stats', '/rundown', '/rundown/t/seattle', '/rundown/t/ai-progress']) {++ for (const p of ['/', '/stats', '/design', '/ai']) {assert.ok(r.text.includes(`<loc>https://openvibe.report${p}</loc>`), `no ${p} entry`);}++ for (const p of ['/rundown', '/rundown/t/seattle', '/rundown/t/ai-progress']) {++ assert.ok(!r.text.includes(`<loc>https://openvibe.report${p}</loc>`), `${p} must not be listed: Live is its canonical`);++ }assert.ok(!r.text.includes('<loc>https://openvibe.report/api/'), 'the API must not be in the sitemap');const lastmods = [...r.text.matchAll(/<lastmod>([^<]+)<\/lastmod>/g)].map((m) => m[1]);assert.ok(lastmods.length > 0, 'no lastmod');@@@ -49,19 +52,22 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'STfor (const link of ['https://openvibe.report/index.md','https://openvibe.report/stats.md',-- 'https://openvibe.report/rundown.md',++ 'https://openvibe.live/rundown','https://openvibe.report/design.md','https://openvibe.report/api/v1/stats?days=7','https://openvibe.report/sitemap.xml',]) assert.ok(r.text.includes(`(${link})`), `missing link ${link}`);assert.ok(r.text.includes('\n## Optional\n'), 'llms.txt has an Optional section');-- // 11-03: /design and /rundown have the twins llms.txt links, and their pages name them.++ // /design and /rundown have Markdown twins; The Rundown's twin carries Live's canonical, so the two++ // representations agree about which URL is the page.for (const p of ['/design', '/rundown']) {const twin = await t.get(`${p}.md`);assert.strictEqual(twin.status, 200, `${p}.md`);assert.match(twin.headers.get('content-type') || '', /^text\/markdown/);assert.match((await t.get(p)).headers.get('link') || '', /text\/markdown/, `${p} names its twin`);}++ const rundownTwin = await t.get('/rundown.md');++ assert.ok(rundownTwin.text.includes('canonical: "https://openvibe.live/rundown"'), 'the twin canonicalises to Live');});await check('llms-full.txt (SEO-33): the site in full, every page llms.txt lists with what it holds', async () => {@@@ -74,12 +80,12 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'STassert.match(r.text, /### Design health\n\nURL: https:\/\/openvibe\.report\/design\n\nhow closely each site follows/);});-- await check('a topics source that answers nothing still leaves the three static sitemap pages', async () => {++ await check('a topics source that answers nothing still leaves the pages this site owns', async () => {const empty = await boot({ data: { topics: null } });try {const r = await empty.get('/sitemap.xml');assert.strictEqual(r.status, 200);-- for (const p of ['/', '/stats', '/rundown']) assert.ok(r.text.includes(`<loc>https://openvibe.report${p}</loc>`), p);++ for (const p of ['/', '/stats']) assert.ok(r.text.includes(`<loc>https://openvibe.report${p}</loc>`), p);assert.ok(!r.text.includes('/rundown/t/'), 'no topic should be listed');} finally { await empty.close(); }});@@@ -32,6 +32,21 @@ const { boot, check, done, defaults } = require('./helpers/boot');});}++ await check('every Rundown page canonicalises to OpenVibe.Live, and the rest to Report', async () => {++ const canon = {++ '/rundown': 'https://openvibe.live/rundown',++ '/rundown/t/seattle': 'https://openvibe.live/rundown/t/seattle',++ '/rundown/e/7': 'https://openvibe.live/rundown/e/7',++ '/rundown/s/goosely': 'https://openvibe.live/rundown/goosely',++ '/stats': 'https://openvibe.report/stats',++ '/': 'https://openvibe.report/',++ };++ for (const [path, url] of Object.entries(canon)) {++ const r = await t.get(path);++ assert.ok(r.text.includes(`<link rel="canonical" href="${url}">`), `${path}: ${url}`);++ }++ });++await check('the CSP allows the Live and Community thumbnails on the pages that show them', async () => {for (const path of ['/', '/rundown']) {const r = await t.get(path);@@@ -55,9 +70,10 @@ 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('a page names its canonical and mounts 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');-- assert.ok(r.text.includes('<link rel="canonical" href="https://openvibe.report/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.report/rundown/t/seattle">'), 'Report must not claim it');assert.ok(r.text.includes('<div id="navbar-mount"></div>'), 'no navbar mount');assert.ok(r.text.includes('id="ov-footer"'), 'no Shared footer');assert.ok(r.text.includes('"service":"report"'), 'the Frame is not initialised for report');
@@ -77,10 +77,10 @@ All server-rendered in `server/views/index.js`; every dynamic value goes through
@@ -77,10 +77,10 @@ All server-rendered in `server/views/index.js`; every dynamic value goes through
|---|---|
|---|---|
| `/` | the 7-day open numbers, the Rundown's counts and lead, and the trending topics |
| `/` | the 7-day open numbers, the Rundown's counts and lead, and the trending topics |
| `/stats?days=7\|30\|90` | the network totals and a first look at every site (anything else is 30) |
| `/stats?days=7\|30\|90` | the network totals and a first look at every site (anything else is 30) |
-| `/rundown` | on air, the lead, the latest streams and every topic |
-| `/rundown/t/:slug` | one topic, its takes and related topics |
-| `/rundown/e/:streamId` | one stream as an episode, with its segments |
-| `/rundown/s/:username` | one streamer's episodes and topics |
+| `/rundown` | on air, the lead, the latest streams and every topic; canonical is OpenVibe.Live's `/rundown` |
+| `/rundown/t/:slug` | one topic, its takes and related topics; canonical is Live's `/rundown/t/:slug` |
+| `/rundown/e/:streamId` | one stream as an episode, with its segments; canonical is Live's `/rundown/e/:streamId` |
+| `/rundown/s/:username` | one streamer's episodes and topics; canonical is Live's `/rundown/:username` |
| `/after-show/:streamId` | one after-show report |
| `/after-show/:streamId` | one after-show report |
| `/c/:username` | one channel's latest after-show reports |
| `/c/:username` | one channel's latest after-show reports |
| `/ai` | benchmark kinds grouped by room, with leaders and game counts |
| `/ai` | benchmark kinds grouped by room, with leaders and game counts |
@@ -16,7 +16,6 @@ function createAgentPages({ publicPages, bench }) {
@@ -16,7 +16,6 @@ function createAgentPages({ publicPages, bench }) {
if (path === '/') page = await publicPages.home();
if (path === '/') page = await publicPages.home();
else if (path === '/developers') page = publicPages.developers();
else if (path === '/developers') page = publicPages.developers();
else if (path === '/design') page = publicPages.design();
else if (path === '/design') page = publicPages.design();
- else if (path === '/rundown') page = await publicPages.rundown();
else if (path === '/stats') page = await publicPages.stats(normaliseDays(query && query.get('days')));
else if (path === '/stats') page = await publicPages.stats(normaliseDays(query && query.get('days')));
else if (path === '/ai') page = await publicPages.aiIndex();
else if (path === '/ai') page = await publicPages.aiIndex();
else if (model) page = await publicPages.aiModel(decodeURIComponent(model[1]));
else if (model) page = await publicPages.aiModel(decodeURIComponent(model[1]));
@@ -7,8 +7,8 @@
@@ -7,8 +7,8 @@
* GET /sitemap.xml /, /stats, /design, /rundown and every /rundown/t/<slug> from the Rundown's topics
* GET /sitemap.xml /, /stats, /design, /rundown and every /rundown/t/<slug> from the Rundown's topics
* GET /llms.txt a markdown map of the site and its machine-readable answers
* GET /llms.txt a markdown map of the site and its machine-readable answers
*
*
- * The sitemap reads topics through the same cached client as the pages, so a crawler is no heavier than a reader
- * and a source that does not answer leaves the three static pages in place.
+ * The sitemap lists only the pages this site owns; The Rundown pages carry rel=canonical to openvibe.live and are left
+ * out, so the two sites never present the same page as two canonical URLs.
*/
*/
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
@@ -46,25 +46,20 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null }) {
@@ -46,25 +46,20 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null }) {
});
});
r.get('/sitemap.xml', async (_req, res) => {
r.get('/sitemap.xml', async (_req, res) => {
- const [data, ai, reports] = await Promise.all([live.topics(), bench.kinds(), sessionReports ? sessionReports.listPublic({ limit: 500 }) : []]);
- const topics = (data && Array.isArray(data.topics)) ? data.topics : [];
+ const [ai, reports] = await Promise.all([bench.kinds(), sessionReports ? sessionReports.listPublic({ limit: 500 }) : []]);
const kinds = (ai && Array.isArray(ai.data.kinds)) ? ai.data.kinds : [];
const kinds = (ai && Array.isArray(ai.data.kinds)) ? ai.data.kinds : [];
const lastmod = siteUpdated();
const lastmod = siteUpdated();
+ // Only the pages this site owns. /rundown and its topic, episode and speaker pages carry rel=canonical to
+ // OpenVibe.Live's Rundown, so listing them here would advertise a duplicate.
const pages = [
const pages = [
{ loc: abs('/'), changefreq: 'daily', priority: 1.0 },
{ loc: abs('/'), changefreq: 'daily', priority: 1.0 },
{ loc: abs('/stats'), changefreq: 'daily', priority: 0.9 },
{ loc: abs('/stats'), changefreq: 'daily', priority: 0.9 },
{ loc: abs('/design'), changefreq: 'weekly', priority: 0.6 },
{ loc: abs('/design'), changefreq: 'weekly', priority: 0.6 },
- { loc: abs('/rundown'), changefreq: 'daily', priority: 0.9 },
{ loc: abs('/ai'), changefreq: 'daily', priority: 0.9 },
{ loc: abs('/ai'), changefreq: 'daily', priority: 0.9 },
...reports.map((rep) => ({ loc: abs('/s/' + encodeURIComponent(rep.id)), changefreq: 'daily', priority: 0.6 })),
...reports.map((rep) => ({ loc: abs('/s/' + encodeURIComponent(rep.id)), changefreq: 'daily', priority: 0.6 })),
...kinds.filter((k) => k && bench.isKindId(k.id)).map((k) => ({
...kinds.filter((k) => k && bench.isKindId(k.id)).map((k) => ({
loc: abs(`/ai/${encodeURIComponent(k.id)}`), changefreq: 'daily', priority: 0.7,
loc: abs(`/ai/${encodeURIComponent(k.id)}`), changefreq: 'daily', priority: 0.7,
})),
})),
- ...topics.filter((t) => t && typeof t.slug === 'string').map((t) => ({
- loc: abs(`/rundown/t/${encodeURIComponent(t.slug)}`),
- changefreq: 'weekly',
- priority: 0.7,
- })),
].map((p) => ({ ...p, ...(lastmod ? { lastmod } : {}) }));
].map((p) => ({ ...p, ...(lastmod ? { lastmod } : {}) }));
res.type('application/xml').set('Cache-Control', CACHE).send(seo.sitemapXml(pages));
res.type('application/xml').set('Cache-Control', CACHE).send(seo.sitemapXml(pages));
});
});
@@ -81,7 +76,7 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null }) {
@@ -81,7 +76,7 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null }) {
{ title: 'OpenVibe.Report', url: abs('/'), note: 'the Report room' },
{ title: 'OpenVibe.Report', url: abs('/'), note: 'the Report room' },
{ title: 'Open numbers', url: abs('/stats'), note: 'page views, accounts and traffic for every OpenVibe site' },
{ title: 'Open numbers', url: abs('/stats'), note: 'page views, accounts and traffic for every OpenVibe site' },
{ title: 'Design health', url: abs('/design'), note: 'how closely each site follows the shared design kit, read from its code; per site at /design/<site>' },
{ title: 'Design health', url: abs('/design'), note: 'how closely each site follows the shared design kit, read from its code; per site at /design/<site>' },
- { title: 'The Rundown', url: abs('/rundown'), note: 'what streamers talked about, topic by topic' },
+ { title: 'The Rundown', url: 'https://openvibe.live/rundown', note: 'what streamers talked about, topic by topic (OpenVibe.Live is its canonical home)' },
{ title: 'AI boards', url: abs('/ai'), note: 'Bench ratings, quality and cost across rooms' },
{ title: 'AI boards', url: abs('/ai'), note: 'Bench ratings, quality and cost across rooms' },
] },
] },
// A JSON API is what an agent came for, not something to skip (AGT-5 review).
// A JSON API is what an agent came for, not something to skip (AGT-5 review).
@@ -87,6 +87,15 @@ function createPageRoutes(ctx) {
@@ -87,6 +87,15 @@ function createPageRoutes(ctx) {
const { live, bench, sessionReports } = ctx;
const { live, bench, sessionReports } = ctx;
const r = express.Router();
const r = express.Router();
const publicPages = ctx.publicPages || createPublicPages(ctx);
const publicPages = ctx.publicPages || createPublicPages(ctx);
+ // The Rundown's pages canonicalise to OpenVibe.Live, and so does its Markdown twin: the generic agent layer
+ // (server/agent-pages.js) would sign the twin with this site's own address. Answer it here instead, before the
+ // .md hand-off, with Live's canonical.
+ r.get('/rundown.md', async (_req, res) => {
+ const { markdownTwin } = require('openvibe-shared/agent-layer');
+ const page = await publicPages.rundown();
+ const body = markdownTwin({ html: page.html, canonical: 'https://openvibe.live/rundown' });
+ res.set('Content-Type', 'text/markdown; charset=utf-8').set('Cache-Control', CACHE).send(body);
+ });
// A path ending in .md is a Markdown twin (server/agent-pages.js), never a page parameter.
// A path ending in .md is a Markdown twin (server/agent-pages.js), never a page parameter.
r.use((req, res, next) => (req.path.endsWith('.md') ? next('router') : next()));
r.use((req, res, next) => (req.path.endsWith('.md') ? next('router') : next()));
@@ -23,6 +23,7 @@ const { asset } = require('../assets');
@@ -23,6 +23,7 @@ const { asset } = require('../assets');
const NAME = 'OpenVibe.Report';
const NAME = 'OpenVibe.Report';
const NETWORK = 'https://openvibe.network';
const NETWORK = 'https://openvibe.network';
+const LIVE = 'https://openvibe.live';
const BENCH = 'https://bench.openvibe.services';
const BENCH = 'https://bench.openvibe.services';
const ROOM = rooms.get('report');
const ROOM = rooms.get('report');
const esc = kit.esc;
const esc = kit.esc;
@@ -317,7 +318,7 @@ ${all.length ? `<section aria-labelledby="rp-t-h"><h2 id="rp-t-h">Topics</h2><ul
@@ -317,7 +318,7 @@ ${all.length ? `<section aria-labelledby="rp-t-h"><h2 id="rp-t-h">Topics</h2><ul
${voices.length ? `<section aria-labelledby="rp-v-h"><h2 id="rp-v-h">Voices</h2><ul class="rp-voices">${voices.map((v) => `<li><a href="/rundown/s/${encodeURIComponent((v.user || {}).username || '')}">${kit.mark({ name: who(v.user), src: v.user && v.user.avatar_url, size: 40 })}<span><b>${esc(who(v.user))}</b><small>${[n(v.topics) ? `${exact(v.topics)} topic${Number(v.topics) === 1 ? '' : 's'}` : '', n(v.segments) ? `${exact(v.segments)} take${Number(v.segments) === 1 ? '' : 's'}` : ''].filter(Boolean).join(' · ')}</small></span></a></li>`).join('')}</ul></section>` : ''}
${voices.length ? `<section aria-labelledby="rp-v-h"><h2 id="rp-v-h">Voices</h2><ul class="rp-voices">${voices.map((v) => `<li><a href="/rundown/s/${encodeURIComponent((v.user || {}).username || '')}">${kit.mark({ name: who(v.user), src: v.user && v.user.avatar_url, size: 40 })}<span><b>${esc(who(v.user))}</b><small>${[n(v.topics) ? `${exact(v.topics)} topic${Number(v.topics) === 1 ? '' : 's'}` : '', n(v.segments) ? `${exact(v.segments)} take${Number(v.segments) === 1 ? '' : 's'}` : ''].filter(Boolean).join(' · ')}</small></span></a></li>`).join('')}</ul></section>` : ''}
${streams.length ? kit.rail({ id: 'episodes', title: 'Recent streams', items: streams }) : ''}
${streams.length ? kit.rail({ id: 'episodes', title: 'Recent streams', items: streams }) : ''}
${reports && reports.length ? reportsSection(reports) : ''}`;
${reports && reports.length ? reportsSection(reports) : ''}`;
- return layout({ title: 'The Rundown: what streamers talked about', description: 'Topics, takes and crossovers from what streamers said on OpenVibe, written up by AI from the streams themselves.', body, active: '/rundown', path: '/rundown' });
+ return layout({ title: 'The Rundown: what streamers talked about', description: 'Topics, takes and crossovers from what streamers said on OpenVibe, written up by AI from the streams themselves.', body, active: '/rundown', path: '/rundown', canonical: `${LIVE}/rundown` });
}
}
@@ -462,7 +463,7 @@ ${t.headline ? `<p class="rp-lede">${esc(t.headline)}</p>` : ''}${t.summary ? `<
@@ -462,7 +463,7 @@ ${t.headline ? `<p class="rp-lede">${esc(t.headline)}</p>` : ''}${t.summary ? `<
${takes.length ? `<section aria-labelledby="rp-tk-h"><h2 id="rp-tk-h">Takes</h2><ol class="rp-takes">${takes.map(takeRow).join('')}</ol></section>` : ''}
${takes.length ? `<section aria-labelledby="rp-tk-h"><h2 id="rp-tk-h">Takes</h2><ol class="rp-takes">${takes.map(takeRow).join('')}</ol></section>` : ''}
${citesHTML}
${citesHTML}
${(d.related || []).length ? `<section aria-labelledby="rp-rel-h"><h2 id="rp-rel-h">Related</h2><ul class="rp-chips">${d.related.slice(0, 12).map((r) => `<li><a href="/rundown/t/${encodeURIComponent(r.slug)}">${esc(r.title)}</a></li>`).join('')}</ul></section>` : ''}`;
${(d.related || []).length ? `<section aria-labelledby="rp-rel-h"><h2 id="rp-rel-h">Related</h2><ul class="rp-chips">${d.related.slice(0, 12).map((r) => `<li><a href="/rundown/t/${encodeURIComponent(r.slug)}">${esc(r.title)}</a></li>`).join('')}</ul></section>` : ''}`;
- return layout({ title: `${t.title}: The Rundown`, description: t.headline || `What streamers said about ${t.title}.`, body, active: '/rundown', path: `/rundown/t/${encodeURIComponent(t.slug || '')}` });
+ return layout({ title: `${t.title}: The Rundown`, description: t.headline || `What streamers said about ${t.title}.`, body, active: '/rundown', path: `/rundown/t/${encodeURIComponent(t.slug || '')}`, canonical: `${LIVE}/rundown/t/${encodeURIComponent(t.slug || '')}` });
}
}
/** A chapter's moment in the recording on OpenVibe.Video (the same link Live's own Rundown draws). */
/** A chapter's moment in the recording on OpenVibe.Video (the same link Live's own Rundown draws). */
@@ -489,17 +490,17 @@ function episode({ episode: d, citations = [] } = {}) {
@@ -489,17 +490,17 @@ function episode({ episode: d, citations = [] } = {}) {
${chaptersHTML}
${chaptersHTML}
${segs.length ? `<section aria-labelledby="rp-sg-h"><h2 id="rp-sg-h">What happened</h2><ol class="rp-takes rp-timeline">${segs.map(takeRow).join('')}</ol></section>` : ''}
${segs.length ? `<section aria-labelledby="rp-sg-h"><h2 id="rp-sg-h">What happened</h2><ol class="rp-takes rp-timeline">${segs.map(takeRow).join('')}</ol></section>` : ''}
${(d.more || []).length ? `<section aria-labelledby="rp-mo-h"><h2 id="rp-mo-h">More from ${esc(who(s.user) || 'this streamer')}</h2><ul class="rp-chips">${d.more.slice(0, 8).map((m) => `<li><a href="/rundown/e/${encodeURIComponent(m.stream_id)}">${esc(m.title)}</a></li>`).join('')}</ul></section>` : ''}`;
${(d.more || []).length ? `<section aria-labelledby="rp-mo-h"><h2 id="rp-mo-h">More from ${esc(who(s.user) || 'this streamer')}</h2><ul class="rp-chips">${d.more.slice(0, 8).map((m) => `<li><a href="/rundown/e/${encodeURIComponent(m.stream_id)}">${esc(m.title)}</a></li>`).join('')}</ul></section>` : ''}`;
- return layout({ title: `${e.title || s.title}: The Rundown`, description: e.logline || '', body, active: '/rundown', path: `/rundown/e/${encodeURIComponent(s.id || e.stream_id || '')}` });
+ return layout({ title: `${e.title || s.title}: The Rundown`, description: e.logline || '', body, active: '/rundown', path: `/rundown/e/${encodeURIComponent(s.id || e.stream_id || '')}`, canonical: `${LIVE}/rundown/e/${encodeURIComponent(s.id || e.stream_id || '')}` });
}
}
function speaker({ speaker: d, username } = {}) {
function speaker({ speaker: d, username } = {}) {
const u = d.user || { username };
const u = d.user || { username };
- if (d.opted_out) return layout({ title: `${who(u)}: The Rundown`, robots: 'noindex, follow', body: `<header class="rp-page-h"><h1>${esc(who(u))}</h1><p class="rp-lede">${esc(who(u))} has opted out of The Rundown: nothing they say on stream is written up here.</p></header>`, active: '/rundown' });
+ if (d.opted_out) return layout({ title: `${who(u)}: The Rundown`, robots: 'noindex, follow', body: `<header class="rp-page-h"><h1>${esc(who(u))}</h1><p class="rp-lede">${esc(who(u))} has opted out of The Rundown: nothing they say on stream is written up here.</p></header>`, active: '/rundown', canonical: `${LIVE}/rundown/${encodeURIComponent(u.username || username || '')}` });
const body = `<header class="rp-page-h rp-person">${kit.mark({ name: who(u), src: u.avatar_url, size: 64 })}<div><p class="rp-kicker">The Rundown · voice</p><h1>${esc(who(u))}</h1><p class="rp-meta"><a href="https://openvibe.live/@${encodeURIComponent(u.username || username)}">Channel on OpenVibe.Live</a> · <a href="/c/${encodeURIComponent(u.username || username)}">After-show reports</a></p></div></header>
const body = `<header class="rp-page-h rp-person">${kit.mark({ name: who(u), src: u.avatar_url, size: 64 })}<div><p class="rp-kicker">The Rundown · voice</p><h1>${esc(who(u))}</h1><p class="rp-meta"><a href="https://openvibe.live/@${encodeURIComponent(u.username || username)}">Channel on OpenVibe.Live</a> · <a href="/c/${encodeURIComponent(u.username || username)}">After-show reports</a></p></div></header>
${(d.topics || []).length ? `<section aria-labelledby="rp-st-h"><h2 id="rp-st-h">Topics</h2><ul class="rp-topics">${d.topics.slice(0, 12).map(topicCard).join('')}</ul></section>` : ''}
${(d.topics || []).length ? `<section aria-labelledby="rp-st-h"><h2 id="rp-st-h">Topics</h2><ul class="rp-topics">${d.topics.slice(0, 12).map(topicCard).join('')}</ul></section>` : ''}
${(d.lines || []).length ? `<section aria-labelledby="rp-sl-h"><h2 id="rp-sl-h">Takes</h2><ol class="rp-takes">${d.lines.slice(0, 20).map(takeRow).join('')}</ol><p class="rp-meta">${AI}</p></section>` : ''}
${(d.lines || []).length ? `<section aria-labelledby="rp-sl-h"><h2 id="rp-sl-h">Takes</h2><ol class="rp-takes">${d.lines.slice(0, 20).map(takeRow).join('')}</ol><p class="rp-meta">${AI}</p></section>` : ''}
${(d.episodes || []).length ? `<section aria-labelledby="rp-se-h"><h2 id="rp-se-h">Episodes</h2><ul class="rp-chips">${d.episodes.slice(0, 12).map((m) => `<li><a href="/rundown/e/${encodeURIComponent(m.stream_id)}">${esc(m.title)}</a></li>`).join('')}</ul></section>` : ''}`;
${(d.episodes || []).length ? `<section aria-labelledby="rp-se-h"><h2 id="rp-se-h">Episodes</h2><ul class="rp-chips">${d.episodes.slice(0, 12).map((m) => `<li><a href="/rundown/e/${encodeURIComponent(m.stream_id)}">${esc(m.title)}</a></li>`).join('')}</ul></section>` : ''}`;
- return layout({ title: `${who(u)}: The Rundown`, description: `What ${who(u)} talked about on stream, topic by topic.`, body, active: '/rundown', path: `/rundown/s/${encodeURIComponent(u.username || username || '')}` });
+ return layout({ title: `${who(u)}: The Rundown`, description: `What ${who(u)} talked about on stream, topic by topic.`, body, active: '/rundown', path: `/rundown/s/${encodeURIComponent(u.username || username || '')}`, canonical: `${LIVE}/rundown/${encodeURIComponent(u.username || username || '')}` });
}
}
function recap({ recap: d } = {}) {
function recap({ recap: d } = {}) {
@@ -1,9 +1,9 @@
@@ -1,9 +1,9 @@
'use strict';
'use strict';
/**
/**
- * The crawl artifacts: robots.txt allows the public pages and disallows the API; sitemap.xml is the home, /stats,
- * /rundown and every topic from the Rundown; llms.txt names the site and its machine-readable answers. All three
- * are public for an hour.
+ * The crawl artifacts: robots.txt allows the public pages and disallows the API; sitemap.xml is the pages this site
+ * owns (not The Rundown, which canonicalises to OpenVibe.Live); llms.txt names the site and its machine-readable
+ * answers. All three are public for an hour.
*/
*/
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
@@ -26,14 +26,17 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ST
@@ -26,14 +26,17 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ST
for (const p of ['/stats', '/rundown']) assert.ok(!r.text.includes(`Disallow: ${p}`), `${p} must stay crawlable`);
for (const p of ['/stats', '/rundown']) assert.ok(!r.text.includes(`Disallow: ${p}`), `${p} must stay crawlable`);
});
});
- await check('sitemap.xml: the home, /stats, /rundown and every topic, with lastmod from STATUS.json', async () => {
+ await check('sitemap.xml: the site\'s own pages, never The Rundown (Live is its canonical), with lastmod from STATUS.json', async () => {
const r = await t.get('/sitemap.xml');
const r = await t.get('/sitemap.xml');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.match(r.headers.get('content-type') || '', /^application\/xml/);
assert.match(r.headers.get('content-type') || '', /^application\/xml/);
assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600');
assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600');
- for (const p of ['/', '/stats', '/rundown', '/rundown/t/seattle', '/rundown/t/ai-progress']) {
+ for (const p of ['/', '/stats', '/design', '/ai']) {
assert.ok(r.text.includes(`<loc>https://openvibe.report${p}</loc>`), `no ${p} entry`);
assert.ok(r.text.includes(`<loc>https://openvibe.report${p}</loc>`), `no ${p} entry`);
}
}
+ for (const p of ['/rundown', '/rundown/t/seattle', '/rundown/t/ai-progress']) {
+ assert.ok(!r.text.includes(`<loc>https://openvibe.report${p}</loc>`), `${p} must not be listed: Live is its canonical`);
+ }
assert.ok(!r.text.includes('<loc>https://openvibe.report/api/'), 'the API must not be in the sitemap');
assert.ok(!r.text.includes('<loc>https://openvibe.report/api/'), 'the API must not be in the sitemap');
const lastmods = [...r.text.matchAll(/<lastmod>([^<]+)<\/lastmod>/g)].map((m) => m[1]);
const lastmods = [...r.text.matchAll(/<lastmod>([^<]+)<\/lastmod>/g)].map((m) => m[1]);
assert.ok(lastmods.length > 0, 'no lastmod');
assert.ok(lastmods.length > 0, 'no lastmod');
@@ -49,19 +52,22 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ST
@@ -49,19 +52,22 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ST
for (const link of [
for (const link of [
'https://openvibe.report/index.md',
'https://openvibe.report/index.md',
'https://openvibe.report/stats.md',
'https://openvibe.report/stats.md',
- 'https://openvibe.report/rundown.md',
+ 'https://openvibe.live/rundown',
'https://openvibe.report/design.md',
'https://openvibe.report/design.md',
'https://openvibe.report/api/v1/stats?days=7',
'https://openvibe.report/api/v1/stats?days=7',
'https://openvibe.report/sitemap.xml',
'https://openvibe.report/sitemap.xml',
]) assert.ok(r.text.includes(`(${link})`), `missing link ${link}`);
]) assert.ok(r.text.includes(`(${link})`), `missing link ${link}`);
assert.ok(r.text.includes('\n## Optional\n'), 'llms.txt has an Optional section');
assert.ok(r.text.includes('\n## Optional\n'), 'llms.txt has an Optional section');
- // 11-03: /design and /rundown have the twins llms.txt links, and their pages name them.
+ // /design and /rundown have Markdown twins; The Rundown's twin carries Live's canonical, so the two
+ // representations agree about which URL is the page.
for (const p of ['/design', '/rundown']) {
for (const p of ['/design', '/rundown']) {
const twin = await t.get(`${p}.md`);
const twin = await t.get(`${p}.md`);
assert.strictEqual(twin.status, 200, `${p}.md`);
assert.strictEqual(twin.status, 200, `${p}.md`);
assert.match(twin.headers.get('content-type') || '', /^text\/markdown/);
assert.match(twin.headers.get('content-type') || '', /^text\/markdown/);
assert.match((await t.get(p)).headers.get('link') || '', /text\/markdown/, `${p} names its twin`);
assert.match((await t.get(p)).headers.get('link') || '', /text\/markdown/, `${p} names its twin`);
}
}
+ const rundownTwin = await t.get('/rundown.md');
+ assert.ok(rundownTwin.text.includes('canonical: "https://openvibe.live/rundown"'), 'the twin canonicalises to Live');
});
});
await check('llms-full.txt (SEO-33): the site in full, every page llms.txt lists with what it holds', async () => {
await check('llms-full.txt (SEO-33): the site in full, every page llms.txt lists with what it holds', async () => {
@@ -74,12 +80,12 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ST
@@ -74,12 +80,12 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ST
assert.match(r.text, /### Design health\n\nURL: https:\/\/openvibe\.report\/design\n\nhow closely each site follows/);
assert.match(r.text, /### Design health\n\nURL: https:\/\/openvibe\.report\/design\n\nhow closely each site follows/);
});
});
- await check('a topics source that answers nothing still leaves the three static sitemap pages', async () => {
+ await check('a topics source that answers nothing still leaves the pages this site owns', async () => {
const empty = await boot({ data: { topics: null } });
const empty = await boot({ data: { topics: null } });
try {
try {
const r = await empty.get('/sitemap.xml');
const r = await empty.get('/sitemap.xml');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
- for (const p of ['/', '/stats', '/rundown']) assert.ok(r.text.includes(`<loc>https://openvibe.report${p}</loc>`), p);
+ for (const p of ['/', '/stats']) assert.ok(r.text.includes(`<loc>https://openvibe.report${p}</loc>`), p);
assert.ok(!r.text.includes('/rundown/t/'), 'no topic should be listed');
assert.ok(!r.text.includes('/rundown/t/'), 'no topic should be listed');
} finally { await empty.close(); }
} finally { await empty.close(); }
});
});
@@ -32,6 +32,21 @@ const { boot, check, done, defaults } = require('./helpers/boot');
@@ -32,6 +32,21 @@ const { boot, check, done, defaults } = require('./helpers/boot');
});
});
}
}
+ await check('every Rundown page canonicalises to OpenVibe.Live, and the rest to Report', async () => {
+ const canon = {
+ '/rundown': 'https://openvibe.live/rundown',
+ '/rundown/t/seattle': 'https://openvibe.live/rundown/t/seattle',
+ '/rundown/e/7': 'https://openvibe.live/rundown/e/7',
+ '/rundown/s/goosely': 'https://openvibe.live/rundown/goosely',
+ '/stats': 'https://openvibe.report/stats',
+ '/': 'https://openvibe.report/',
+ };
+ for (const [path, url] of Object.entries(canon)) {
+ const r = await t.get(path);
+ assert.ok(r.text.includes(`<link rel="canonical" href="${url}">`), `${path}: ${url}`);
+ }
+ });
+
await check('the CSP allows the Live and Community thumbnails on the pages that show them', async () => {
await check('the CSP allows the Live and Community thumbnails on the pages that show them', async () => {
for (const path of ['/', '/rundown']) {
for (const path of ['/', '/rundown']) {
const r = await t.get(path);
const r = await t.get(path);
@@ -55,9 +70,10 @@ const { boot, check, done, defaults } = require('./helpers/boot');
@@ -55,9 +70,10 @@ 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('a page names its canonical and mounts 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.report/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.report/rundown/t/seattle">'), 'Report must not claim it');
assert.ok(r.text.includes('<div id="navbar-mount"></div>'), 'no navbar mount');
assert.ok(r.text.includes('<div id="navbar-mount"></div>'), 'no navbar mount');
assert.ok(r.text.includes('id="ov-footer"'), 'no Shared footer');
assert.ok(r.text.includes('id="ov-footer"'), 'no Shared footer');
assert.ok(r.text.includes('"service":"report"'), 'the Frame is not initialised for report');
assert.ok(r.text.includes('"service":"report"'), 'the Frame is not initialised for report');