openvibers/openvibe.community Commits

The Community home is built with the showcase kit (plan T11, openvibe-shared v2.3.1): a hero with the terminal sample beside it, the latest and most-viewed pastes as before (the language cloud now under Latest), what lives here (pastes, screenshots, spaces, threads, Pulse, open source), 'Paste from anywhere' with curl and fetch samples, and a closing call. The page links /shared/showcase.css before community.css (layout styles option) and the kit's sections share the page's column; the old hero/cta/coming CSS is gone. A test proves the home's curl -F sample makes a text paste without an account; the SSRF inventory classifies the fetch() sample as text. Budgets: HTML 22.0 KB measured (unchanged budget); CSS raised to 40 KB raw / 8.5 KB br for the network's cached showcase.css. npm test 47/47.

OP OpenVibers committed 29 Sept 2026 · d4b42e9bcd · 9 files, +86 −59


Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex 1068c22..277b23e 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -19,7 +19,7 @@
"multer": "^2.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.21.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
"pg": "^8.23.0"
},
"devDependencies": {
@@@ -1236,9 +1236,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "2.2.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
-- "integrity": "sha512-1q+iUuUxzPeylTYmithW4jR++Jccvn5GEOrB1B3D18Dgn6OXQhPpvhPQAa1D1k1p+dWIa1/SmggyJwSGFnTNtw==",
++ "version": "2.3.1",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
++ "integrity": "sha512-K+MXnB4O+8xIFi+NQGC2SDTvDq3RkzfxQ3bTVx76Y/Rbuq7kC/L5kL3c/06xr9sPYcjyzF3Bhpv5yH2YNfR4Zw==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex dec9458..b0d00f3 100644
---- a/package.json
++++ b/package.json
@@@ -30,7 +30,7 @@
"multer": "^2.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.21.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
"pg": "^8.23.0"
},
"engines": {
ddiff --git a/public/css/community.css b/public/css/community.css
iindex 044a4e9..f20fd6e 100644
---- a/public/css/community.css
++++ b/public/css/community.css
@@@ -40,28 +40,21 @@ img { max-width: 100%; height: auto; }
.btn-sm { padding: 6px 11px; font-size: .85rem; border-radius: 7px; }
.btn[disabled] { opacity: .6; cursor: wait; }
--/* Hero */
--.hero { padding: 40px 0 24px; border-bottom: 1px solid var(--border); margin-bottom: 8px; }
--.hero h1 { font-size: clamp(2.1rem, 5vw, 3.2rem); }
--.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 20px 0 18px; }
--.hero-points { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 8px 24px; color: var(--text-secondary); font-size: .92rem; }
--.hero-points i { color: var(--accent-light); width: 18px; text-align: center; margin-right: 4px; }
++
++/* Home: the showcase kit (openvibe-shared/showcase.css) draws the hero, features, code and closing call; this is the terminal sample beside the hero. */
++/* The kit centres its sections at 1080px with their own gutter; here they share the page's column with the paste grids. */
++.page > .sc-hero, .page > .sc-sec, .page > .sc-cta { max-width: none; padding-left: 0; padding-right: 0; }
++.page > .sc-sec .sc-grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
++.cm-hero-code { margin: 0; padding: 16px 18px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-primary); color: var(--text-primary); font-size: 13px; line-height: 1.65; overflow: auto; white-space: pre; }
/* Sections */
.section { padding: 28px 0 8px; }
.section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.section-head h2 { margin: 0; }
.more { font-size: .9rem; font-weight: 600; white-space: nowrap; }
--.cta { margin-top: 20px; }
--.cta-inner { background: linear-gradient(135deg, rgba(var(--accent-rgb), .18), rgba(var(--accent-rgb), .04)); border: 1px solid rgba(var(--accent-rgb), .35); border-radius: var(--radius); padding: 28px; }
--.cta-inner p { max-width: 52rem; color: var(--text-secondary); }
.lang-cloud { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0 0; }
.lang-cloud a { background: var(--bg-card); border: 1px solid var(--border); border-radius: 999px; padding: 4px 12px; font-size: .85rem; color: var(--text-primary); }
.lang-cloud a small { color: var(--text-muted); margin-left: 4px; }
--.coming-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; margin: 12px 0 14px; }
--.coming-item { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px; }
--.coming-item i { font-size: 1.4rem; color: var(--accent-light); margin-bottom: 8px; display: block; }
--.coming-item p { margin: 0; color: var(--text-secondary); font-size: .93rem; }
/* Cards */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex d1cd793..c954c9e 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -97,6 +97,7 @@ function footerInit(opts) {
* ogImage, jsonLd (array), body (main HTML), active ('home'|'pastes'|'spaces'|'pulse'|'new'|'my'),
* feeds ([{ title, href }] RSS alternates; default: the latest-pastes feed),
* historyType ('page'|'paste'), historyTitle, footerVariant ('full'|'compact'), bodyClass,
++ * styles (Shared stylesheets by name, linked before community.css so this site's rules win: ['showcase.css']),
* published/modified (ISO, for article:*), noFrame (error pages during outages)
*/
function renderPage(o) {
@@@ -133,6 +134,7 @@ ${o.modified ? `<meta property="article:modified_time" content="${escapeHtml(o.m
${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">
${jsonLdScript(o.jsonLd)}
ddiff --git a/server/render/pages.js b/server/render/pages.js
iindex 663fb63..27739d5 100644
---- a/server/render/pages.js
++++ b/server/render/pages.js
@@@ -10,6 +10,7 @@ const ovServe = require('openvibe-shared/serve');
const seo = require('../seo');
const { renderPage, SITE_NAME, DEFAULT_OG_IMAGE } = require('./layout');
const frame = require('openvibe-shared/frame');
++const showcase = require('openvibe-shared/showcase');
const { highlight, escapeHtml: esc, languageLabel, extensionFor, LANGUAGES } = require('./highlight');
const NETWORK_URL = 'https://openvibe.network';
@@@ -106,52 +107,66 @@ function cardGrid(pastes, empty = 'Nothing here yet.') {
}
// ── Home ─────────────────────────────────────────────────────
--function homePage({ latest, trending, languages, user }) {
-- const body = `
--<section class="hero">
-- <p class="eyebrow">OpenVibe.Community</p>
-- <h1>Share it with a link. Talk about it here.</h1>
-- <p class="lede">Paste code, logs, configs or a screenshot and get a short link that works anywhere. Reading and sharing need no account. Sign in with your OpenVibe account to edit, comment and keep a list of your own, and talk things through in <a href="/s">Spaces</a>.</p>
-- <div class="hero-actions">
-- <a class="btn btn-primary" href="/new"><i class="fa-solid fa-plus" aria-hidden="true"></i> Start a paste</a>
-- <a class="btn" href="/pastes"><i class="fa-solid fa-paste" aria-hidden="true"></i> Browse pastes</a>
-- <a class="btn" href="/s"><i class="fa-solid fa-comments" aria-hidden="true"></i> Spaces</a>
-- ${user ? `<a class="btn btn-ghost" href="/my"><i class="fa-solid fa-user" aria-hidden="true"></i> My pastes</a>` : `<a class="btn btn-ghost" href="/auth/login?next=%2F"><i class="fa-solid fa-right-to-bracket" aria-hidden="true"></i> Sign in with your OpenVibe account</a>`}
-- </div>
-- <ul class="hero-points">
-- <li><i class="fa-solid fa-people-group" aria-hidden="true"></i> Run by the people using it, moderated in the open</li>
-- <li><i class="fa-solid fa-comment" aria-hidden="true"></i> Open expression within the rules: say it, own it</li>
-- <li><i class="fa-brands fa-github" aria-hidden="true"></i> Open source — <a href="https://github.com/OpenVibers/OpenVibe.Community" rel="noopener">read the code</a></li>
-- </ul>
--</section>
++// The home (openvibe-shared/showcase): what Community is and where to start, the pastes people are sharing right
++// now, what else lives here, and the API a terminal can paste with. The paste grids stay this site's own markup.
++const TERMINAL = `$ curl -F 'content=<build.log' \\
++ https://openvibe.community/api/pastes
++{ "slug": "amber-fox-42",
++ "url": "/p/amber-fox-42", … }`;
++function homePage({ latest, trending, languages, user }) {
++ const body = showcase.hero({
++ eyebrow: 'OpenVibe.Community',
++ title: 'Share it with a link.', accent: 'Talk about it here.',
++ lede: 'Paste code, logs, configs or a screenshot and get a short link that works anywhere. Reading and sharing need no account. Sign in with your OpenVibe account to edit, comment and keep a list of your own, and talk things through in Spaces.',
++ actions: [
++ { label: 'Start a paste', href: '/new', primary: true, icon: 'fa-plus' },
++ { label: 'Browse pastes', href: '/pastes', icon: 'fa-paste' },
++ ...(user ? [{ label: 'My pastes', href: '/my', icon: 'fa-user' }] : []),
++ ],
++ note: 'Run by the people using it and moderated in the open. Say it, own it.',
++ aside: { html: `<pre class="cm-hero-code" aria-label="Paste a file from a terminal"><code>${esc(TERMINAL)}</code></pre>` },
++ }) + `
<section class="section" id="latest">
<div class="section-head"><h2>Latest pastes</h2><a class="more" href="/pastes">All pastes <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>
${cardGrid(latest, 'No pastes yet — be the first.')}
++ ${languages && languages.length ? `<p class="lang-cloud" aria-label="Browse by language">${languages.slice(0, 12).map((l) => `<a href="/pastes?lang=${encodeURIComponent(l.language)}">${esc(languageLabel(l.language))} <small>${l.count}</small></a>`).join(' ')}</p>` : ''}
</section>
<section class="section" id="trending">
<div class="section-head"><h2>Most viewed</h2><a class="more" href="/pastes?sort=views">By views <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>
${cardGrid(trending, 'Nothing trending yet.')}
</section>
--
--<section class="section cta" id="start">
-- <div class="cta-inner">
-- <h2>Start a paste</h2>
-- <p>Code, logs, configs, a screenshot — paste it, get a link, share it anywhere. Sign in to keep your pastes together under your OpenVibe account, or post anonymously.</p>
-- <a class="btn btn-primary" href="/new"><i class="fa-solid fa-plus" aria-hidden="true"></i> New paste</a>
-- ${languages && languages.length ? `<p class="lang-cloud">${languages.slice(0, 12).map((l) => `<a href="/pastes?lang=${encodeURIComponent(l.language)}">${esc(languageLabel(l.language))} <small>${l.count}</small></a>`).join(' ')}</p>` : ''}
-- </div>
--</section>
--
--<section class="section coming" id="spaces">
-- <h2>Spaces and threads</h2>
-- <div class="coming-grid">
-- <div class="coming-item"><i class="fa-solid fa-layer-group" aria-hidden="true"></i><h3><a href="/s">Spaces</a></h3><p>General, Feedback and Showcase: places for the community to talk, share and get help.</p></div>
-- <div class="coming-item"><i class="fa-solid fa-comments" aria-hidden="true"></i><h3><a href="/s/general">Threads</a></h3><p>Long-form discussion that outlives a chat scrollback: start one in any open space.</p></div>
-- <div class="coming-item"><i class="fa-solid fa-wave-square" aria-hidden="true"></i><h3><a href="/pulse">Pulse</a></h3><p>What is happening across the OpenVibe network, in one feed.</p></div>
-- </div>
--</section>
++` + showcase.features({
++ id: 'spaces',
++ title: 'What lives here',
++ items: [
++ { icon: 'fa-paste', title: 'Pastes', text: 'Code, logs and configs with highlighting for every common language, a raw link and a download.', href: '/new' },
++ { icon: 'fa-image', title: 'Screenshots', text: 'Drop an image and share it with a link. Location data is stripped before it is stored.', href: '/new' },
++ { icon: 'fa-layer-group', title: 'Spaces', text: 'General, Feedback and Showcase: places to talk, share and get help.', href: '/s' },
++ { icon: 'fa-comments', title: 'Threads', text: 'Long-form discussion that outlives a chat scrollback. Start one in any open space.', href: '/s/general' },
++ { icon: 'fa-wave-square', title: 'Pulse', text: 'What is happening across the OpenVibe network, in one feed.', href: '/pulse' },
++ { icon: 'fa-code-branch', title: 'Open source', text: 'Every line that runs this site is public. Read it, file an issue, send a fix.', href: 'https://github.com/OpenVibers/OpenVibe.Community' },
++ ],
++ }) + showcase.code({
++ id: 'api',
++ title: 'Paste from anywhere',
++ lede: 'The page you use is the API a script uses: no key for public pastes. Sign in, or send your OpenVibe token, to keep them under your account.',
++ samples: [
++ { label: 'A file from a terminal', lang: 'bash', code: `curl -F 'content=<build.log' -F title='Build log' \\\n https://openvibe.community/api/pastes` },
++ { label: 'JavaScript', lang: 'js', code: `const api = 'https://openvibe.community/api/pastes';
++const res = await fetch(api, {
++ method: 'POST',
++ headers: { 'content-type': 'application/json' },
++ body: JSON.stringify({ title: 'Snippet', content }),
++});
++const { url } = await res.json(); // "/p/<slug>"` },
++ ],
++ }) + showcase.cta({
++ title: 'Start a paste',
++ text: 'Paste it, get a link, share it anywhere. Sign in to keep your pastes together under your OpenVibe account, or post without one.',
++ actions: [{ label: 'New paste', href: '/new', primary: true, icon: 'fa-plus' }, { label: 'Visit the Spaces', href: '/s' }],
++ }) + `
<section class="section">${frame.shipped({ service: 'community', title: 'Recently shipped on OpenVibe.Community' })}</section>`;
return renderPage({
title: null,
@@@ -159,6 +174,7 @@ function homePage({ latest, trending, languages, user }) {
canonicalPath: '/',
active: 'home',
jsonLd: [seo.websiteLd()],
++ styles: [showcase.STYLESHEET],
body,
});
}
ddiff --git a/test/pastes-api.test.js b/test/pastes-api.test.js
iindex c609782..c61a07e 100644
---- a/test/pastes-api.test.js
++++ b/test/pastes-api.test.js
@@@ -219,6 +219,19 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual((await call('/api/pastes', { method: 'POST', ip, token: svc([CREATE]), json: { content: 'service' } })).status, 201);
});
++ await check('the home page sample works: a multipart text field (curl -F \'content=<file\') makes a text paste, no account', async () => {
++ const fd = new FormData();
++ fd.append('content', 'npm ERR! code ERESOLVE');
++ fd.append('title', 'Build log');
++ const r = await call('/api/pastes', { method: 'POST', ip: '203.0.113.90', body: fd });
++ assert.strictEqual(r.status, 201, r.text);
++ const out = r.json();
++ assert.strictEqual(out.url, `/p/${out.slug}`);
++ assert.notStrictEqual(out.paste.type, 'screenshot');
++ assert.strictEqual(out.paste.title, 'Build log');
++ assert.strictEqual(out.paste.content, 'npm ERR! code ERESOLVE');
++ });
++
// A JPEG whose APP1 segment carries EXIF (with a fake GPS marker).
const jpeg = Buffer.concat([
Buffer.from([0xff, 0xd8]),
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex 2272c74..5eb38ea 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -10,14 +10,14 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
-- htmlRawKB: 26, // measured 20.2 (fresh database)
-- htmlBrotliKB: 6.5, // 5.1
++ htmlRawKB: 26, // measured 22.0 (fresh database; the showcase home)
++ htmlBrotliKB: 6.5, // 5.7
jsFiles: 5, // 4
jsRawKB: 240, // 208.8
jsBrotliKB: 56, // 48.9
cssFiles: 3, // 2
-- cssRawKB: 31, // 26.2
-- cssBrotliKB: 6.5, // 5.2
++ cssRawKB: 40, // 36.5: community.css + the network's cached showcase.css (raised with the showcase home)
++ cssBrotliKB: 8.5, // 7.7
externalFiles: 2, // 1
};
ddiff --git a/test/security-ssrf.test.js b/test/security-ssrf.test.js
iindex c919b1d..33dbb85 100644
---- a/test/security-ssrf.test.js
++++ b/test/security-ssrf.test.js
@@@ -27,6 +27,7 @@ const INVENTORY = {
'server/chat-rooms.js': [1, 'OV_CHAT_INTERNAL_URL /api/chat/rooms/<slug>/attachments; the slug must match ROOM_SLUG and is encoded'],
'server/events.js': [3, 'service token (Network) and the outbox publisher: EVENTS_URL'],
'server/identity/network.js': [2, 'Network /internal/identity/resolve-batch with a service token'],
++ 'server/render/pages.js': [1, 'not a call: the home page shows a fetch() sample as text (never executed)'],
'server/identity/account-data.js': [2, 'Network /oauth/token and /internal/account-exports and /internal/account-deletions (fixed paths) with a service token'],
'server/identity/profile-module.js': [3, 'Network modules API (community.profile) with a service token'],
'server/media/files.js': [2, 'OV_MEDIA_INTERNAL_URL v1 file store, service token'],
ddiff --git a/test/ssr.test.js b/test/ssr.test.js
iindex b6905c4..9fc94e0 100644
---- a/test/ssr.test.js
++++ b/test/ssr.test.js
@@@ -31,7 +31,9 @@ const { boot, check, done } = require('./helpers/app');
has(r.text, '/shared/theme-loader.js?v=');
has(r.text, '/shared/navbar.js?v=');
has(r.text, '/shared/footer.js?v=');
-- has(r.text, '<h1>Share it with a link. Talk about it here.</h1>');
++ has(r.text, '<h1>Share it with a link.<span class="sc-accent"> Talk about it here.</span></h1>');
++ has(r.text, '/shared/showcase.css?v=');
++ has(r.text, 'id="api"');
has(r.text, '<h2>Latest pastes</h2>');
has(r.text, '<h2>Most viewed</h2>');
has(r.text, 'id="spaces"');