openvibers/openvibe.community Commits

Share the content-hashed Community card (#255)

OP OpenVibers committed 7 days ago · d05d5df84d · 4 files, +28 −6

* Share the content-hashed Community card, not og-default.png

Every page now advertises openvibe-shared/social's 1200x630 community card (with its ?v content hash, dimensions and the Twitter mirror) instead of the retired og-default.png. The old file is still served for links already shared.

* The home's SSR test expects the large share card it now has

Changes

ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex 2da15ce..3f00c10 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -14,12 +14,14 @@ const appIcon = require('openvibe-shared/app-icon');
const fs = require('fs');
const path = require('path');
const config = require('../config');
++const social = require('openvibe-shared/social');
const { escapeHtml } = require('./highlight');
const SITE_NAME = 'OpenVibe.Community';
const NETWORK_URL = 'https://openvibe.network';
const DEFAULT_DESCRIPTION = 'The people of OpenVibe — a community-run, open source home for pastes, forums and threads, and submissions. Free speech within the rules.';
--const DEFAULT_OG_IMAGE = `${config.baseUrl}/og-default.png`;
++// The site's content-hashed share card (openvibe-shared/social), served from this site's own /shared copy.
++const DEFAULT_OG_IMAGE = `${config.baseUrl}${social.url('community')}`;
// Content-hashed asset URLs so browsers and nginx can cache them for a year and still pick up
// every deploy (the same scheme Live uses in server/web/assets.js).
@@@ -120,6 +122,7 @@ function renderPage(o) {
const nav = navbarInit(o);
const foot = footerInit(o);
const feeds = o.feeds || [{ title: `${SITE_NAME} — latest pastes`, href: '/feed.xml' }];
++ const image = o.ogImage || DEFAULT_OG_IMAGE;
// shell.page writes the document, the SEO head and ai-summary, the theme-loader, navbar.js/footer.js
// with the navbar init, the noscript nav and the SSR footer; the rest of the head is this site's own.
@@@ -128,7 +131,7 @@ function renderPage(o) {
title: o.title || `${SITE_NAME} — the people of OpenVibe`,
titleSuffix: o.title ? ` · ${SITE_NAME}` : undefined,
siteName: SITE_NAME, description, canonical, robots: o.robots || 'index,follow',
-- type: o.ogType || 'website', image: o.ogImage || DEFAULT_OG_IMAGE, imageAlt: o.imageAlt,
++ type: o.ogType || 'website', image, imageAlt: o.imageAlt,
jsonLd: o.jsonLd, alternates: o.alternates,
// The page names its Markdown twin (openvibe-shared/agent-layer) in the head; only pages whose
// twin is mounted set o.agentLayer (the home, /pastes and a paste).
@@@ -140,6 +143,8 @@ function renderPage(o) {
o.published ? `<meta property="article:published_time" content="${escapeHtml(o.published)}">` : '',
o.modified ? `<meta property="article:modified_time" content="${escapeHtml(o.modified)}">` : '',
appIcon.headTags({ site: 'community', iconBase: '/assets' }),
++ // The shared card is 1200×630; a page's own image carries its own dimensions.
++ image === DEFAULT_OG_IMAGE ? '<meta property="og:image:width" content="1200">\n<meta property="og:image:height" content="630">' : '',
...feeds.map((f) => `<link rel="alternate" type="application/rss+xml" title="${escapeHtml(f.title)}" href="${escapeHtml(f.href)}">`),
...(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),
...(o.inlineStyles || []).map((css) => `<style>${css}</style>`),
@@@ -167,8 +172,7 @@ document.addEventListener('DOMContentLoaded', function () {
});
</script>`,
});
-- // The default share image is a small card: only a page's own image gets the large Twitter card.
-- return o.ogImage ? html : html.replace('<meta name="twitter:card" content="summary_large_image">', '<meta name="twitter:card" content="summary">');
++ return html;
}
/**
ddiff --git a/test/discovery.test.js b/test/discovery.test.js
iindex 0b3aea0..ed5274a 100644
---- a/test/discovery.test.js
++++ b/test/discovery.test.js
@@@ -84,7 +84,9 @@ const { check, done } = require('./helpers/app');
assert.strictEqual((head.match(/<title>/g) || []).length, 1);
assert.strictEqual((head.match(/<link rel="canonical" href="https:\/\/openvibe.community\/p\/x">/g) || []).length, 1);
assert.ok(head.includes('<meta property="og:type" content="article">'));
-- assert.ok(head.includes('<meta name="twitter:card" content="summary">'), 'no large card without a page image');
++ assert.ok(head.includes('<meta name="twitter:card" content="summary_large_image">'), 'the content-hashed site card is large');
++ assert.match(head, /<meta property="og:image" content="https:\/\/openvibe\.community\/shared\/social\/community\.jpg\?v=[0-9a-f]{12}">/);
++ assert.ok(head.includes('<meta property="og:image:width" content="1200">') && head.includes('<meta property="og:image:height" content="630">'));
assert.ok(head.includes('<meta name="ai-summary" content="A paste.">'));
assert.ok(head.includes('<meta property="article:published_time" content="2026-09-15T10:00:00.000Z">'));
assert.ok(head.includes('"name":"\\u003c/script>\\u003cb>"') && !head.includes('</script><b>'));
ddiff --git a/test/seo.test.js b/test/seo.test.js
iindex 4609792..2f221b5 100644
---- a/test/seo.test.js
++++ b/test/seo.test.js
@@@ -139,6 +139,22 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual((head.match(/<link rel="canonical"/g) || []).length, 1, 'one canonical');
});
++ await check('the home page shares the site card, content-hashed, with its 1200×630 size and the Twitter mirror', async () => {
++ const r = await t.get('/');
++ assert.strictEqual(r.status, 200);
++ const head = r.text.split('</head>')[0];
++ const image = /<meta property="og:image" content="([^"]+)">/.exec(head)?.[1] || '';
++ assert.match(image, /^https:\/\/openvibe\.community\/shared\/social\/community\.jpg\?v=[0-9a-f]{12}$/);
++ assert.ok(head.includes('<meta property="og:image:width" content="1200">'));
++ assert.ok(head.includes('<meta property="og:image:height" content="630">'));
++ assert.ok(head.includes('<meta name="twitter:card" content="summary_large_image">'));
++ assert.ok(head.includes(`<meta name="twitter:image" content="${image}">`));
++ const path = new URL(image).pathname + new URL(image).search;
++ const card = await t.get(path);
++ assert.strictEqual(card.status, 200, path);
++ assert.ok(card.headers.get('content-type').startsWith('image/jpeg'));
++ });
++
await check('HTML pages carry the private page policy; raw and download never reach a shared cache', async () => {
for (const path of ['/', '/pastes', '/p/amber-fox-42', '/updates', '/nope-not-a-page']) {
const r = await t.get(path);
ddiff --git a/test/ssr.test.js b/test/ssr.test.js
iindex ef2d176..2b0aeb4 100644
---- a/test/ssr.test.js
++++ b/test/ssr.test.js
@@@ -26,7 +26,7 @@ const { boot, check, done } = require('./helpers/app');
has(r.text, '<title>OpenVibe.Community: pastes, forums and the OpenVibe roadmap</title>');
has(r.text, '<link rel="canonical" href="https://openvibe.community/">');
has(r.text, '<meta property="og:type" content="website">');
-- has(r.text, '<meta name="twitter:card" content="summary">');
++ has(r.text, '<meta name="twitter:card" content="summary_large_image">');
has(r.text, '"@type":"WebSite"');
has(r.text, '/shared/theme-loader.js?v=');
has(r.text, '/shared/navbar.js?v=');