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)

OP OpenVibers committed 24 days ago · 519e3ad08c · 5 files, +53 −4

Changes

ddiff --git a/public/dmca.html b/public/dmca.html
iindex af842be1..c507fb71 100644
---- a/public/dmca.html
++++ b/public/dmca.html
@@@ -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>
ddiff --git a/public/privacy.html b/public/privacy.html
iindex aa1f15d9..d7b431ef 100644
---- a/public/privacy.html
++++ b/public/privacy.html
@@@ -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>
ddiff --git a/public/tos.html b/public/tos.html
iindex a4772ca4..84b7aab7 100644
---- a/public/tos.html
++++ b/public/tos.html
@@@ -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>
ddiff --git a/server/seo/pages.js b/server/seo/pages.js
iindex 922c1380..8457a68d 100644
---- a/server/seo/pages.js
++++ b/server/seo/pages.js
@@@ -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) {
ddiff --git a/test/seo-ssr.test.js b/test/seo-ssr.test.js
iindex 7ee52f79..a1c5c8ed 100644
---- a/test/seo-ssr.test.js
++++ b/test/seo-ssr.test.js
@@@ -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(/&#39;/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');