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.
Changes
@@@ -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"@@@ -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": {@@@ -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; }@@@ -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)}@@@ -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,});}@@@ -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]),@@@ -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.7jsFiles: 5, // 4jsRawKB: 240, // 208.8jsBrotliKB: 56, // 48.9cssFiles: 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.7externalFiles: 2, // 1};@@@ -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'],@@@ -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"');
@@ -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.76.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-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"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -1236,9 +1236,9 @@
@@ -1236,9 +1236,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"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.76.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-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"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -40,28 +40,21 @@ img { max-width: 100%; height: auto; }
@@ -40,28 +40,21 @@ img { max-width: 100%; height: auto; }
.btn-sm { padding: 6px 11px; font-size: .85rem; border-radius: 7px; }
.btn-sm { padding: 6px 11px; font-size: .85rem; border-radius: 7px; }
.btn[disabled] { opacity: .6; cursor: wait; }
.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 */
/* Sections */
.section { padding: 28px 0 8px; }
.section { padding: 28px 0 8px; }
.section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.section-head h2 { margin: 0; }
.section-head h2 { margin: 0; }
.more { font-size: .9rem; font-weight: 600; white-space: nowrap; }
.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 { 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 { 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; }
.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 */
/* Cards */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
@@ -97,6 +97,7 @@ function footerInit(opts) {
@@ -97,6 +97,7 @@ function footerInit(opts) {
* ogImage, jsonLd (array), body (main HTML), active ('home'|'pastes'|'spaces'|'pulse'|'new'|'my'),
* ogImage, jsonLd (array), body (main HTML), active ('home'|'pastes'|'spaces'|'pulse'|'new'|'my'),
* feeds ([{ title, href }] RSS alternates; default: the latest-pastes feed),
* feeds ([{ title, href }] RSS alternates; default: the latest-pastes feed),
* historyType ('page'|'paste'), historyTitle, footerVariant ('full'|'compact'), bodyClass,
* 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)
* published/modified (ISO, for article:*), noFrame (error pages during outages)
*/
*/
function renderPage(o) {
function renderPage(o) {
@@ -133,6 +134,7 @@ ${o.modified ? `<meta property="article:modified_time" content="${escapeHtml(o.m
@@ -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' })}
${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')}
${(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>
<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="${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">
<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)}
${jsonLdScript(o.jsonLd)}
@@ -10,6 +10,7 @@ const ovServe = require('openvibe-shared/serve');
@@ -10,6 +10,7 @@ const ovServe = require('openvibe-shared/serve');
const seo = require('../seo');
const seo = require('../seo');
const { renderPage, SITE_NAME, DEFAULT_OG_IMAGE } = require('./layout');
const { renderPage, SITE_NAME, DEFAULT_OG_IMAGE } = require('./layout');
const frame = require('openvibe-shared/frame');
const frame = require('openvibe-shared/frame');
+const showcase = require('openvibe-shared/showcase');
const { highlight, escapeHtml: esc, languageLabel, extensionFor, LANGUAGES } = require('./highlight');
const { highlight, escapeHtml: esc, languageLabel, extensionFor, LANGUAGES } = require('./highlight');
const NETWORK_URL = 'https://openvibe.network';
const NETWORK_URL = 'https://openvibe.network';
@@ -106,52 +107,66 @@ function cardGrid(pastes, empty = 'Nothing here yet.') {
@@ -106,52 +107,66 @@ function cardGrid(pastes, empty = 'Nothing here yet.') {
}
}
// ── Home ─────────────────────────────────────────────────────
// ── 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">
<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>
<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.')}
${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>
<section class="section" id="trending">
<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>
<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.')}
${cardGrid(trending, 'Nothing trending yet.')}
</section>
</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>`;
<section class="section">${frame.shipped({ service: 'community', title: 'Recently shipped on OpenVibe.Community' })}</section>`;
return renderPage({
return renderPage({
title: null,
title: null,
@@ -159,6 +174,7 @@ function homePage({ latest, trending, languages, user }) {
@@ -159,6 +174,7 @@ function homePage({ latest, trending, languages, user }) {
canonicalPath: '/',
canonicalPath: '/',
active: 'home',
active: 'home',
jsonLd: [seo.websiteLd()],
jsonLd: [seo.websiteLd()],
+ styles: [showcase.STYLESHEET],
body,
body,
});
});
}
}
@@ -219,6 +219,19 @@ const { boot, check, done } = require('./helpers/app');
@@ -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);
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).
// A JPEG whose APP1 segment carries EXIF (with a fake GPS marker).
const jpeg = Buffer.concat([
const jpeg = Buffer.concat([
Buffer.from([0xff, 0xd8]),
Buffer.from([0xff, 0xd8]),
@@ -10,14 +10,14 @@ const { spawn } = require('child_process');
@@ -10,14 +10,14 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
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
jsFiles: 5, // 4
jsRawKB: 240, // 208.8
jsRawKB: 240, // 208.8
jsBrotliKB: 56, // 48.9
jsBrotliKB: 56, // 48.9
cssFiles: 3, // 2
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
externalFiles: 2, // 1
};
};
@@ -27,6 +27,7 @@ const INVENTORY = {
@@ -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/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/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/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/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/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'],
'server/media/files.js': [2, 'OV_MEDIA_INTERNAL_URL v1 file store, service token'],
@@ -31,7 +31,9 @@ const { boot, check, done } = require('./helpers/app');
@@ -31,7 +31,9 @@ const { boot, check, done } = require('./helpers/app');
has(r.text, '/shared/theme-loader.js?v=');
has(r.text, '/shared/theme-loader.js?v=');
has(r.text, '/shared/navbar.js?v=');
has(r.text, '/shared/navbar.js?v=');
has(r.text, '/shared/footer.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>Latest pastes</h2>');
has(r.text, '<h2>Most viewed</h2>');
has(r.text, '<h2>Most viewed</h2>');
has(r.text, 'id="spaces"');
has(r.text, 'id="spaces"');