openvibers/openvibe.live Commits
SEO: /documentation and /updates have their own title, description and canonical (they repeated the home page's), and the legal pages name themselves canonical with share tags (#269)
Changes
@@@ -4,7 +4,15 @@<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>DMCA Policy — OpenVibe</title>-- <meta name="description" content="DMCA / copyright takedown policy for the OpenVibe (OpenVibe.Live, openvibe.tools, openvibe.games).">++ <meta name="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.">++ <link rel="canonical" href="https://openvibe.live/dmca">++ <meta property="og:type" content="website">++ <meta property="og:site_name" content="OpenVibe.Live">++ <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 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"><style>@@@ -4,7 +4,15 @@<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Privacy Policy — OpenVibe</title>-- <meta name="description" content="Privacy Policy for the OpenVibe — OpenVibe.Live, openvibe.tools, openvibe.games. We collect the least we can, we don't sell it, and transcription runs locally.">++ <meta name="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.">++ <link rel="canonical" href="https://openvibe.live/privacy">++ <meta property="og:type" content="website">++ <meta property="og:site_name" content="OpenVibe.Live">++ <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 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"><style>@@@ -4,7 +4,15 @@<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Terms of Service — OpenVibe</title>-- <meta name="description" content="Terms of Service for the OpenVibe — OpenVibe.Live, openvibe.tools, openvibe.games. Minimal rules, maximum freedom.">++ <meta name="description" content="Terms of Service for OpenVibe: OpenVibe.Live, openvibe.tools, openvibe.games. Minimal rules, maximum freedom.">++ <link rel="canonical" href="https://openvibe.live/tos">++ <meta property="og:type" content="website">++ <meta property="og:site_name" content="OpenVibe.Live">++ <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 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"><style>@@@ -212,6 +212,9 @@ async function _pageMeta(routePath, { page = 1 } = {}) {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: [] };++ // 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: [] };return null;void bu;@@@ -870,7 +873,7 @@ function _cacheSet(key, html, ttl = CACHE_TTL_MS) {_cache.set(key, { html, at: Date.now(), ttl });}--const SEO_ROUTE_RE = /^\/(?:$|content$|moments$|vods$|clips$|pastes$|rundown(?:\/(?:topics|t\/[a-z0-9-]{1,80}|e\/\d{1,12}|[A-Za-z0-9_.-]{1,64}))?$|chat$|search$|vod\/\d+$|clip\/\d+$|recap\/\d+$|p\/[A-Za-z0-9_-]+$|@[A-Za-z0-9_]{3,24}$)/;++const SEO_ROUTE_RE = /^\/(?:$|content$|moments$|vods$|clips$|pastes$|rundown(?:\/(?:topics|t\/[a-z0-9-]{1,80}|e\/\d{1,12}|[A-Za-z0-9_.-]{1,64}))?$|chat$|search$|documentation$|updates$|vod\/\d+$|clip\/\d+$|recap\/\d+$|p\/[A-Za-z0-9_-]+$|@[A-Za-z0-9_]{3,24}$)/;/** ?page=N on a channel page (its video list); 1 everywhere else and for anything that is not a number. */function _pageParam(p, query) {@@@ -103,6 +103,7 @@ function get(p, headers = {}) {}const head = (html) => html.slice(0, html.search(/<\/head>/i));const titleOf = (html) => ((head(html).match(/<title>([^<]*)<\/title>/) || [])[1] || '').replace(/'/g, "'");++const descOf = (html) => ((head(html).match(/<meta name="description" content="([^"]*)">/) || [])[1] || '');const canonicalsOf = (html) => [...head(html).matchAll(/<link rel="canonical" href="([^"]+)">/g)].map((m) => m[1]);const robotsOf = (html) => [...head(html).matchAll(/<meta name="robots" content="([^"]+)">/g)].map((m) => m[1]);const ldOf = (html) => [...head(html).matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => { try { return JSON.parse(m[1]); } catch { return null; } }).filter(Boolean);@@@ -222,6 +223,27 @@ const server = http.createServer(app).listen(0, '127.0.0.1', async () => {assert.ok(/<meta name="robots" content="noindex,follow">/.test(head(search)));});++ await check('/documentation and /updates have their own title, description and canonical (they repeated the home page\'s)', async () => {++ const homeDesc = descOf((await get('/')).body);++ for (const [path, title] of [['/documentation', 'API docs | OpenVibe.Live'], ['/updates', 'Updates | OpenVibe.Live']]) {++ const body = (await get(path)).body;++ assert.deepStrictEqual(canonicalsOf(body), [`https://openvibe.live${path}`], path);++ assert.strictEqual(titleOf(body), title);++ const desc = descOf(body);++ assert.ok(desc && desc !== homeDesc && desc.length <= 160, `${path}: ${desc}`);++ }++ });++++ await check('the legal pages name themselves canonical and carry share tags', () => {++ for (const name of ['tos', 'privacy', 'dmca']) {++ 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(!/for the OpenVibe\b/.test(html.slice(0, html.indexOf('</head>'))), `${name}: "for the OpenVibe" reads wrong`);++ }++ });++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');
@@ -4,7 +4,15 @@
@@ -4,7 +4,15 @@
<meta charset="UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DMCA Policy — OpenVibe</title>
<title>DMCA Policy — OpenVibe</title>
- <meta name="description" content="DMCA / copyright takedown policy for the OpenVibe (OpenVibe.Live, openvibe.tools, openvibe.games).">
+ <meta name="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.">
+ <link rel="canonical" href="https://openvibe.live/dmca">
+ <meta property="og:type" content="website">
+ <meta property="og:site_name" content="OpenVibe.Live">
+ <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 name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<style>
<style>
@@ -4,7 +4,15 @@
@@ -4,7 +4,15 @@
<meta charset="UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy — OpenVibe</title>
<title>Privacy Policy — OpenVibe</title>
- <meta name="description" content="Privacy Policy for the OpenVibe — OpenVibe.Live, openvibe.tools, openvibe.games. We collect the least we can, we don't sell it, and transcription runs locally.">
+ <meta name="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.">
+ <link rel="canonical" href="https://openvibe.live/privacy">
+ <meta property="og:type" content="website">
+ <meta property="og:site_name" content="OpenVibe.Live">
+ <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 name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<style>
<style>
@@ -4,7 +4,15 @@
@@ -4,7 +4,15 @@
<meta charset="UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Terms of Service — OpenVibe</title>
<title>Terms of Service — OpenVibe</title>
- <meta name="description" content="Terms of Service for the OpenVibe — OpenVibe.Live, openvibe.tools, openvibe.games. Minimal rules, maximum freedom.">
+ <meta name="description" content="Terms of Service for OpenVibe: OpenVibe.Live, openvibe.tools, openvibe.games. Minimal rules, maximum freedom.">
+ <link rel="canonical" href="https://openvibe.live/tos">
+ <meta property="og:type" content="website">
+ <meta property="og:site_name" content="OpenVibe.Live">
+ <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 name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<style>
<style>
@@ -212,6 +212,9 @@ async function _pageMeta(routePath, { page = 1 } = {}) {
@@ -212,6 +212,9 @@ async function _pageMeta(routePath, { page = 1 } = {}) {
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: [] };
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).
// 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', 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: [] };
return null;
return null;
void bu;
void bu;
@@ -870,7 +873,7 @@ function _cacheSet(key, html, ttl = CACHE_TTL_MS) {
@@ -870,7 +873,7 @@ function _cacheSet(key, html, ttl = CACHE_TTL_MS) {
_cache.set(key, { html, at: Date.now(), ttl });
_cache.set(key, { html, at: Date.now(), ttl });
}
}
-const SEO_ROUTE_RE = /^\/(?:$|content$|moments$|vods$|clips$|pastes$|rundown(?:\/(?:topics|t\/[a-z0-9-]{1,80}|e\/\d{1,12}|[A-Za-z0-9_.-]{1,64}))?$|chat$|search$|vod\/\d+$|clip\/\d+$|recap\/\d+$|p\/[A-Za-z0-9_-]+$|@[A-Za-z0-9_]{3,24}$)/;
+const SEO_ROUTE_RE = /^\/(?:$|content$|moments$|vods$|clips$|pastes$|rundown(?:\/(?:topics|t\/[a-z0-9-]{1,80}|e\/\d{1,12}|[A-Za-z0-9_.-]{1,64}))?$|chat$|search$|documentation$|updates$|vod\/\d+$|clip\/\d+$|recap\/\d+$|p\/[A-Za-z0-9_-]+$|@[A-Za-z0-9_]{3,24}$)/;
/** ?page=N on a channel page (its video list); 1 everywhere else and for anything that is not a number. */
/** ?page=N on a channel page (its video list); 1 everywhere else and for anything that is not a number. */
function _pageParam(p, query) {
function _pageParam(p, query) {
@@ -103,6 +103,7 @@ function get(p, headers = {}) {
@@ -103,6 +103,7 @@ function get(p, headers = {}) {
}
}
const head = (html) => html.slice(0, html.search(/<\/head>/i));
const head = (html) => html.slice(0, html.search(/<\/head>/i));
const titleOf = (html) => ((head(html).match(/<title>([^<]*)<\/title>/) || [])[1] || '').replace(/'/g, "'");
const titleOf = (html) => ((head(html).match(/<title>([^<]*)<\/title>/) || [])[1] || '').replace(/'/g, "'");
+const descOf = (html) => ((head(html).match(/<meta name="description" content="([^"]*)">/) || [])[1] || '');
const canonicalsOf = (html) => [...head(html).matchAll(/<link rel="canonical" href="([^"]+)">/g)].map((m) => m[1]);
const canonicalsOf = (html) => [...head(html).matchAll(/<link rel="canonical" href="([^"]+)">/g)].map((m) => m[1]);
const robotsOf = (html) => [...head(html).matchAll(/<meta name="robots" content="([^"]+)">/g)].map((m) => m[1]);
const robotsOf = (html) => [...head(html).matchAll(/<meta name="robots" content="([^"]+)">/g)].map((m) => m[1]);
const ldOf = (html) => [...head(html).matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => { try { return JSON.parse(m[1]); } catch { return null; } }).filter(Boolean);
const ldOf = (html) => [...head(html).matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => { try { return JSON.parse(m[1]); } catch { return null; } }).filter(Boolean);
@@ -222,6 +223,27 @@ const server = http.createServer(app).listen(0, '127.0.0.1', async () => {
@@ -222,6 +223,27 @@ const server = http.createServer(app).listen(0, '127.0.0.1', async () => {
assert.ok(/<meta name="robots" content="noindex,follow">/.test(head(search)));
assert.ok(/<meta name="robots" content="noindex,follow">/.test(head(search)));
});
});
+ await check('/documentation and /updates have their own title, description and canonical (they repeated the home page\'s)', async () => {
+ const homeDesc = descOf((await get('/')).body);
+ for (const [path, title] of [['/documentation', 'API docs | OpenVibe.Live'], ['/updates', 'Updates | OpenVibe.Live']]) {
+ const body = (await get(path)).body;
+ assert.deepStrictEqual(canonicalsOf(body), [`https://openvibe.live${path}`], path);
+ assert.strictEqual(titleOf(body), title);
+ const desc = descOf(body);
+ assert.ok(desc && desc !== homeDesc && desc.length <= 160, `${path}: ${desc}`);
+ }
+ });
+
+ await check('the legal pages name themselves canonical and carry share tags', () => {
+ for (const name of ['tos', 'privacy', 'dmca']) {
+ 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(!/for the OpenVibe\b/.test(html.slice(0, html.indexOf('</head>'))), `${name}: "for the OpenVibe" reads wrong`);
+ }
+ });
+
await check('the home page\'s JSON-LD lists are named by the snapshot\'s visible headings', async () => {
await check('the home page\'s JSON-LD lists are named by the snapshot\'s visible headings', async () => {
const home = (await get('/')).body;
const home = (await get('/')).body;
const lists = ldOf(home).filter((n) => n['@type'] === 'ItemList');
const lists = ldOf(home).filter((n) => n['@type'] === 'ItemList');