openvibers/openvibe.community Commits
Agent layer: /.well-known/agent.json and Markdown twins of the public pages (#174)
Changes
@@@ -42,7 +42,7 @@ little progressive JavaScript for pagination, copy buttons and the upload path.(a space's room), OpenVibe.Events (the outbox relay and the Pulse and account subscriptions)- OpenVibe.Live only for old VOD/clip comment imports (it reads and writes no pastes here any more)- `openvibe-contracts` v0.112.0, `openvibe-sdk` v0.67.0 (events outbox, per-actor limits, no-looks),-- `openvibe-shared` v4.33.0, pinned by release tarball++ `openvibe-shared` v4.34.0, pinned by release tarball## How it fits the network@@@ -846,5 +846,5 @@ release after paste comments moved onto the typed comment threads. [Submissions]<!-- versions:start -->- openvibe-contracts: v0.222.0- openvibe-sdk: v0.67.0--- openvibe-shared: v4.33.0++- openvibe-shared: v4.34.0<!-- versions:end -->@@@ -9,7 +9,7 @@"domain": "openvibe.community","contracts": "openvibe-contracts v0.222.0","sdk": "openvibe-sdk v0.67.0",-- "shared": "openvibe-shared v4.33.0",++ "shared": "openvibe-shared v4.34.0","capabilities": ["community.paste.create","community.paste.write",@@@ -44,5 +44,5 @@"N-1 compatibility tests","per-actor limits (openvibe-sdk/limits)"],-- "updated": "2026-10-28"++ "updated": "2026-10-29"}@@@ -19,7 +19,7 @@"multer": "^2.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0","pg": "^8.23.0"},"devDependencies": {@@@ -29,6 +29,12 @@"node": ">=20.0.0"}},++ "node_modules/@acemir/cssom": {++ "version": "0.9.32",++ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.32.tgz",++ "integrity": "sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==",++ "license": "MIT"++ },"node_modules/@electric-sql/pglite": {"version": "0.5.8","resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",@@@ -124,6 +130,19 @@"npm": "1.2.8000 || >= 1.4.16"}},++ "node_modules/boolbase": {++ "version": "2.0.0",++ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",++ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",++ "license": "ISC",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },"node_modules/buffer-equal-constant-time": {"version": "1.0.1","resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",@@@ -237,6 +256,39 @@"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==","license": "MIT"},++ "node_modules/css-select": {++ "version": "7.0.0",++ "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",++ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "boolbase": "^2.0.0",++ "css-what": "^8.0.0",++ "domhandler": "^6.0.1",++ "domutils": "^4.0.2",++ "nth-check": "^3.0.1"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },++ "node_modules/css-what": {++ "version": "8.0.0",++ "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",++ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },"node_modules/debug": {"version": "2.6.9","resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",@@@ -274,6 +326,73 @@"npm": "1.2.8000 || >= 1.4.16"}},++ "node_modules/dom-serializer": {++ "version": "3.1.1",++ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",++ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^3.0.0",++ "domhandler": "^6.0.0",++ "entities": "^8.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"++ }++ },++ "node_modules/domelementtype": {++ "version": "3.0.0",++ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",++ "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ }++ },++ "node_modules/domhandler": {++ "version": "6.0.1",++ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",++ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "domelementtype": "^3.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/domhandler?sponsor=1"++ }++ },++ "node_modules/domutils": {++ "version": "4.0.2",++ "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",++ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "dom-serializer": "^3.0.0",++ "domelementtype": "^3.0.0",++ "domhandler": "^6.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/domutils?sponsor=1"++ }++ },"node_modules/dotenv": {"version": "16.6.1","resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",@@@ -324,6 +443,18 @@"node": ">= 0.8"}},++ "node_modules/entities": {++ "version": "8.1.0",++ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",++ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },"node_modules/es-define-property": {"version": "1.0.1","resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",@@@ -591,6 +722,110 @@"node": ">=12.0.0"}},++ "node_modules/html-escaper": {++ "version": "3.0.3",++ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",++ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",++ "license": "MIT"++ },++ "node_modules/htmlparser2": {++ "version": "10.1.0",++ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",++ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",++ "funding": [++ "https://github.com/fb55/htmlparser2?sponsor=1",++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.3",++ "domutils": "^3.2.2",++ "entities": "^7.0.1"++ }++ },++ "node_modules/htmlparser2/node_modules/dom-serializer": {++ "version": "2.0.0",++ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",++ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.2",++ "entities": "^4.2.0"++ },++ "funding": {++ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {++ "version": "4.5.0",++ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",++ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=0.12"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/domelementtype": {++ "version": "2.3.0",++ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",++ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "BSD-2-Clause"++ },++ "node_modules/htmlparser2/node_modules/domhandler": {++ "version": "5.0.3",++ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",++ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "domelementtype": "^2.3.0"++ },++ "engines": {++ "node": ">= 4"++ },++ "funding": {++ "url": "https://github.com/fb55/domhandler?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/domutils": {++ "version": "3.2.2",++ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",++ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "dom-serializer": "^2.0.0",++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.3"++ },++ "funding": {++ "url": "https://github.com/fb55/domutils?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/entities": {++ "version": "7.0.1",++ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",++ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=0.12"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },"node_modules/http-errors": {"version": "2.0.1","resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",@@@ -736,6 +971,30 @@"safe-buffer": "^5.0.1"}},++ "node_modules/linkedom": {++ "version": "0.18.14",++ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.14.tgz",++ "integrity": "sha512-BRUnkOm4YR5vo+qI3j0EbungB0vZPSCue5AdnTQRaxAGSbsOZiBFRO0rh4zlo8qXUhli8pxvnpEXDklMnlq2Rg==",++ "license": "ISC",++ "dependencies": {++ "@acemir/cssom": "^0.9.32",++ "css-select": "^7.0.0",++ "html-escaper": "^3.0.3",++ "htmlparser2": "^10.1.0",++ "uhyphen": "^0.2.0"++ },++ "engines": {++ "node": ">=16"++ },++ "peerDependencies": {++ "canvas": ">= 2"++ },++ "peerDependenciesMeta": {++ "canvas": {++ "optional": true++ }++ }++ },"node_modules/lodash.defaults": {"version": "4.2.0","resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",@@@ -892,6 +1151,22 @@"node": ">= 0.6"}},++ "node_modules/nth-check": {++ "version": "3.0.1",++ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",++ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "boolbase": "^2.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/nth-check?sponsor=1"++ }++ },"node_modules/object-inspect": {"version": "1.13.4","resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",@@@ -961,12 +1236,13 @@}},"node_modules/openvibe-shared": {-- "version": "4.33.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",-- "integrity": "sha512-t9XvhrQ+lQebOrFTjMCzI6/7/i4iQn9dmMr/8HVTNW1XoV1exyJa7IH/HfyyKuLxjA8xaowrDfD28LwKIibBNg==",++ "version": "4.34.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",++ "integrity": "sha512-Rj5uRO3MbWkyIYAm+vK4Y94ugDNYEjEgGkYnPjSej4yMqhalpaPCkU96dzphMGiOUGP1PNWsiKhwaBj8tewgBg==","license": "MIT","dependencies": {-- "jsonwebtoken": "^9.0.0"++ "jsonwebtoken": "^9.0.0",++ "linkedom": "^0.18.13"},"bin": {"openvibe-build-assets": "scripts/build-assets.js"@@@ -1417,6 +1693,12 @@"node": ">= 0.6"}},++ "node_modules/uhyphen": {++ "version": "0.2.0",++ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",++ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",++ "license": "ISC"++ },"node_modules/unpipe": {"version": "1.0.0","resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",@@@ -30,7 +30,7 @@"multer": "^2.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0","pg": "^8.23.0"},"engines": {nnew file mode 100644@@@ -0,0 +1,18 @@++'use strict';++// What openvibe.community tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.++// Every field is something this host really serves: the public home, paste browse and paste pages (with++// their comment threads), the native paste API under /api/pastes and the typed API under /api/v1, their++// OpenAPI document, llms.txt/llms-full.txt and the sitemap.++module.exports = {++ id: 'community',++ name: 'OpenVibe.Community',++ purpose: 'The community hub of the OpenVibe network: pastes, forum spaces and threads, comment threads, Pulse and public submissions.',++ home: 'https://openvibe.community',++ site: 'openvibe.community',++ api: '/api',++ openapi: '/openapi.json',++ policy: {++ auth: ['none', 'Network OIDC', 'bearer'],++ rateLimit: 'The public pages are not limited by the app; /api is limited to 120 requests a minute per address and sign-in attempts to 60 per 15 minutes',++ },++};nnew file mode 100644@@@ -0,0 +1,33 @@++'use strict';++// The Markdown twins of openvibe.community's public pages (openvibe-shared/agent-layer): the home,++// /pastes and a paste. The bodies come from the same page builders the HTML routes use (render/pages.js,++// called through app.js's views with the anonymous viewer and noView), so a twin cannot drift from its++// page and never carries a signed-in person's view. A pasted twin carries the paste's text as prose and++// its comments as the page's threaded comment list.++//++// A private, deleted, burned or missing paste answers 404 and has no twin; a burn-after-read paste is++// never consumed by a twin (app.js checks before the read).++const TWIN_PATH_RE = /^\/(?:pastes|p\/[^/]+)?$/;++++function createAgentPages({ views }) {++ async function renderPublicPage(path, { query } = {}) {++ const p = String(path || '/').replace(/\/+$/, '') || '/';++ if (!TWIN_PATH_RE.test(p)) return null;++ const q = Object.fromEntries(new URLSearchParams(query ? query.toString() : ''));++ let html;++ if (p === '/') html = await views.home();++ else if (p === '/pastes') html = await views.browse(q);++ else {++ let slug;++ try { slug = decodeURIComponent(p.slice(3)); } catch { return null; }++ const out = await views.paste(slug, q);++ if (!out || out.status !== 200) return null;++ html = out.body;++ }++ return html ? { html } : null;++ }++++ return { renderPublicPage };++}++++module.exports = { createAgentPages, TWIN_PATH_RE };@@@ -40,7 +40,7 @@ const { assetVersion, SITE_NAME, DEFAULT_DESCRIPTION } = require('./render/layouconst { createAuthClient, createAuthRoutes } = require('./auth/routes');const { getDb } = require('./db');const { createNetworkIdentity } = require('./identity/network');--const { createViewerResolver } = require('./identity/viewer');++const { createViewerResolver, ANONYMOUS } = require('./identity/viewer');const v1 = require('./http/v1');const { createCommentService } = require('./comments/service');const { createCommentsApi } = require('./comments/api');@@@ -276,7 +276,8 @@ async function createApp(opts = {}) {// ── Machine endpoints ────────────────────────────────────// Written by openvibe-shared/seo from server/discovery.js; the sitemap is rebuilt at most hourly.app.get('/robots.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))-- .send(seo.robotsTxt({ sitemaps: [`${config.baseUrl}/sitemap.xml`], disallow: discovery.ROBOTS_DISALLOW })));++ .send(seo.robotsTxt({ sitemaps: [`${config.baseUrl}/sitemap.xml`], disallow: discovery.ROBOTS_DISALLOW })++ + `# Agent manifest: ${config.baseUrl}/.well-known/agent.json\n`));app.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({ name: SITE_NAME, summary: discovery.SUMMARY, details: discovery.CONTENT_LABELS, sections: discovery.llmsSections() })));app.get('/llms-full.txt', wrap(async (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))@@@ -310,6 +311,12 @@ async function createApp(opts = {}) {const withUser = viewers.middleware({ services: false });const ctxOf = (req) => ({ token: req.token, ip: req.ip, userAgent: req.get('user-agent') || '', viewer: req.viewer });const html = (res, body, status = 200) => res.status(status).type('html').set('Cache-Control', PAGE_CACHE).send(body);++ // A public page with a Markdown twin names it in the Link header and varies on Accept. Only the++ // pages the agent layer twins (the home, /pastes and a paste) call this.++ const twinLink = (res, pathname) => {++ res.vary('Accept');++ res.set('Link', require('openvibe-shared/agent-layer').linkHeaderTags(`${config.baseUrl}${pathname}`, `${config.baseUrl}/llms.txt`).header);++ };// Every page response below (redirects and error pages too) that sets no policy of its own.app.use(cache.applyHtml({ private: true }));@@@ -344,10 +351,50 @@ async function createApp(opts = {}) {return value;}-- app.get('/', withUser, wrap(async (req, res) => {++ // The public pages the agent layer twins, built once here and used by both the HTML routes and++ // server/agent-pages.js. A twin passes the anonymous viewer and noView: it must never carry a++ // signed-in person's view, count a page view or consume a burn-after-read paste.++ async function homeView(user) {// People's pastes lead; OpenVibe.AI's moment pastes from streams have their own shelf.const [latest, trending, fromStreams, languages, extras] = await Promise.all([catalog.latest(12, { from: 'people' }), catalog.trending(8, { from: 'people' }), catalog.latest(8, { from: 'streams' }), catalog.languages(), readHomeExtras()]);-- html(res, pages.homePage({ latest, trending, fromStreams, languages, user: req.user, ...extras }));++ return pages.homePage({ latest, trending, fromStreams, languages, user, ...extras });++ }++++ async function browseView(query) {++ const [result, languages] = await Promise.all([catalog.browse(query), catalog.languages()]);++ return pages.browsePage(result, languages);++ }++++ const agentViews = {++ home: () => homeView(null),++ browse: (query) => browseView(query),++ paste: (slug, query) => pasteView(slug, query, {++ viewer: ANONYMOUS, user: null, twin: true,++ ctx: { viewer: ANONYMOUS, noView: true, ip: '127.0.0.1', userAgent: '' },++ }),++ };++++ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json, and the Markdown twins of the++ // home, /pastes and a paste. Gated to the manifest and twin requests only, so a browser (signed in or++ // not) keeps the page its own route renders; the twins render through the same page builders signed out.++ {++ const agentRouter = express.Router();++ require('openvibe-shared/agent-layer').mount(agentRouter, {++ descriptor: require('./agent-descriptor'),++ render: require('./agent-pages').createAgentPages({ views: agentViews }).renderPublicPage,++ });++ app.use((req, res, next) => {++ // The manifest exactly: the rest of /.well-known/ (security.txt) belongs to the app.++ if (req.path === '/.well-known/agent.json') return agentRouter(req, res, next);++ if (String(req.path || '').endsWith('.md') || (req.query && req.query.format === 'markdown')++ || req.accepts(['html', 'text/markdown']) === 'text/markdown') return agentRouter(req, res, next);++ return next();++ });++ }++++ app.get('/', withUser, wrap(async (req, res) => {++ twinLink(res, '/');++ html(res, await homeView(req.user));}));app.get('/updates', (req, res) => html(res, pages.updatesPage()));@@@ -370,8 +417,8 @@ async function createApp(opts = {}) {}));app.get('/pastes', withUser, wrap(async (req, res) => {-- const [result, languages] = await Promise.all([catalog.browse(req.query), catalog.languages()]);-- html(res, pages.browsePage(result, languages));++ twinLink(res, req.originalUrl);++ html(res, await browseView(req.query));}));// Old Live-style /pastes/<slug> links and bare slugs land on the paste page.@@@ -394,15 +441,20 @@ async function createApp(opts = {}) {res.redirect(301, `/p/${clean}${rest}${q >= 0 ? req.originalUrl.slice(q) : ''}`);});-- app.get('/p/:slug', withUser, wrap(async (req, res) => {-- const { slug } = req.params;-- if (!SLUG_RE.test(slug)) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'That link does not point at a paste.' }), 404);++ /** The paste page's body, or the error page it answers with; the HTML route and the twin share it. */++ async function pasteView(slug, query, { viewer, user, ctx, twin = false } = {}) {++ if (!SLUG_RE.test(slug)) return { status: 404, body: pages.errorPage({ status: 404, title: 'Paste not found', message: 'That link does not point at a paste.' }) };++ // A twin is not a person reading: never consume a burn-after-read paste for one.++ if (twin) {++ const row = await db.prepare('SELECT burn_after_read FROM pastes WHERE slug = ?').get(slug);++ if (!row || Number(row.burn_after_read)) return null;++ }let paste;try {-- paste = await source.getPaste(slug, ctxOf(req));++ paste = await source.getPaste(slug, ctx);} catch (err) {-- if (err.status === 404) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }), 404);-- if (err.status === 410) return html(res, pages.errorPage({ status: 410, title: 'This paste has burned', message: 'It was set to burn after reading, and it has been read.' }), 410);++ if (err.status === 404) return { status: 404, body: pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }) };++ if (err.status === 410) return { status: 410, body: pages.errorPage({ status: 410, title: 'This paste has burned', message: 'It was set to burn after reading, and it has been read.' }) };throw err;}const related = await catalog.related(paste, 6);@@@ -410,10 +462,20 @@ async function createApp(opts = {}) {// comments API show, sorted (?sort=old|new) and paged (?after=<last comment id>) as there; null while// nobody has commented. The comment service applies the paste's visibility again.// A burn-after-read paste is gone once shown, so it has no comment panel.-- const after = /^\d{1,15}$/.test(String(req.query.after || '')) ? String(req.query.after) : null;-- const sort = req.query.sort === 'old' ? 'old' : 'new';-- const thread = paste.burn_after_read ? null : await comments.forRef(req.viewer, pasteRef(paste.slug), { sort, after, limit: 30 });-- html(res, pages.pastePage({ paste, related, user: req.user, comments: thread, commentSort: sort, commentsAfter: after }));++ const after = /^\d{1,15}$/.test(String(query.after || '')) ? String(query.after) : null;++ const sort = query.sort === 'old' ? 'old' : 'new';++ const thread = paste.burn_after_read ? null : await comments.forRef(viewer, pasteRef(paste.slug), { sort, after, limit: 30 });++ return {++ status: 200, slug: paste.slug,++ body: pages.pastePage({ paste, related, user, comments: thread, commentSort: sort, commentsAfter: after }),++ };++ }++++ app.get('/p/:slug', withUser, wrap(async (req, res) => {++ const out = await pasteView(req.params.slug, req.query, { viewer: req.viewer, user: req.user, ctx: ctxOf(req) });++ if (!out) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }), 404);++ if (out.status === 200) twinLink(res, `/p/${encodeURIComponent(out.slug)}`);++ html(res, out.body, out.status);}));// No-JS comment form: signed in, the cookie (SameSite=Lax) and no Origin from another site, as on /c/:accessId.@@@ -129,6 +129,9 @@ function renderPage(o) {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,++ // 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).++ agentLayer: !!o.agentLayer && !!canonical,summary: description, url: canonical, updated: o.modified,navbar: nav, home: '/', navLinks: nav.links.map(({ label, href }) => ({ label, href })),footer: { service: 'community', variant: 'auto', path: o.canonicalPath, updates: '/updates' }, // full on the front page only (Shared 4.19)@@@ -254,6 +254,7 @@ const { url } = await res.json(); // "/p/<slug>"` },title: 'OpenVibe.Community: pastes, forums and the OpenVibe roadmap',description: 'Paste code, logs or a screenshot and get a short link. Talk it over in the forum, follow the OpenVibe roadmap, and see what is happening in Pulse.',canonicalPath: '/',++ agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)active: 'home',jsonLd: [ld.websiteLd(),@@@ -316,6 +317,7 @@ function browsePage(result, languages) {title,description: `Browse ${sort === 'views' ? 'the most viewed' : 'the latest'} pastes on OpenVibe.Community${lang ? ` in ${languageLabel(lang)}` : ''} — code, logs and screenshots shared by the people of OpenVibe.`,canonicalPath: canonical,++ agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)robots: q ? 'noindex,follow' : 'index,follow',active: 'pastes',jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pastes', url: '/pastes' }])],@@@ -439,6 +441,7 @@ ${related && related.length ? `<section class="section" id="related">title,description,canonicalPath: `/p/${p.slug}`,++ agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)robots: indexable ? 'index,follow' : 'noindex,follow',ogType: 'article',ogImage: shot || undefined,@@@ -10,7 +10,8 @@ const { spawn } = require('child_process');const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {-- htmlRawKB: 41, // 36.4 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 31.7 on 10-22: the home gained the forum's places, the roadmap, Pulse, real counts and six++ htmlRawKB: 42, // 41.6 with the agent layer's two discovery <link> tags (Shared 4.34.0, 2026-10-29); was 41 at 41.0++ // 36.4 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 31.7 on 10-22: the home gained the forum's places, the roadmap, Pulse, real counts and six// questions with their FAQPage JSON-LD; was 26 at 22.0htmlBrotliKB: 10, // 8.6 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); 7.3 on 10-22 (the same shelves); was 6.5 at 5.7jsFiles: 7, // 6: openvibe-shared/shell adds web-runtime.js (raised with the shell, 2026-10-04)
@@ -42,7 +42,7 @@ little progressive JavaScript for pagination, copy buttons and the upload path.
@@ -42,7 +42,7 @@ little progressive JavaScript for pagination, copy buttons and the upload path.
(a space's room), OpenVibe.Events (the outbox relay and the Pulse and account subscriptions)
(a space's room), OpenVibe.Events (the outbox relay and the Pulse and account subscriptions)
- OpenVibe.Live only for old VOD/clip comment imports (it reads and writes no pastes here any more)
- OpenVibe.Live only for old VOD/clip comment imports (it reads and writes no pastes here any more)
- `openvibe-contracts` v0.112.0, `openvibe-sdk` v0.67.0 (events outbox, per-actor limits, no-looks),
- `openvibe-contracts` v0.112.0, `openvibe-sdk` v0.67.0 (events outbox, per-actor limits, no-looks),
- `openvibe-shared` v4.33.0, pinned by release tarball
+ `openvibe-shared` v4.34.0, pinned by release tarball
## How it fits the network
## How it fits the network
@@ -846,5 +846,5 @@ release after paste comments moved onto the typed comment threads. [Submissions]
@@ -846,5 +846,5 @@ release after paste comments moved onto the typed comment threads. [Submissions]
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.222.0
- openvibe-contracts: v0.222.0
- openvibe-sdk: v0.67.0
- openvibe-sdk: v0.67.0
-- openvibe-shared: v4.33.0
+- openvibe-shared: v4.34.0
<!-- versions:end -->
<!-- versions:end -->
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
"domain": "openvibe.community",
"domain": "openvibe.community",
"contracts": "openvibe-contracts v0.222.0",
"contracts": "openvibe-contracts v0.222.0",
"sdk": "openvibe-sdk v0.67.0",
"sdk": "openvibe-sdk v0.67.0",
- "shared": "openvibe-shared v4.33.0",
+ "shared": "openvibe-shared v4.34.0",
"capabilities": [
"capabilities": [
"community.paste.create",
"community.paste.create",
"community.paste.write",
"community.paste.write",
@@ -44,5 +44,5 @@
@@ -44,5 +44,5 @@
"N-1 compatibility tests",
"N-1 compatibility tests",
"per-actor limits (openvibe-sdk/limits)"
"per-actor limits (openvibe-sdk/limits)"
],
],
- "updated": "2026-10-28"
+ "updated": "2026-10-29"
}
}
@@ -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.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -29,6 +29,12 @@
@@ -29,6 +29,12 @@
"node": ">=20.0.0"
"node": ">=20.0.0"
}
}
},
},
+ "node_modules/@acemir/cssom": {
+ "version": "0.9.32",
+ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.32.tgz",
+ "integrity": "sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==",
+ "license": "MIT"
+ },
"node_modules/@electric-sql/pglite": {
"node_modules/@electric-sql/pglite": {
"version": "0.5.8",
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",
@@ -124,6 +130,19 @@
@@ -124,6 +130,19 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
+ "node_modules/boolbase": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",
+ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/buffer-equal-constant-time": {
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -237,6 +256,39 @@
@@ -237,6 +256,39 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
"license": "MIT"
},
},
+ "node_modules/css-select": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",
+ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^2.0.0",
+ "css-what": "^8.0.0",
+ "domhandler": "^6.0.1",
+ "domutils": "^4.0.2",
+ "nth-check": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",
+ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/debug": {
"node_modules/debug": {
"version": "2.6.9",
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -274,6 +326,73 @@
@@ -274,6 +326,73 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
+ "node_modules/dom-serializer": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",
+ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^3.0.0",
+ "domhandler": "^6.0.0",
+ "entities": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",
+ "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/domhandler": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",
+ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",
+ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^3.0.0",
+ "domelementtype": "^3.0.0",
+ "domhandler": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dotenv": {
"node_modules/dotenv": {
"version": "16.6.1",
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -324,6 +443,18 @@
@@ -324,6 +443,18 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
+ "node_modules/entities": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
+ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/es-define-property": {
"node_modules/es-define-property": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -591,6 +722,110 @@
@@ -591,6 +722,110 @@
"node": ">=12.0.0"
"node": ">=12.0.0"
}
}
},
},
+ "node_modules/html-escaper": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
+ "license": "MIT"
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
+ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "entities": "^7.0.1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/htmlparser2/node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/http-errors": {
"node_modules/http-errors": {
"version": "2.0.1",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -736,6 +971,30 @@
@@ -736,6 +971,30 @@
"safe-buffer": "^5.0.1"
"safe-buffer": "^5.0.1"
}
}
},
},
+ "node_modules/linkedom": {
+ "version": "0.18.14",
+ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.14.tgz",
+ "integrity": "sha512-BRUnkOm4YR5vo+qI3j0EbungB0vZPSCue5AdnTQRaxAGSbsOZiBFRO0rh4zlo8qXUhli8pxvnpEXDklMnlq2Rg==",
+ "license": "ISC",
+ "dependencies": {
+ "@acemir/cssom": "^0.9.32",
+ "css-select": "^7.0.0",
+ "html-escaper": "^3.0.3",
+ "htmlparser2": "^10.1.0",
+ "uhyphen": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "canvas": ">= 2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/lodash.defaults": {
"node_modules/lodash.defaults": {
"version": "4.2.0",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@@ -892,6 +1151,22 @@
@@ -892,6 +1151,22 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
+ "node_modules/nth-check": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",
+ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
"node_modules/object-inspect": {
"node_modules/object-inspect": {
"version": "1.13.4",
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -961,12 +1236,13 @@
@@ -961,12 +1236,13 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.33.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
- "integrity": "sha512-t9XvhrQ+lQebOrFTjMCzI6/7/i4iQn9dmMr/8HVTNW1XoV1exyJa7IH/HfyyKuLxjA8xaowrDfD28LwKIibBNg==",
+ "version": "4.34.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
+ "integrity": "sha512-Rj5uRO3MbWkyIYAm+vK4Y94ugDNYEjEgGkYnPjSej4yMqhalpaPCkU96dzphMGiOUGP1PNWsiKhwaBj8tewgBg==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "jsonwebtoken": "^9.0.0"
+ "jsonwebtoken": "^9.0.0",
+ "linkedom": "^0.18.13"
},
},
"bin": {
"bin": {
"openvibe-build-assets": "scripts/build-assets.js"
"openvibe-build-assets": "scripts/build-assets.js"
@@ -1417,6 +1693,12 @@
@@ -1417,6 +1693,12 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
+ "node_modules/uhyphen": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
+ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
+ "license": "ISC"
+ },
"node_modules/unpipe": {
"node_modules/unpipe": {
"version": "1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -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.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+'use strict';
+// What openvibe.community tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.
+// Every field is something this host really serves: the public home, paste browse and paste pages (with
+// their comment threads), the native paste API under /api/pastes and the typed API under /api/v1, their
+// OpenAPI document, llms.txt/llms-full.txt and the sitemap.
+module.exports = {
+ id: 'community',
+ name: 'OpenVibe.Community',
+ purpose: 'The community hub of the OpenVibe network: pastes, forum spaces and threads, comment threads, Pulse and public submissions.',
+ home: 'https://openvibe.community',
+ site: 'openvibe.community',
+ api: '/api',
+ openapi: '/openapi.json',
+ policy: {
+ auth: ['none', 'Network OIDC', 'bearer'],
+ rateLimit: 'The public pages are not limited by the app; /api is limited to 120 requests a minute per address and sign-in attempts to 60 per 15 minutes',
+ },
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
+'use strict';
+// The Markdown twins of openvibe.community's public pages (openvibe-shared/agent-layer): the home,
+// /pastes and a paste. The bodies come from the same page builders the HTML routes use (render/pages.js,
+// called through app.js's views with the anonymous viewer and noView), so a twin cannot drift from its
+// page and never carries a signed-in person's view. A pasted twin carries the paste's text as prose and
+// its comments as the page's threaded comment list.
+//
+// A private, deleted, burned or missing paste answers 404 and has no twin; a burn-after-read paste is
+// never consumed by a twin (app.js checks before the read).
+const TWIN_PATH_RE = /^\/(?:pastes|p\/[^/]+)?$/;
+
+function createAgentPages({ views }) {
+ async function renderPublicPage(path, { query } = {}) {
+ const p = String(path || '/').replace(/\/+$/, '') || '/';
+ if (!TWIN_PATH_RE.test(p)) return null;
+ const q = Object.fromEntries(new URLSearchParams(query ? query.toString() : ''));
+ let html;
+ if (p === '/') html = await views.home();
+ else if (p === '/pastes') html = await views.browse(q);
+ else {
+ let slug;
+ try { slug = decodeURIComponent(p.slice(3)); } catch { return null; }
+ const out = await views.paste(slug, q);
+ if (!out || out.status !== 200) return null;
+ html = out.body;
+ }
+ return html ? { html } : null;
+ }
+
+ return { renderPublicPage };
+}
+
+module.exports = { createAgentPages, TWIN_PATH_RE };
@@ -40,7 +40,7 @@ const { assetVersion, SITE_NAME, DEFAULT_DESCRIPTION } = require('./render/layou
@@ -40,7 +40,7 @@ const { assetVersion, SITE_NAME, DEFAULT_DESCRIPTION } = require('./render/layou
const { createAuthClient, createAuthRoutes } = require('./auth/routes');
const { createAuthClient, createAuthRoutes } = require('./auth/routes');
const { getDb } = require('./db');
const { getDb } = require('./db');
const { createNetworkIdentity } = require('./identity/network');
const { createNetworkIdentity } = require('./identity/network');
-const { createViewerResolver } = require('./identity/viewer');
+const { createViewerResolver, ANONYMOUS } = require('./identity/viewer');
const v1 = require('./http/v1');
const v1 = require('./http/v1');
const { createCommentService } = require('./comments/service');
const { createCommentService } = require('./comments/service');
const { createCommentsApi } = require('./comments/api');
const { createCommentsApi } = require('./comments/api');
@@ -276,7 +276,8 @@ async function createApp(opts = {}) {
@@ -276,7 +276,8 @@ async function createApp(opts = {}) {
// ── Machine endpoints ────────────────────────────────────
// ── Machine endpoints ────────────────────────────────────
// Written by openvibe-shared/seo from server/discovery.js; the sitemap is rebuilt at most hourly.
// Written by openvibe-shared/seo from server/discovery.js; the sitemap is rebuilt at most hourly.
app.get('/robots.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))
app.get('/robots.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))
- .send(seo.robotsTxt({ sitemaps: [`${config.baseUrl}/sitemap.xml`], disallow: discovery.ROBOTS_DISALLOW })));
+ .send(seo.robotsTxt({ sitemaps: [`${config.baseUrl}/sitemap.xml`], disallow: discovery.ROBOTS_DISALLOW })
+ + `# Agent manifest: ${config.baseUrl}/.well-known/agent.json\n`));
app.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))
app.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))
.send(seo.llmsTxt({ name: SITE_NAME, summary: discovery.SUMMARY, details: discovery.CONTENT_LABELS, sections: discovery.llmsSections() })));
.send(seo.llmsTxt({ name: SITE_NAME, summary: discovery.SUMMARY, details: discovery.CONTENT_LABELS, sections: discovery.llmsSections() })));
app.get('/llms-full.txt', wrap(async (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))
app.get('/llms-full.txt', wrap(async (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))
@@ -310,6 +311,12 @@ async function createApp(opts = {}) {
@@ -310,6 +311,12 @@ async function createApp(opts = {}) {
const withUser = viewers.middleware({ services: false });
const withUser = viewers.middleware({ services: false });
const ctxOf = (req) => ({ token: req.token, ip: req.ip, userAgent: req.get('user-agent') || '', viewer: req.viewer });
const ctxOf = (req) => ({ token: req.token, ip: req.ip, userAgent: req.get('user-agent') || '', viewer: req.viewer });
const html = (res, body, status = 200) => res.status(status).type('html').set('Cache-Control', PAGE_CACHE).send(body);
const html = (res, body, status = 200) => res.status(status).type('html').set('Cache-Control', PAGE_CACHE).send(body);
+ // A public page with a Markdown twin names it in the Link header and varies on Accept. Only the
+ // pages the agent layer twins (the home, /pastes and a paste) call this.
+ const twinLink = (res, pathname) => {
+ res.vary('Accept');
+ res.set('Link', require('openvibe-shared/agent-layer').linkHeaderTags(`${config.baseUrl}${pathname}`, `${config.baseUrl}/llms.txt`).header);
+ };
// Every page response below (redirects and error pages too) that sets no policy of its own.
// Every page response below (redirects and error pages too) that sets no policy of its own.
app.use(cache.applyHtml({ private: true }));
app.use(cache.applyHtml({ private: true }));
@@ -344,10 +351,50 @@ async function createApp(opts = {}) {
@@ -344,10 +351,50 @@ async function createApp(opts = {}) {
return value;
return value;
}
}
- app.get('/', withUser, wrap(async (req, res) => {
+ // The public pages the agent layer twins, built once here and used by both the HTML routes and
+ // server/agent-pages.js. A twin passes the anonymous viewer and noView: it must never carry a
+ // signed-in person's view, count a page view or consume a burn-after-read paste.
+ async function homeView(user) {
// People's pastes lead; OpenVibe.AI's moment pastes from streams have their own shelf.
// People's pastes lead; OpenVibe.AI's moment pastes from streams have their own shelf.
const [latest, trending, fromStreams, languages, extras] = await Promise.all([catalog.latest(12, { from: 'people' }), catalog.trending(8, { from: 'people' }), catalog.latest(8, { from: 'streams' }), catalog.languages(), readHomeExtras()]);
const [latest, trending, fromStreams, languages, extras] = await Promise.all([catalog.latest(12, { from: 'people' }), catalog.trending(8, { from: 'people' }), catalog.latest(8, { from: 'streams' }), catalog.languages(), readHomeExtras()]);
- html(res, pages.homePage({ latest, trending, fromStreams, languages, user: req.user, ...extras }));
+ return pages.homePage({ latest, trending, fromStreams, languages, user, ...extras });
+ }
+
+ async function browseView(query) {
+ const [result, languages] = await Promise.all([catalog.browse(query), catalog.languages()]);
+ return pages.browsePage(result, languages);
+ }
+
+ const agentViews = {
+ home: () => homeView(null),
+ browse: (query) => browseView(query),
+ paste: (slug, query) => pasteView(slug, query, {
+ viewer: ANONYMOUS, user: null, twin: true,
+ ctx: { viewer: ANONYMOUS, noView: true, ip: '127.0.0.1', userAgent: '' },
+ }),
+ };
+
+ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json, and the Markdown twins of the
+ // home, /pastes and a paste. Gated to the manifest and twin requests only, so a browser (signed in or
+ // not) keeps the page its own route renders; the twins render through the same page builders signed out.
+ {
+ const agentRouter = express.Router();
+ require('openvibe-shared/agent-layer').mount(agentRouter, {
+ descriptor: require('./agent-descriptor'),
+ render: require('./agent-pages').createAgentPages({ views: agentViews }).renderPublicPage,
+ });
+ app.use((req, res, next) => {
+ // The manifest exactly: the rest of /.well-known/ (security.txt) belongs to the app.
+ if (req.path === '/.well-known/agent.json') return agentRouter(req, res, next);
+ if (String(req.path || '').endsWith('.md') || (req.query && req.query.format === 'markdown')
+ || req.accepts(['html', 'text/markdown']) === 'text/markdown') return agentRouter(req, res, next);
+ return next();
+ });
+ }
+
+ app.get('/', withUser, wrap(async (req, res) => {
+ twinLink(res, '/');
+ html(res, await homeView(req.user));
}));
}));
app.get('/updates', (req, res) => html(res, pages.updatesPage()));
app.get('/updates', (req, res) => html(res, pages.updatesPage()));
@@ -370,8 +417,8 @@ async function createApp(opts = {}) {
@@ -370,8 +417,8 @@ async function createApp(opts = {}) {
}));
}));
app.get('/pastes', withUser, wrap(async (req, res) => {
app.get('/pastes', withUser, wrap(async (req, res) => {
- const [result, languages] = await Promise.all([catalog.browse(req.query), catalog.languages()]);
- html(res, pages.browsePage(result, languages));
+ twinLink(res, req.originalUrl);
+ html(res, await browseView(req.query));
}));
}));
// Old Live-style /pastes/<slug> links and bare slugs land on the paste page.
// Old Live-style /pastes/<slug> links and bare slugs land on the paste page.
@@ -394,15 +441,20 @@ async function createApp(opts = {}) {
@@ -394,15 +441,20 @@ async function createApp(opts = {}) {
res.redirect(301, `/p/${clean}${rest}${q >= 0 ? req.originalUrl.slice(q) : ''}`);
res.redirect(301, `/p/${clean}${rest}${q >= 0 ? req.originalUrl.slice(q) : ''}`);
});
});
- app.get('/p/:slug', withUser, wrap(async (req, res) => {
- const { slug } = req.params;
- if (!SLUG_RE.test(slug)) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'That link does not point at a paste.' }), 404);
+ /** The paste page's body, or the error page it answers with; the HTML route and the twin share it. */
+ async function pasteView(slug, query, { viewer, user, ctx, twin = false } = {}) {
+ if (!SLUG_RE.test(slug)) return { status: 404, body: pages.errorPage({ status: 404, title: 'Paste not found', message: 'That link does not point at a paste.' }) };
+ // A twin is not a person reading: never consume a burn-after-read paste for one.
+ if (twin) {
+ const row = await db.prepare('SELECT burn_after_read FROM pastes WHERE slug = ?').get(slug);
+ if (!row || Number(row.burn_after_read)) return null;
+ }
let paste;
let paste;
try {
try {
- paste = await source.getPaste(slug, ctxOf(req));
+ paste = await source.getPaste(slug, ctx);
} catch (err) {
} catch (err) {
- if (err.status === 404) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }), 404);
- if (err.status === 410) return html(res, pages.errorPage({ status: 410, title: 'This paste has burned', message: 'It was set to burn after reading, and it has been read.' }), 410);
+ if (err.status === 404) return { status: 404, body: pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }) };
+ if (err.status === 410) return { status: 410, body: pages.errorPage({ status: 410, title: 'This paste has burned', message: 'It was set to burn after reading, and it has been read.' }) };
throw err;
throw err;
}
}
const related = await catalog.related(paste, 6);
const related = await catalog.related(paste, 6);
@@ -410,10 +462,20 @@ async function createApp(opts = {}) {
@@ -410,10 +462,20 @@ async function createApp(opts = {}) {
// comments API show, sorted (?sort=old|new) and paged (?after=<last comment id>) as there; null while
// comments API show, sorted (?sort=old|new) and paged (?after=<last comment id>) as there; null while
// nobody has commented. The comment service applies the paste's visibility again.
// nobody has commented. The comment service applies the paste's visibility again.
// A burn-after-read paste is gone once shown, so it has no comment panel.
// A burn-after-read paste is gone once shown, so it has no comment panel.
- const after = /^\d{1,15}$/.test(String(req.query.after || '')) ? String(req.query.after) : null;
- const sort = req.query.sort === 'old' ? 'old' : 'new';
- const thread = paste.burn_after_read ? null : await comments.forRef(req.viewer, pasteRef(paste.slug), { sort, after, limit: 30 });
- html(res, pages.pastePage({ paste, related, user: req.user, comments: thread, commentSort: sort, commentsAfter: after }));
+ const after = /^\d{1,15}$/.test(String(query.after || '')) ? String(query.after) : null;
+ const sort = query.sort === 'old' ? 'old' : 'new';
+ const thread = paste.burn_after_read ? null : await comments.forRef(viewer, pasteRef(paste.slug), { sort, after, limit: 30 });
+ return {
+ status: 200, slug: paste.slug,
+ body: pages.pastePage({ paste, related, user, comments: thread, commentSort: sort, commentsAfter: after }),
+ };
+ }
+
+ app.get('/p/:slug', withUser, wrap(async (req, res) => {
+ const out = await pasteView(req.params.slug, req.query, { viewer: req.viewer, user: req.user, ctx: ctxOf(req) });
+ if (!out) return html(res, pages.errorPage({ status: 404, title: 'Paste not found', message: 'It may have been deleted, burned after reading, or never existed.' }), 404);
+ if (out.status === 200) twinLink(res, `/p/${encodeURIComponent(out.slug)}`);
+ html(res, out.body, out.status);
}));
}));
// No-JS comment form: signed in, the cookie (SameSite=Lax) and no Origin from another site, as on /c/:accessId.
// No-JS comment form: signed in, the cookie (SameSite=Lax) and no Origin from another site, as on /c/:accessId.
@@ -129,6 +129,9 @@ function renderPage(o) {
@@ -129,6 +129,9 @@ function renderPage(o) {
siteName: SITE_NAME, description, canonical, robots: o.robots || 'index,follow',
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: o.ogImage || DEFAULT_OG_IMAGE, imageAlt: o.imageAlt,
jsonLd: o.jsonLd, alternates: o.alternates,
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).
+ agentLayer: !!o.agentLayer && !!canonical,
summary: description, url: canonical, updated: o.modified,
summary: description, url: canonical, updated: o.modified,
navbar: nav, home: '/', navLinks: nav.links.map(({ label, href }) => ({ label, href })),
navbar: nav, home: '/', navLinks: nav.links.map(({ label, href }) => ({ label, href })),
footer: { service: 'community', variant: 'auto', path: o.canonicalPath, updates: '/updates' }, // full on the front page only (Shared 4.19)
footer: { service: 'community', variant: 'auto', path: o.canonicalPath, updates: '/updates' }, // full on the front page only (Shared 4.19)
@@ -254,6 +254,7 @@ const { url } = await res.json(); // "/p/<slug>"` },
@@ -254,6 +254,7 @@ const { url } = await res.json(); // "/p/<slug>"` },
title: 'OpenVibe.Community: pastes, forums and the OpenVibe roadmap',
title: 'OpenVibe.Community: pastes, forums and the OpenVibe roadmap',
description: 'Paste code, logs or a screenshot and get a short link. Talk it over in the forum, follow the OpenVibe roadmap, and see what is happening in Pulse.',
description: 'Paste code, logs or a screenshot and get a short link. Talk it over in the forum, follow the OpenVibe roadmap, and see what is happening in Pulse.',
canonicalPath: '/',
canonicalPath: '/',
+ agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)
active: 'home',
active: 'home',
jsonLd: [
jsonLd: [
ld.websiteLd(),
ld.websiteLd(),
@@ -316,6 +317,7 @@ function browsePage(result, languages) {
@@ -316,6 +317,7 @@ function browsePage(result, languages) {
title,
title,
description: `Browse ${sort === 'views' ? 'the most viewed' : 'the latest'} pastes on OpenVibe.Community${lang ? ` in ${languageLabel(lang)}` : ''} — code, logs and screenshots shared by the people of OpenVibe.`,
description: `Browse ${sort === 'views' ? 'the most viewed' : 'the latest'} pastes on OpenVibe.Community${lang ? ` in ${languageLabel(lang)}` : ''} — code, logs and screenshots shared by the people of OpenVibe.`,
canonicalPath: canonical,
canonicalPath: canonical,
+ agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)
robots: q ? 'noindex,follow' : 'index,follow',
robots: q ? 'noindex,follow' : 'index,follow',
active: 'pastes',
active: 'pastes',
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pastes', url: '/pastes' }])],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pastes', url: '/pastes' }])],
@@ -439,6 +441,7 @@ ${related && related.length ? `<section class="section" id="related">
@@ -439,6 +441,7 @@ ${related && related.length ? `<section class="section" id="related">
title,
title,
description,
description,
canonicalPath: `/p/${p.slug}`,
canonicalPath: `/p/${p.slug}`,
+ agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)
robots: indexable ? 'index,follow' : 'noindex,follow',
robots: indexable ? 'index,follow' : 'noindex,follow',
ogType: 'article',
ogType: 'article',
ogImage: shot || undefined,
ogImage: shot || undefined,
@@ -10,7 +10,8 @@ const { spawn } = require('child_process');
@@ -10,7 +10,8 @@ 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: 41, // 36.4 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 31.7 on 10-22: the home gained the forum's places, the roadmap, Pulse, real counts and six
+ htmlRawKB: 42, // 41.6 with the agent layer's two discovery <link> tags (Shared 4.34.0, 2026-10-29); was 41 at 41.0
+ // 36.4 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 31.7 on 10-22: the home gained the forum's places, the roadmap, Pulse, real counts and six
// questions with their FAQPage JSON-LD; was 26 at 22.0
// questions with their FAQPage JSON-LD; was 26 at 22.0
htmlBrotliKB: 10, // 8.6 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); 7.3 on 10-22 (the same shelves); was 6.5 at 5.7
htmlBrotliKB: 10, // 8.6 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); 7.3 on 10-22 (the same shelves); was 6.5 at 5.7
jsFiles: 7, // 6: openvibe-shared/shell adds web-runtime.js (raised with the shell, 2026-10-04)
jsFiles: 7, // 6: openvibe-shared/shell adds web-runtime.js (raised with the shell, 2026-10-04)