openvibers/openvibe.space Commits

Share the content-hashed Space card (#202)

OP OpenVibers committed 7 days ago · af6830ecba · 2 files, +24 −4

Every page now advertises openvibe-shared/social's 1200x630 space 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.

Changes

ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex 332779b..a5fc61b 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -13,6 +13,7 @@ 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');
/** Escape a value for HTML text or a quoted attribute. */
function escapeHtml(s) {
@@@ -22,7 +23,8 @@ function escapeHtml(s) {
const SITE_NAME = 'OpenVibe.Space';
const NETWORK_URL = 'https://openvibe.network';
const DEFAULT_DESCRIPTION = 'OpenVibe.Space: apps on your OpenVibe account, built with OpenVibe AI or from a template, and static hosting with immutable deploys and rollback.';
--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('space')}`;
// 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).
@@@ -118,6 +120,7 @@ function renderPage(o) {
const nav = navbarInit(o);
const foot = footerInit(o);
const feeds = o.feeds || [];
++ 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) {
siteName: SITE_NAME, description, canonical, robots: o.robots || 'index,follow',
agentLayer: o.agentLayer,
llms: o.llms,
-- 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,
summary: description, url: canonical, updated: o.modified,
navbar: nav, home: '/', navLinks: nav.links.map(({ label, href }) => ({ label, href })),
@@@ -137,6 +140,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: 'space', 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)}">`),
`<link rel="stylesheet" href="${asset('css/space.css')}">`,
@@@ -160,8 +165,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;
}
module.exports = { renderPage, asset, assetVersion, abs, setRelease, escapeHtml, SITE_NAME, DEFAULT_DESCRIPTION, DEFAULT_OG_IMAGE };
ddiff --git a/test/home.test.js b/test/home.test.js
iindex 4f46b06..83364c1 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -37,6 +37,22 @@ const { boot, check, done } = require('./helpers/app');
assert.doesNotMatch(r.text, /href="\/s"/, 'nothing links a /s that only redirects');
});
++ 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\.space\/shared\/social\/space\.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('/updates is this site\'s own update log, and the footer links it', async () => {
const r = await t.get('/updates');
assert.strictEqual(r.status, 200);