openvibers/openvibe.tools Commits

Tools: links unfurl with the site's own share card (og:image was a 404 on openvibe.network) (#61)

OP OpenVibers committed 29 days ago · aa94b517ab · 5 files, +54 −1

* Tools: links unfurl with the site's own share card (og:image was a 404 on openvibe.network)

**openvibe.tools links unfurl with their share card again: og:image is the site's own pinned card.**

**Found by:** a check of every OpenVibe home's social tags. openvibe.tools was the only one whose `og:image`/`twitter:image` failed: `https://openvibe.network/assets/og-tools.png` answers 404, so a shared Tools link showed no picture on X, Discord or Slack.

**Change (`apps/gateway/server/pages/site.js`):** the gateway's head uses `openvibe-shared/social`'s `url('tools')`, so the image is `https://openvibe.tools/shared/social/tools.jpg?v=<content hash>`. That is the 1200 × 630 card the shared Frame draws for every site, served from this site's pinned copy (it already answers 200, image/jpeg). Every other site gets its card the same way.

**Test:** `developers-page.test.js` checks that `og:image` is the pinned card. The gateway tests that load `site.js` pass: developers page, llms-full, registry, shipped pages and services registry.

* Tools: a tool subdomain whose page names no share image gets the Tools card (dev, dns, fancy, logo, net, regex, ssl, whois)

Changes

ddiff --git a/apps/gateway/server/pages/site.js b/apps/gateway/server/pages/site.js
iindex 981e63f..cf3f5df 100644
---- a/apps/gateway/server/pages/site.js
++++ b/apps/gateway/server/pages/site.js
@@@ -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>`;
ddiff --git a/apps/gateway/server/seo/render.js b/apps/gateway/server/seo/render.js
iindex 15a237c..1f07e67 100644
---- a/apps/gateway/server/seo/render.js
++++ b/apps/gateway/server/seo/render.js
@@@ -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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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">',
ddiff --git a/apps/gateway/test/developers-page.test.js b/apps/gateway/test/developers-page.test.js
iindex 3dcb1bf..f20b2cf 100644
---- a/apps/gateway/test/developers-page.test.js
++++ b/apps/gateway/test/developers-page.test.js
@@@ -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); });
ddiff --git a/apps/gateway/test/seo-share-image.test.js b/apps/gateway/test/seo-share-image.test.js
nnew file mode 100644
iindex 0000000..a42250f
---- /dev/null
++++ b/apps/gateway/test/seo-share-image.test.js
@@@ -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 });
++}
ddiff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
nnew file mode 100644
iindex 0000000..9b60ae1
---- /dev/null
++++ b/pnpm-lock.yaml
@@@ -0,0 +1,9 @@
++lockfileVersion: '9.0'
++
++settings:
++ autoInstallPeers: true
++ excludeLinksFromLockfile: false
++
++importers:
++
++ .: {}