openvibers/openvibe.community Commits
Render every Community page through openvibe-shared/shell v2.6.0
Changes
@@@ -19,7 +19,7 @@"multer": "^2.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0","pg": "^8.23.0"},"devDependencies": {@@@ -1236,9 +1236,9 @@}},"node_modules/openvibe-shared": {-- "version": "2.5.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",-- "integrity": "sha512-fHuUS6SXM7ezatfAyqLm1V8B6puV9YXSnz6aG6XQbWx28qghRCCHeUF3lr4nxiCGdn9SyikYtIyLGL5r1ycp9Q==",++ "version": "2.6.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",++ "integrity": "sha512-IKKvWTM/9UonQ4msrzM4yir0QcTUNbhyqeN1r5ywWavU4VTsL1sI5QdHQlxge7BAsMrQ4OvMEPlHjuAfUJM3rg==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -30,7 +30,7 @@"multer": "^2.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0","pg": "^8.23.0"},"engines": {@@@ -1,15 +1,16 @@'use strict';/**-- * Page shell — every page on the site is server-rendered through this: full <head> SEO from-- * openvibe-shared/seo (description, canonical, robots, Open Graph, Twitter card, JSON-LD and the-- * ai-summary page summary), the shared-- * OpenVibe Frame (theme-loader first so there is no flash, navbar.js + footer.js from the-- * Network), this site's small stylesheet and its progressive script.++ * Page shell — every page on the site is server-rendered through openvibe-shared/shell: full <head>++ * SEO (description, canonical, robots, Open Graph, Twitter card, JSON-LD and the ai-summary page++ * summary), the shared OpenVibe Frame (theme-loader first so there is no flash, navbar.js +++ * footer.js from the Network, the SSR footer), plus this site's small stylesheet and its++ * progressive script.*/const crypto = require('crypto');const ovServe = require('openvibe-shared/serve');--const seo = require('openvibe-shared/seo');++const shell = require('openvibe-shared/shell');++const appIcon = require('openvibe-shared/app-icon');const fs = require('fs');const path = require('path');const config = require('../config');@@@ -110,52 +111,51 @@ function footerInit(opts) {* published/modified (ISO, for article:*), noFrame (error pages during outages)*/function renderPage(o) {-- const title = o.title ? `${o.title} · ${SITE_NAME}` : `${SITE_NAME} — the people of OpenVibe`;const description = fitDescription((o.description || DEFAULT_DESCRIPTION).replace(/\s+/g, ' ').trim().slice(0, 300));const canonical = abs(o.canonicalPath || '/');-- const robots = o.robots || 'index,follow';-- const ogType = o.ogType || 'website';-- const ogImage = o.ogImage || DEFAULT_OG_IMAGE;const nav = navbarInit(o);const foot = footerInit(o);++ const feeds = o.feeds || [{ title: `${SITE_NAME} — latest pastes`, href: '/feed.xml' }];-- return `<!DOCTYPE html>--<html lang="en" data-page="${escapeHtml(o.active || 'page')}">--<head>--<meta charset="utf-8">--<meta name="viewport" content="width=device-width, initial-scale=1">--<title>${escapeHtml(title)}</title>--${seo.headTags({ title: o.title || SITE_NAME, noTitle: true, siteName: SITE_NAME, description, canonical, robots, type: ogType, image: ogImage, imageAlt: o.imageAlt, largeImage: !!o.ogImage, jsonLd: o.jsonLd, alternates: o.alternates })}--${o.published ? `<meta property="article:published_time" content="${escapeHtml(o.published)}">` : ''}--${o.modified ? `<meta property="article:modified_time" content="${escapeHtml(o.modified)}">` : ''}--${seo.pageSummary({ title: o.title || SITE_NAME, summary: description, url: canonical, updated: o.modified })}--${require('openvibe-shared/app-icon').headTags({ site: 'community', iconBase: '/assets' })}--${(o.feeds || [{ title: `${SITE_NAME} — latest pastes`, href: '/feed.xml' }]).map((f) => `<link rel="alternate" type="application/rss+xml" title="${escapeHtml(f.title)}" href="${escapeHtml(f.href)}">`).join('\n')}--<script src="${ovServe.url('theme-loader.js')}" defer></script>--${(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`).join('\n')}--<link rel="stylesheet" href="${asset('css/community.css')}">--<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">--<script src="${ovServe.url('navbar.js')}" defer></script>--<script src="${ovServe.url('footer.js')}" defer></script>--<script src="${asset('js/community.js')}" defer></script>--<meta name="ov-boost" content="community@${escapeHtml(RELEASE)}">--<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>--</head>--<body class="${escapeHtml(o.bodyClass || '')}">--<div id="navbar-mount"></div>++ // 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.++ const html = shell.page({++ name: SITE_NAME, service: 'community', lang: 'en',++ 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,++ jsonLd: o.jsonLd, alternates: o.alternates,++ summary: description, url: canonical, updated: o.modified,++ navbar: nav, home: '/', navLinks: nav.links.map(({ label, href }) => ({ label, href })),++ footer: { service: 'community', variant: 'full', updates: '/updates' },++ head: [++ 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' }),++ ...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/community.css')}">`,++ '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">',++ `<script src="${asset('js/community.js')}" defer></script>`,++ `<meta name="ov-boost" content="community@${escapeHtml(RELEASE)}">`,++ `<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,++ ].filter(Boolean).join('\n'),++ bodyClass: o.bodyClass,++ bodyAttributes: { 'data-page': o.active || 'page' },++ body: `<div id="navbar-mount"></div><main id="main" class="page">${o.body || ''}</main>--${require('openvibe-shared/frame').footer({ service: 'community', variant: 'full', updates: '/updates' })}<script>window.__OV_PAGE = ${JSON.stringify({ navbar: nav, footer: foot }).replace(/</g, '\\u003c')};document.addEventListener('DOMContentLoaded', function () {-- try { if (window.OpenVibeNavbar) OpenVibeNavbar.init(window.__OV_PAGE.navbar); } catch (e) { /* the Frame is optional */ }-- try { if (window.OpenVibeFooter) OpenVibeFooter.init(window.__OV_PAGE.footer); } catch (e) { /* */ }++ try { if (window.OpenVibeFooter) OpenVibeFooter.init(window.__OV_PAGE.footer); } catch (e) { /* the SSR footer stays */ }});--</script>--</body>--</html>`;++</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">');}module.exports = { renderPage, asset, assetVersion, abs, setRelease, SITE_NAME, DEFAULT_DESCRIPTION, DEFAULT_OG_IMAGE };@@@ -97,10 +97,10 @@ const { boot, check, done } = require('./helpers/app');const head = r.text.split('</head>')[0];assert.ok(head.includes('<link rel="canonical" href="https://openvibe.community/p/amber-fox-42">'));assert.ok(head.includes('<meta property="og:url" content="https://openvibe.community/p/amber-fox-42">'));-- assert.ok(head.includes('<meta property="og:title" content="Hello world in JavaScript">'));++ assert.ok(head.includes('<meta property="og:title" content="Hello world in JavaScript · OpenVibe.Community">'));assert.ok(head.includes('<meta property="og:site_name" content="OpenVibe.Community">'));assert.match(head, /<meta property="og:description" content="[^"]+">/);-- assert.match(head, /<meta name="twitter:title" content="Hello world in JavaScript">/);++ assert.match(head, /<meta name="twitter:title" content="Hello world in JavaScript · OpenVibe.Community">/);assert.match(head, /<meta name="ai-summary" content="[^"]+">/);assert.match(head, /<meta property="article:published_time" content="2026-09-15T10:00:00.000Z">/);assert.ok(head.includes('"@type":"BreadcrumbList"') && head.includes('"@type":"WebPage"'));@@@ -57,6 +57,30 @@ const { boot, check, done } = require('./helpers/app');assert.ok(!/\bfree\b(?! speech)/i.test(r.text.replace(/<script[\s\S]*?<\/script>/g, '')), 'no "free" claims outside "free speech"');});++ await check('the home head comes whole from openvibe-shared/shell plus this site\'s extras', async () => {++ const r = await t.get('/');++ assert.ok(r.text.startsWith('<!doctype html>'));++ const head = r.text.split('</head>')[0];++ assert.strictEqual((head.match(/<title>/g) || []).length, 1, 'one title');++ has(head, '<title>OpenVibe.Community — the people of OpenVibe</title>');++ assert.strictEqual((head.match(/<link rel="canonical" href="https:\/\/openvibe\.community\/">/g) || []).length, 1, 'one canonical');++ has(head, '<meta name="robots" content="index,follow');++ has(head, '<script type="application/ld+json">');++ has(head, '"@type":"WebSite"');++ has(head, '<meta name="ai-summary" content="');++ has(head, '<link rel="alternate" type="application/rss+xml" title="OpenVibe.Community — latest pastes" href="/feed.xml">');++ has(head, '/css/community.css?v=');++ has(head, '/js/community.js?v=');++ has(head, 'font-awesome');++ has(head, '<link rel="icon"');++ assert.match(head, /<meta name="ov-boost" content="community@[^"]+">/);++ for (const js of ['theme-loader.js', 'navbar.js', 'footer.js', 'boost.js']) has(head, `/shared/${js}?v=`);++ assert.match(r.text, /<body[^>]* data-page="home">/);++ has(r.text, '<div id="navbar-mount"></div>');++ has(r.text, 'id="ov-footer"');++ has(r.text, 'OpenVibeFooter.init(window.__OV_PAGE.footer)');++ });++await check('every page carries the boost marker and script, and the navbar signs in back to the current page', async () => {for (const url of ['/', '/pastes', '/new', '/p/amber-fox-42']) {const r = await t.get(url);@@@ -110,7 +110,7 @@ const has = (html, s, msg) => assert.ok(html.includes(s), msg || `expected to fiassert.strictEqual(pub.status, 200);has(pub.text, `<link rel="canonical" href="https://openvibe.community/submissions/${slug}">`);has(pub.text, '<meta property="og:type" content="article">');-- has(pub.text, '<meta property="og:title" content="Best play of the week">');++ has(pub.text, '<meta property="og:title" content="Best play of the week · OpenVibe.Community">');has(pub.text, 'index,follow');assert.ok(!pub.text.includes('Great one'), 'the review note is for the author and moderators');assert.ok(!pub.text.includes(`/submissions/${slug}/withdraw`) && !pub.text.includes(`/submissions/${slug}/review"`));
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
"multer": "^2.4.0",
"multer": "^2.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -1236,9 +1236,9 @@
@@ -1236,9 +1236,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "2.5.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",
- "integrity": "sha512-fHuUS6SXM7ezatfAyqLm1V8B6puV9YXSnz6aG6XQbWx28qghRCCHeUF3lr4nxiCGdn9SyikYtIyLGL5r1ycp9Q==",
+ "version": "2.6.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
+ "integrity": "sha512-IKKvWTM/9UonQ4msrzM4yir0QcTUNbhyqeN1r5ywWavU4VTsL1sI5QdHQlxge7BAsMrQ4OvMEPlHjuAfUJM3rg==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -30,7 +30,7 @@
@@ -30,7 +30,7 @@
"multer": "^2.4.0",
"multer": "^2.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -1,15 +1,16 @@
@@ -1,15 +1,16 @@
'use strict';
'use strict';
/**
/**
- * Page shell — every page on the site is server-rendered through this: full <head> SEO from
- * openvibe-shared/seo (description, canonical, robots, Open Graph, Twitter card, JSON-LD and the
- * ai-summary page summary), the shared
- * OpenVibe Frame (theme-loader first so there is no flash, navbar.js + footer.js from the
- * Network), this site's small stylesheet and its progressive script.
+ * Page shell — every page on the site is server-rendered through openvibe-shared/shell: full <head>
+ * SEO (description, canonical, robots, Open Graph, Twitter card, JSON-LD and the ai-summary page
+ * summary), the shared OpenVibe Frame (theme-loader first so there is no flash, navbar.js +
+ * footer.js from the Network, the SSR footer), plus this site's small stylesheet and its
+ * progressive script.
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
const ovServe = require('openvibe-shared/serve');
const ovServe = require('openvibe-shared/serve');
-const seo = require('openvibe-shared/seo');
+const shell = require('openvibe-shared/shell');
+const appIcon = require('openvibe-shared/app-icon');
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
const config = require('../config');
const config = require('../config');
@@ -110,52 +111,51 @@ function footerInit(opts) {
@@ -110,52 +111,51 @@ function footerInit(opts) {
* published/modified (ISO, for article:*), noFrame (error pages during outages)
* published/modified (ISO, for article:*), noFrame (error pages during outages)
*/
*/
function renderPage(o) {
function renderPage(o) {
- const title = o.title ? `${o.title} · ${SITE_NAME}` : `${SITE_NAME} — the people of OpenVibe`;
const description = fitDescription((o.description || DEFAULT_DESCRIPTION).replace(/\s+/g, ' ').trim().slice(0, 300));
const description = fitDescription((o.description || DEFAULT_DESCRIPTION).replace(/\s+/g, ' ').trim().slice(0, 300));
const canonical = abs(o.canonicalPath || '/');
const canonical = abs(o.canonicalPath || '/');
- const robots = o.robots || 'index,follow';
- const ogType = o.ogType || 'website';
- const ogImage = o.ogImage || DEFAULT_OG_IMAGE;
const nav = navbarInit(o);
const nav = navbarInit(o);
const foot = footerInit(o);
const foot = footerInit(o);
+ const feeds = o.feeds || [{ title: `${SITE_NAME} — latest pastes`, href: '/feed.xml' }];
- return `<!DOCTYPE html>
-<html lang="en" data-page="${escapeHtml(o.active || 'page')}">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>${escapeHtml(title)}</title>
-${seo.headTags({ title: o.title || SITE_NAME, noTitle: true, siteName: SITE_NAME, description, canonical, robots, type: ogType, image: ogImage, imageAlt: o.imageAlt, largeImage: !!o.ogImage, jsonLd: o.jsonLd, alternates: o.alternates })}
-${o.published ? `<meta property="article:published_time" content="${escapeHtml(o.published)}">` : ''}
-${o.modified ? `<meta property="article:modified_time" content="${escapeHtml(o.modified)}">` : ''}
-${seo.pageSummary({ title: o.title || SITE_NAME, summary: description, url: canonical, updated: o.modified })}
-${require('openvibe-shared/app-icon').headTags({ site: 'community', iconBase: '/assets' })}
-${(o.feeds || [{ title: `${SITE_NAME} — latest pastes`, href: '/feed.xml' }]).map((f) => `<link rel="alternate" type="application/rss+xml" title="${escapeHtml(f.title)}" href="${escapeHtml(f.href)}">`).join('\n')}
-<script src="${ovServe.url('theme-loader.js')}" defer></script>
-${(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`).join('\n')}
-<link rel="stylesheet" href="${asset('css/community.css')}">
-<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
-<script src="${ovServe.url('navbar.js')}" defer></script>
-<script src="${ovServe.url('footer.js')}" defer></script>
-<script src="${asset('js/community.js')}" defer></script>
-<meta name="ov-boost" content="community@${escapeHtml(RELEASE)}">
-<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>
-</head>
-<body class="${escapeHtml(o.bodyClass || '')}">
-<div id="navbar-mount"></div>
+ // 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.
+ const html = shell.page({
+ name: SITE_NAME, service: 'community', lang: 'en',
+ 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,
+ jsonLd: o.jsonLd, alternates: o.alternates,
+ summary: description, url: canonical, updated: o.modified,
+ navbar: nav, home: '/', navLinks: nav.links.map(({ label, href }) => ({ label, href })),
+ footer: { service: 'community', variant: 'full', updates: '/updates' },
+ head: [
+ 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' }),
+ ...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/community.css')}">`,
+ '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">',
+ `<script src="${asset('js/community.js')}" defer></script>`,
+ `<meta name="ov-boost" content="community@${escapeHtml(RELEASE)}">`,
+ `<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,
+ ].filter(Boolean).join('\n'),
+ bodyClass: o.bodyClass,
+ bodyAttributes: { 'data-page': o.active || 'page' },
+ body: `<div id="navbar-mount"></div>
<main id="main" class="page">
<main id="main" class="page">
${o.body || ''}
${o.body || ''}
</main>
</main>
-${require('openvibe-shared/frame').footer({ service: 'community', variant: 'full', updates: '/updates' })}
<script>
<script>
window.__OV_PAGE = ${JSON.stringify({ navbar: nav, footer: foot }).replace(/</g, '\\u003c')};
window.__OV_PAGE = ${JSON.stringify({ navbar: nav, footer: foot }).replace(/</g, '\\u003c')};
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('DOMContentLoaded', function () {
- try { if (window.OpenVibeNavbar) OpenVibeNavbar.init(window.__OV_PAGE.navbar); } catch (e) { /* the Frame is optional */ }
- try { if (window.OpenVibeFooter) OpenVibeFooter.init(window.__OV_PAGE.footer); } catch (e) { /* */ }
+ try { if (window.OpenVibeFooter) OpenVibeFooter.init(window.__OV_PAGE.footer); } catch (e) { /* the SSR footer stays */ }
});
});
-</script>
-</body>
-</html>`;
+</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">');
}
}
module.exports = { renderPage, asset, assetVersion, abs, setRelease, SITE_NAME, DEFAULT_DESCRIPTION, DEFAULT_OG_IMAGE };
module.exports = { renderPage, asset, assetVersion, abs, setRelease, SITE_NAME, DEFAULT_DESCRIPTION, DEFAULT_OG_IMAGE };
@@ -97,10 +97,10 @@ const { boot, check, done } = require('./helpers/app');
@@ -97,10 +97,10 @@ const { boot, check, done } = require('./helpers/app');
const head = r.text.split('</head>')[0];
const head = r.text.split('</head>')[0];
assert.ok(head.includes('<link rel="canonical" href="https://openvibe.community/p/amber-fox-42">'));
assert.ok(head.includes('<link rel="canonical" href="https://openvibe.community/p/amber-fox-42">'));
assert.ok(head.includes('<meta property="og:url" content="https://openvibe.community/p/amber-fox-42">'));
assert.ok(head.includes('<meta property="og:url" content="https://openvibe.community/p/amber-fox-42">'));
- assert.ok(head.includes('<meta property="og:title" content="Hello world in JavaScript">'));
+ assert.ok(head.includes('<meta property="og:title" content="Hello world in JavaScript · OpenVibe.Community">'));
assert.ok(head.includes('<meta property="og:site_name" content="OpenVibe.Community">'));
assert.ok(head.includes('<meta property="og:site_name" content="OpenVibe.Community">'));
assert.match(head, /<meta property="og:description" content="[^"]+">/);
assert.match(head, /<meta property="og:description" content="[^"]+">/);
- assert.match(head, /<meta name="twitter:title" content="Hello world in JavaScript">/);
+ assert.match(head, /<meta name="twitter:title" content="Hello world in JavaScript · OpenVibe.Community">/);
assert.match(head, /<meta name="ai-summary" content="[^"]+">/);
assert.match(head, /<meta name="ai-summary" content="[^"]+">/);
assert.match(head, /<meta property="article:published_time" content="2026-09-15T10:00:00.000Z">/);
assert.match(head, /<meta property="article:published_time" content="2026-09-15T10:00:00.000Z">/);
assert.ok(head.includes('"@type":"BreadcrumbList"') && head.includes('"@type":"WebPage"'));
assert.ok(head.includes('"@type":"BreadcrumbList"') && head.includes('"@type":"WebPage"'));
@@ -57,6 +57,30 @@ const { boot, check, done } = require('./helpers/app');
@@ -57,6 +57,30 @@ const { boot, check, done } = require('./helpers/app');
assert.ok(!/\bfree\b(?! speech)/i.test(r.text.replace(/<script[\s\S]*?<\/script>/g, '')), 'no "free" claims outside "free speech"');
assert.ok(!/\bfree\b(?! speech)/i.test(r.text.replace(/<script[\s\S]*?<\/script>/g, '')), 'no "free" claims outside "free speech"');
});
});
+ await check('the home head comes whole from openvibe-shared/shell plus this site\'s extras', async () => {
+ const r = await t.get('/');
+ assert.ok(r.text.startsWith('<!doctype html>'));
+ const head = r.text.split('</head>')[0];
+ assert.strictEqual((head.match(/<title>/g) || []).length, 1, 'one title');
+ has(head, '<title>OpenVibe.Community — the people of OpenVibe</title>');
+ assert.strictEqual((head.match(/<link rel="canonical" href="https:\/\/openvibe\.community\/">/g) || []).length, 1, 'one canonical');
+ has(head, '<meta name="robots" content="index,follow');
+ has(head, '<script type="application/ld+json">');
+ has(head, '"@type":"WebSite"');
+ has(head, '<meta name="ai-summary" content="');
+ has(head, '<link rel="alternate" type="application/rss+xml" title="OpenVibe.Community — latest pastes" href="/feed.xml">');
+ has(head, '/css/community.css?v=');
+ has(head, '/js/community.js?v=');
+ has(head, 'font-awesome');
+ has(head, '<link rel="icon"');
+ assert.match(head, /<meta name="ov-boost" content="community@[^"]+">/);
+ for (const js of ['theme-loader.js', 'navbar.js', 'footer.js', 'boost.js']) has(head, `/shared/${js}?v=`);
+ assert.match(r.text, /<body[^>]* data-page="home">/);
+ has(r.text, '<div id="navbar-mount"></div>');
+ has(r.text, 'id="ov-footer"');
+ has(r.text, 'OpenVibeFooter.init(window.__OV_PAGE.footer)');
+ });
+
await check('every page carries the boost marker and script, and the navbar signs in back to the current page', async () => {
await check('every page carries the boost marker and script, and the navbar signs in back to the current page', async () => {
for (const url of ['/', '/pastes', '/new', '/p/amber-fox-42']) {
for (const url of ['/', '/pastes', '/new', '/p/amber-fox-42']) {
const r = await t.get(url);
const r = await t.get(url);
@@ -110,7 +110,7 @@ const has = (html, s, msg) => assert.ok(html.includes(s), msg || `expected to fi
@@ -110,7 +110,7 @@ const has = (html, s, msg) => assert.ok(html.includes(s), msg || `expected to fi
assert.strictEqual(pub.status, 200);
assert.strictEqual(pub.status, 200);
has(pub.text, `<link rel="canonical" href="https://openvibe.community/submissions/${slug}">`);
has(pub.text, `<link rel="canonical" href="https://openvibe.community/submissions/${slug}">`);
has(pub.text, '<meta property="og:type" content="article">');
has(pub.text, '<meta property="og:type" content="article">');
- has(pub.text, '<meta property="og:title" content="Best play of the week">');
+ has(pub.text, '<meta property="og:title" content="Best play of the week · OpenVibe.Community">');
has(pub.text, 'index,follow');
has(pub.text, 'index,follow');
assert.ok(!pub.text.includes('Great one'), 'the review note is for the author and moderators');
assert.ok(!pub.text.includes('Great one'), 'the review note is for the author and moderators');
assert.ok(!pub.text.includes(`/submissions/${slug}/withdraw`) && !pub.text.includes(`/submissions/${slug}/review"`));
assert.ok(!pub.text.includes(`/submissions/${slug}/withdraw`) && !pub.text.includes(`/submissions/${slug}/review"`));