openvibers/openvibe.tools Commits
Tools: links unfurl with the site's own share card (og:image was a 404 on openvibe.network) (#61)
Changes
@@@ -13,6 +13,7 @@const crypto = require('crypto');const frame = require('openvibe-shared/frame');const ovServe = require('openvibe-shared/serve');++const social = require('openvibe-shared/social');const express = require('express');const seo = require('openvibe-shared/seo');const icons = require('openvibe-shared/icons');@@@ -135,7 +136,9 @@ ${appIcon.headTags({ site: 'tools', iconBase: '/assets' })}}function head(o) {-- return seo.headTags(Object.assign({ siteName: NAME, image: 'https://openvibe.network/assets/og-tools.png', type: 'website', locale: 'en_US' }, o));++ // The site's own social card from its pinned Frame files (openvibe-shared social/tools.jpg), like every other site;++ // the old openvibe.network/assets/og-tools.png no longer exists (404).++ return seo.headTags(Object.assign({ siteName: NAME, image: `${SITE}${social.url('tools')}`, type: 'website', locale: 'en_US' }, o));}const searchForm = (q) => `<form class="search" action="/search" method="get" role="search"><input type="search" name="q" id="q" value="${esc(q || '')}" placeholder="What do you need to do? Try “merge pdf” or “dns lookup”" aria-label="Search tools" autocomplete="off"><button type="submit">Search</button></form>`;@@@ -17,6 +17,7 @@const fs = require('fs');const path = require('path');const { seoFor, relatedTo, BRAND } = require('./catalog');++const social = require('openvibe-shared/social');const SITE = 'https://openvibe.tools';const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));@@@ -118,7 +119,15 @@ function renderTool(file, sub, canonicalHost) {html = html.replace(/<meta\s+property=["']og:url["'][^>]*>/i, `<meta property="og:url" content="${url}">`);html = html.replace(/<link\s+rel=["']canonical["'][^>]*>/i, `<link rel="canonical" href="${url}">`);++ // A tool page whose own HTML names no share image gets the Tools card (its large-image Twitter card had nothing to++ // show: dev, dns, fancy, logo, net, regex, ssl and whois, 2026-10-15). A page with its own image keeps it.++ const card = `https://openvibe.tools${social.url('tools')}`;++ const image = /<meta\s+property=["']og:image["']/i.test(html) ? [] : [++ `<meta property="og:image" content="${card}">`, '<meta property="og:image:width" content="1200">', '<meta property="og:image:height" content="630">',++ `<meta name="twitter:image" content="${card}">`,++ ];const extraHead = [++ ...image,`<meta name="ov-tool" content="${esc(sub)}">`, // the page script reads this: the host may be descriptive, a mirror or a custom domain`<meta name="keywords" content="${esc(rec.kw || '')}">`,'<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">',@@@ -25,6 +25,9 @@ const site = require('../server/pages/site');assert.ok(html.includes('.cta{display:inline-flex;align-items:center;gap:8px;background:var(--accent-strong,#1d4ed8);color:var(--on-accent-strong,#fff);'));// A tool page's "Primary" address tag sits on the accent glow: muted text there read 3.16:1 (axe, /tool/yaml).assert.ok(html.includes('.hosts li.primary .tag{color:var(--text-primary,#e6e9ef)}'));++ // The share card is the site's own pinned copy (openvibe.network/assets/og-tools.png was a 404, 2026-10-15).++ const og = (/<meta property="og:image" content="([^"]+)"/.exec(html) || [])[1];++ assert.match(String(og), /^https:\/\/openvibe\.tools\/shared\/social\/tools\.jpg\?v=[0-9a-f]+$/, `og:image ${og}`);console.log('developers page: all checks passed');} finally { srv.close(); }})().catch((e) => { console.error(e); process.exit(1); });nnew file mode 100644@@@ -0,0 +1,29 @@++'use strict';++// A tool subdomain's page always has a share image: one whose own HTML names none (dev, dns, regex, …) gets the Tools++// card from the site's pinned Frame files, with twitter:image and its size; one that names its own keeps it, once.++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { renderTool } = require('../server/seo/render');++++const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tools-seo-'));++const page = (name, extra = '') => {++ const file = path.join(dir, `${name}.html`);++ fs.writeFileSync(file, `<!doctype html><html><head><title>x</title><meta name="description" content="x"><meta property="og:title" content="x"><meta property="og:description" content="x"><meta property="og:url" content="x"><link rel="canonical" href="x">${extra}</head><body><main></main></body></html>`);++ return file;++};++try {++ const bare = renderTool(page('bare'), 'dev');++ const og = (/<meta property="og:image" content="([^"]+)">/.exec(bare) || [])[1];++ assert.match(String(og), /^https:\/\/openvibe\.tools\/shared\/social\/tools\.jpg\?v=[0-9a-f]+$/, `og:image ${og}`);++ assert.ok(bare.includes(`<meta name="twitter:image" content="${og}">`), 'twitter:image');++ assert.ok(bare.includes('<meta property="og:image:width" content="1200">') && bare.includes('<meta property="og:image:height" content="630">'));++++ const own = renderTool(page('own', '<meta property="og:image" content="https://img.openvibe.tools/og.png">'), 'img');++ assert.strictEqual((own.match(/property="og:image"/g) || []).length, 1, 'one og:image');++ assert.ok(own.includes('content="https://img.openvibe.tools/og.png"'), 'the page keeps its own image');++ console.log('seo share image: all checks passed');++} finally {++ fs.rmSync(dir, { recursive: true, force: true });++}nnew file mode 100644@@@ -0,0 +1,9 @@++lockfileVersion: '9.0'++++settings:++ autoInstallPeers: true++ excludeLinksFromLockfile: false++++importers:++++ .: {}
@@ -13,6 +13,7 @@
@@ -13,6 +13,7 @@
const crypto = require('crypto');
const crypto = require('crypto');
const frame = require('openvibe-shared/frame');
const frame = require('openvibe-shared/frame');
const ovServe = require('openvibe-shared/serve');
const ovServe = require('openvibe-shared/serve');
+const social = require('openvibe-shared/social');
const express = require('express');
const express = require('express');
const seo = require('openvibe-shared/seo');
const seo = require('openvibe-shared/seo');
const icons = require('openvibe-shared/icons');
const icons = require('openvibe-shared/icons');
@@ -135,7 +136,9 @@ ${appIcon.headTags({ site: 'tools', iconBase: '/assets' })}
@@ -135,7 +136,9 @@ ${appIcon.headTags({ site: 'tools', iconBase: '/assets' })}
}
}
function head(o) {
function head(o) {
- return seo.headTags(Object.assign({ siteName: NAME, image: 'https://openvibe.network/assets/og-tools.png', type: 'website', locale: 'en_US' }, o));
+ // The site's own social card from its pinned Frame files (openvibe-shared social/tools.jpg), like every other site;
+ // the old openvibe.network/assets/og-tools.png no longer exists (404).
+ return seo.headTags(Object.assign({ siteName: NAME, image: `${SITE}${social.url('tools')}`, type: 'website', locale: 'en_US' }, o));
}
}
const searchForm = (q) => `<form class="search" action="/search" method="get" role="search"><input type="search" name="q" id="q" value="${esc(q || '')}" placeholder="What do you need to do? Try “merge pdf” or “dns lookup”" aria-label="Search tools" autocomplete="off"><button type="submit">Search</button></form>`;
const searchForm = (q) => `<form class="search" action="/search" method="get" role="search"><input type="search" name="q" id="q" value="${esc(q || '')}" placeholder="What do you need to do? Try “merge pdf” or “dns lookup”" aria-label="Search tools" autocomplete="off"><button type="submit">Search</button></form>`;
@@ -17,6 +17,7 @@
@@ -17,6 +17,7 @@
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
const { seoFor, relatedTo, BRAND } = require('./catalog');
const { seoFor, relatedTo, BRAND } = require('./catalog');
+const social = require('openvibe-shared/social');
const SITE = 'https://openvibe.tools';
const SITE = 'https://openvibe.tools';
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
@@ -118,7 +119,15 @@ function renderTool(file, sub, canonicalHost) {
@@ -118,7 +119,15 @@ function renderTool(file, sub, canonicalHost) {
html = html.replace(/<meta\s+property=["']og:url["'][^>]*>/i, `<meta property="og:url" content="${url}">`);
html = html.replace(/<meta\s+property=["']og:url["'][^>]*>/i, `<meta property="og:url" content="${url}">`);
html = html.replace(/<link\s+rel=["']canonical["'][^>]*>/i, `<link rel="canonical" href="${url}">`);
html = html.replace(/<link\s+rel=["']canonical["'][^>]*>/i, `<link rel="canonical" href="${url}">`);
+ // A tool page whose own HTML names no share image gets the Tools card (its large-image Twitter card had nothing to
+ // show: dev, dns, fancy, logo, net, regex, ssl and whois, 2026-10-15). A page with its own image keeps it.
+ const card = `https://openvibe.tools${social.url('tools')}`;
+ const image = /<meta\s+property=["']og:image["']/i.test(html) ? [] : [
+ `<meta property="og:image" content="${card}">`, '<meta property="og:image:width" content="1200">', '<meta property="og:image:height" content="630">',
+ `<meta name="twitter:image" content="${card}">`,
+ ];
const extraHead = [
const extraHead = [
+ ...image,
`<meta name="ov-tool" content="${esc(sub)}">`, // the page script reads this: the host may be descriptive, a mirror or a custom domain
`<meta name="ov-tool" content="${esc(sub)}">`, // the page script reads this: the host may be descriptive, a mirror or a custom domain
`<meta name="keywords" content="${esc(rec.kw || '')}">`,
`<meta name="keywords" content="${esc(rec.kw || '')}">`,
'<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">',
'<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">',
@@ -25,6 +25,9 @@ const site = require('../server/pages/site');
@@ -25,6 +25,9 @@ const site = require('../server/pages/site');
assert.ok(html.includes('.cta{display:inline-flex;align-items:center;gap:8px;background:var(--accent-strong,#1d4ed8);color:var(--on-accent-strong,#fff);'));
assert.ok(html.includes('.cta{display:inline-flex;align-items:center;gap:8px;background:var(--accent-strong,#1d4ed8);color:var(--on-accent-strong,#fff);'));
// A tool page's "Primary" address tag sits on the accent glow: muted text there read 3.16:1 (axe, /tool/yaml).
// A tool page's "Primary" address tag sits on the accent glow: muted text there read 3.16:1 (axe, /tool/yaml).
assert.ok(html.includes('.hosts li.primary .tag{color:var(--text-primary,#e6e9ef)}'));
assert.ok(html.includes('.hosts li.primary .tag{color:var(--text-primary,#e6e9ef)}'));
+ // The share card is the site's own pinned copy (openvibe.network/assets/og-tools.png was a 404, 2026-10-15).
+ const og = (/<meta property="og:image" content="([^"]+)"/.exec(html) || [])[1];
+ assert.match(String(og), /^https:\/\/openvibe\.tools\/shared\/social\/tools\.jpg\?v=[0-9a-f]+$/, `og:image ${og}`);
console.log('developers page: all checks passed');
console.log('developers page: all checks passed');
} finally { srv.close(); }
} finally { srv.close(); }
})().catch((e) => { console.error(e); process.exit(1); });
})().catch((e) => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
+'use strict';
+// A tool subdomain's page always has a share image: one whose own HTML names none (dev, dns, regex, …) gets the Tools
+// card from the site's pinned Frame files, with twitter:image and its size; one that names its own keeps it, once.
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { renderTool } = require('../server/seo/render');
+
+const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tools-seo-'));
+const page = (name, extra = '') => {
+ const file = path.join(dir, `${name}.html`);
+ fs.writeFileSync(file, `<!doctype html><html><head><title>x</title><meta name="description" content="x"><meta property="og:title" content="x"><meta property="og:description" content="x"><meta property="og:url" content="x"><link rel="canonical" href="x">${extra}</head><body><main></main></body></html>`);
+ return file;
+};
+try {
+ const bare = renderTool(page('bare'), 'dev');
+ const og = (/<meta property="og:image" content="([^"]+)">/.exec(bare) || [])[1];
+ assert.match(String(og), /^https:\/\/openvibe\.tools\/shared\/social\/tools\.jpg\?v=[0-9a-f]+$/, `og:image ${og}`);
+ assert.ok(bare.includes(`<meta name="twitter:image" content="${og}">`), 'twitter:image');
+ assert.ok(bare.includes('<meta property="og:image:width" content="1200">') && bare.includes('<meta property="og:image:height" content="630">'));
+
+ const own = renderTool(page('own', '<meta property="og:image" content="https://img.openvibe.tools/og.png">'), 'img');
+ assert.strictEqual((own.match(/property="og:image"/g) || []).length, 1, 'one og:image');
+ assert.ok(own.includes('content="https://img.openvibe.tools/og.png"'), 'the page keeps its own image');
+ console.log('seo share image: all checks passed');
+} finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .: {}