openvibers/openvibe.space Commits
Space: the agent layer (the manifest and the Markdown twins of the public pages) (#123)
Changes
@@@ -174,6 +174,30 @@ server {proxy_set_header X-Forwarded-Proto $scheme;}++ # ── Agent layer: the site manifest and the Markdown twins of the public pages ──++ # GET only. The manifest is matched exactly so the host's /.well-known/ snippet (the file that serves++ # security.txt) cannot swallow it; the twins are listed so they reach the app explicitly.++ location = /.well-known/agent.json {++ limit_except GET { deny all; }++ proxy_pass http://127.0.0.1:4940;++ proxy_http_version 1.1;++ proxy_set_header Host $host;++ proxy_set_header X-Real-IP $remote_addr;++ proxy_set_header X-Forwarded-For $remote_addr;++ proxy_set_header CF-Connecting-IP $remote_addr;++ proxy_set_header X-Forwarded-Proto $scheme;++ }++ location ~ "^/(index|updates)\.md$" {++ limit_except GET { deny all; }++ proxy_pass http://127.0.0.1:4940;++ proxy_http_version 1.1;++ proxy_set_header Host $host;++ proxy_set_header X-Real-IP $remote_addr;++ proxy_set_header X-Forwarded-For $remote_addr;++ proxy_set_header CF-Connecting-IP $remote_addr;++ proxy_set_header X-Forwarded-Proto $scheme;++ }++location ~ /\.(?!well-known) {deny all;}nnew file mode 100644@@@ -0,0 +1,13 @@++'use strict';++// The agent manifest of OpenVibe.Space: what the site is, where its API is, and how it is reached.++// openvibe-shared/agent-layer turns this into /.well-known/agent.json.++module.exports = {++ id: 'space', name: 'OpenVibe.Space',++ purpose: 'Static hosting for the OpenVibe network: a space is an app on your OpenVibe account, published at its own address.',++ home: 'https://openvibe.space', api: '/api/v1',++ docs: 'https://openvibe.services/docs/api',++ policy: {++ auth: ['none', 'bearer', 'Network OIDC'],++ rateLimit: 'The hosting API is rate limited per caller; the owner\'s plan sets the numbers.',++ },++};nnew file mode 100644@@@ -0,0 +1,29 @@++'use strict';++// The Markdown twins of OpenVibe.Space's public pages (openvibe-shared/agent-layer): the home and++// the update log.++//++// A twin is the page's own render, signed out: the same builder, the same data, no personalisation.++// The home takes no viewer at all and the update log renders the shared update log, so neither can++// carry a person's view. A path with no twin answers 404.++const TWIN_PATH_RE = /^\/(?:updates)?$/;++++/** The pages that have a twin (the path, without the query). */++function hasTwin(path) {++ const p = String(path || '/').replace(/\/+$/, '') || '/';++ if (p === '/index') return true;++ return TWIN_PATH_RE.test(p);++}++++function createAgentPages({ pages, config }) {++ async function renderPublicPage(path) {++ if (!hasTwin(path)) return null;++ const p = String(path || '/').replace(/\/+$/, '') || '/';++ if (p === '/' || p === '/index') return { html: pages.homePage() };++ if (p === '/updates') return { html: pages.updatesPage({ config }) };++ return null;++ }++++ return { renderPublicPage, hasTwin };++}++++module.exports = { createAgentPages, hasTwin, TWIN_PATH_RE };@@@ -142,11 +142,28 @@ async function createApp(opts = {}) {// /updates: what shipped on OpenVibe.Space, the shared update log every OpenVibe site has (it pointed at the// community's log, which lists the community's changes, not this site's).++ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the++ // public pages. Only the manifest and a twin request reach it — the layer renders every page signed out,++ // and the site's own routes answer every browser.++ {++ const agentRouter = express.Router();++ require('openvibe-shared/agent-layer').mount(agentRouter, {++ descriptor: require('./agent-descriptor'),++ render: require('./agent-pages').createAgentPages({ pages: require('./render/pages'), config }).renderPublicPage,++ });++ app.use((req, res, next) => {++ if (req.path === '/.well-known/agent.json') return agentRouter(req, res, next);++ const p = String(req.path || '');++ if (p.endsWith('.md') || (req.query && req.query.format === 'markdown')++ || req.accepts(['html', 'text/markdown']) === 'text/markdown') return agentRouter(req, res, next);++ return next();++ });++ }++app.get('/updates', (req, res) => {-- const { renderPage } = require('./render/layout');-- const frame = require('openvibe-shared/frame');-- const body = frame.updatesBody({ service: 'space', siteName: 'OpenVibe.Space' }) + `<script src="${require('openvibe-shared/serve').url('shipped.js')}" defer></script>`;-- res.type('html').send(renderPage({ title: 'What shipped on OpenVibe.Space', description: 'Every change deployed to OpenVibe.Space, newest first, from the OpenVibe network\'s shared update log.', canonicalPath: '/updates', body, viewer: req.viewer, config, path: req.originalUrl, indexable: true }));++ const html = require('./render/pages').updatesPage({ config, viewer: req.viewer });++ res.setHeader('Link', require('openvibe-shared/agent-layer').linkHeaderTags(`${config.baseUrl}/updates`, `${config.baseUrl}/llms.txt`).header);++ res.type('html').send(html);});app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-space', version: VERSION }));@@@ -186,7 +203,10 @@ async function createApp(opts = {}) {// ── Pages ────────────────────────────────────────────────// The public home page: what works now and what comes next.-- app.get('/', (_req, res) => res.type('html').set('Cache-Control', PAGE_CACHE).send(require('./render/pages').homePage()));++ app.get('/', (_req, res) => {++ res.setHeader('Link', require('openvibe-shared/agent-layer').linkHeaderTags(`${config.baseUrl}/`, `${config.baseUrl}/llms.txt`).header);++ res.type('html').set('Cache-Control', PAGE_CACHE).send(require('./render/pages').homePage());++ });// ── 404 / errors ─────────────────────────────────────────app.use((req, res) => {@@@ -126,6 +126,8 @@ function renderPage(o) {title: o.title || `${SITE_NAME} — spaces and static hosting`,titleSuffix: o.title ? ` · ${SITE_NAME}` : undefined,siteName: SITE_NAME, description, canonical, robots: o.robots || 'index,follow',++ agentLayer: o.agentLayer,++ llms: o.llms,type: o.ogType || 'website', image: o.ogImage || DEFAULT_OG_IMAGE, imageAlt: o.imageAlt,jsonLd: o.jsonLd, alternates: o.alternates,summary: description, url: canonical, updated: o.modified,@@@ -118,6 +118,7 @@ curl -X POST "https://openvibe.space/api/v1/sites/$SITE_ID/deploys?activate=1" \const site = String(config.publicUrl || config.baseUrl || 'https://openvibe.space').replace(/\/+$/, '');return renderPage({canonicalPath: '/', active: 'home', styles: [sc.STYLESHEET], body, description: HOME_DESCRIPTION,++ agentLayer: true, llms: '/llms.txt',jsonLd: [{ '@context': 'https://schema.org', '@type': 'WebSite', name: 'OpenVibe.Space', url: `${site}/`, description: HOME_DESCRIPTION },{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: HOME_FAQ.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })) },@@@ -137,4 +138,20 @@ function errorPage({ status = 500, title = 'Something went wrong', message = '',return renderPage({ title, description: message || title, canonicalPath: '/', robots: 'noindex,nofollow', footerVariant: 'compact', body });}--module.exports = { homePage, errorPage };++/**++ * The update log (/updates): the shared update log every OpenVibe site has. The agent layer's twin renders++ * the same page, signed out (no viewer), so the two cannot drift.++ */++function updatesPage({ config, viewer = null } = {}) {++ const frame = require('openvibe-shared/frame');++ const body = frame.updatesBody({ service: 'space', siteName: 'OpenVibe.Space' })++ + `<script src="${require('openvibe-shared/serve').url('shipped.js')}" defer></script>`;++ return renderPage({++ title: 'What shipped on OpenVibe.Space',++ description: 'Every change deployed to OpenVibe.Space, newest first, from the OpenVibe network\'s shared update log.',++ canonicalPath: '/updates', body, viewer, config, path: '/updates', indexable: true,++ agentLayer: true, llms: '/llms.txt',++ });++}++++module.exports = { homePage, updatesPage, errorPage };
@@ -174,6 +174,30 @@ server {
@@ -174,6 +174,30 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
+ # ── Agent layer: the site manifest and the Markdown twins of the public pages ──
+ # GET only. The manifest is matched exactly so the host's /.well-known/ snippet (the file that serves
+ # security.txt) cannot swallow it; the twins are listed so they reach the app explicitly.
+ location = /.well-known/agent.json {
+ limit_except GET { deny all; }
+ proxy_pass http://127.0.0.1:4940;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header CF-Connecting-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+ location ~ "^/(index|updates)\.md$" {
+ limit_except GET { deny all; }
+ proxy_pass http://127.0.0.1:4940;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header CF-Connecting-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
location ~ /\.(?!well-known) {
location ~ /\.(?!well-known) {
deny all;
deny all;
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+'use strict';
+// The agent manifest of OpenVibe.Space: what the site is, where its API is, and how it is reached.
+// openvibe-shared/agent-layer turns this into /.well-known/agent.json.
+module.exports = {
+ id: 'space', name: 'OpenVibe.Space',
+ purpose: 'Static hosting for the OpenVibe network: a space is an app on your OpenVibe account, published at its own address.',
+ home: 'https://openvibe.space', api: '/api/v1',
+ docs: 'https://openvibe.services/docs/api',
+ policy: {
+ auth: ['none', 'bearer', 'Network OIDC'],
+ rateLimit: 'The hosting API is rate limited per caller; the owner\'s plan sets the numbers.',
+ },
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
+'use strict';
+// The Markdown twins of OpenVibe.Space's public pages (openvibe-shared/agent-layer): the home and
+// the update log.
+//
+// A twin is the page's own render, signed out: the same builder, the same data, no personalisation.
+// The home takes no viewer at all and the update log renders the shared update log, so neither can
+// carry a person's view. A path with no twin answers 404.
+const TWIN_PATH_RE = /^\/(?:updates)?$/;
+
+/** The pages that have a twin (the path, without the query). */
+function hasTwin(path) {
+ const p = String(path || '/').replace(/\/+$/, '') || '/';
+ if (p === '/index') return true;
+ return TWIN_PATH_RE.test(p);
+}
+
+function createAgentPages({ pages, config }) {
+ async function renderPublicPage(path) {
+ if (!hasTwin(path)) return null;
+ const p = String(path || '/').replace(/\/+$/, '') || '/';
+ if (p === '/' || p === '/index') return { html: pages.homePage() };
+ if (p === '/updates') return { html: pages.updatesPage({ config }) };
+ return null;
+ }
+
+ return { renderPublicPage, hasTwin };
+}
+
+module.exports = { createAgentPages, hasTwin, TWIN_PATH_RE };
@@ -142,11 +142,28 @@ async function createApp(opts = {}) {
@@ -142,11 +142,28 @@ async function createApp(opts = {}) {
// /updates: what shipped on OpenVibe.Space, the shared update log every OpenVibe site has (it pointed at the
// /updates: what shipped on OpenVibe.Space, the shared update log every OpenVibe site has (it pointed at the
// community's log, which lists the community's changes, not this site's).
// community's log, which lists the community's changes, not this site's).
+ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the
+ // public pages. Only the manifest and a twin request reach it — the layer renders every page signed out,
+ // and the site's own routes answer every browser.
+ {
+ const agentRouter = express.Router();
+ require('openvibe-shared/agent-layer').mount(agentRouter, {
+ descriptor: require('./agent-descriptor'),
+ render: require('./agent-pages').createAgentPages({ pages: require('./render/pages'), config }).renderPublicPage,
+ });
+ app.use((req, res, next) => {
+ if (req.path === '/.well-known/agent.json') return agentRouter(req, res, next);
+ const p = String(req.path || '');
+ if (p.endsWith('.md') || (req.query && req.query.format === 'markdown')
+ || req.accepts(['html', 'text/markdown']) === 'text/markdown') return agentRouter(req, res, next);
+ return next();
+ });
+ }
+
app.get('/updates', (req, res) => {
app.get('/updates', (req, res) => {
- const { renderPage } = require('./render/layout');
- const frame = require('openvibe-shared/frame');
- const body = frame.updatesBody({ service: 'space', siteName: 'OpenVibe.Space' }) + `<script src="${require('openvibe-shared/serve').url('shipped.js')}" defer></script>`;
- res.type('html').send(renderPage({ title: 'What shipped on OpenVibe.Space', description: 'Every change deployed to OpenVibe.Space, newest first, from the OpenVibe network\'s shared update log.', canonicalPath: '/updates', body, viewer: req.viewer, config, path: req.originalUrl, indexable: true }));
+ const html = require('./render/pages').updatesPage({ config, viewer: req.viewer });
+ res.setHeader('Link', require('openvibe-shared/agent-layer').linkHeaderTags(`${config.baseUrl}/updates`, `${config.baseUrl}/llms.txt`).header);
+ res.type('html').send(html);
});
});
app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-space', version: VERSION }));
app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-space', version: VERSION }));
@@ -186,7 +203,10 @@ async function createApp(opts = {}) {
@@ -186,7 +203,10 @@ async function createApp(opts = {}) {
// ── Pages ────────────────────────────────────────────────
// ── Pages ────────────────────────────────────────────────
// The public home page: what works now and what comes next.
// The public home page: what works now and what comes next.
- app.get('/', (_req, res) => res.type('html').set('Cache-Control', PAGE_CACHE).send(require('./render/pages').homePage()));
+ app.get('/', (_req, res) => {
+ res.setHeader('Link', require('openvibe-shared/agent-layer').linkHeaderTags(`${config.baseUrl}/`, `${config.baseUrl}/llms.txt`).header);
+ res.type('html').set('Cache-Control', PAGE_CACHE).send(require('./render/pages').homePage());
+ });
// ── 404 / errors ─────────────────────────────────────────
// ── 404 / errors ─────────────────────────────────────────
app.use((req, res) => {
app.use((req, res) => {
@@ -126,6 +126,8 @@ function renderPage(o) {
@@ -126,6 +126,8 @@ function renderPage(o) {
title: o.title || `${SITE_NAME} — spaces and static hosting`,
title: o.title || `${SITE_NAME} — spaces and static hosting`,
titleSuffix: o.title ? ` · ${SITE_NAME}` : undefined,
titleSuffix: o.title ? ` · ${SITE_NAME}` : undefined,
siteName: SITE_NAME, description, canonical, robots: o.robots || 'index,follow',
siteName: SITE_NAME, description, canonical, robots: o.robots || 'index,follow',
+ agentLayer: o.agentLayer,
+ llms: o.llms,
type: o.ogType || 'website', image: o.ogImage || DEFAULT_OG_IMAGE, imageAlt: o.imageAlt,
type: o.ogType || 'website', image: o.ogImage || DEFAULT_OG_IMAGE, imageAlt: o.imageAlt,
jsonLd: o.jsonLd, alternates: o.alternates,
jsonLd: o.jsonLd, alternates: o.alternates,
summary: description, url: canonical, updated: o.modified,
summary: description, url: canonical, updated: o.modified,
@@ -118,6 +118,7 @@ curl -X POST "https://openvibe.space/api/v1/sites/$SITE_ID/deploys?activate=1" \
@@ -118,6 +118,7 @@ curl -X POST "https://openvibe.space/api/v1/sites/$SITE_ID/deploys?activate=1" \
const site = String(config.publicUrl || config.baseUrl || 'https://openvibe.space').replace(/\/+$/, '');
const site = String(config.publicUrl || config.baseUrl || 'https://openvibe.space').replace(/\/+$/, '');
return renderPage({
return renderPage({
canonicalPath: '/', active: 'home', styles: [sc.STYLESHEET], body, description: HOME_DESCRIPTION,
canonicalPath: '/', active: 'home', styles: [sc.STYLESHEET], body, description: HOME_DESCRIPTION,
+ agentLayer: true, llms: '/llms.txt',
jsonLd: [
jsonLd: [
{ '@context': 'https://schema.org', '@type': 'WebSite', name: 'OpenVibe.Space', url: `${site}/`, description: HOME_DESCRIPTION },
{ '@context': 'https://schema.org', '@type': 'WebSite', name: 'OpenVibe.Space', url: `${site}/`, description: HOME_DESCRIPTION },
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: HOME_FAQ.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })) },
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: HOME_FAQ.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })) },
@@ -137,4 +138,20 @@ function errorPage({ status = 500, title = 'Something went wrong', message = '',
@@ -137,4 +138,20 @@ function errorPage({ status = 500, title = 'Something went wrong', message = '',
return renderPage({ title, description: message || title, canonicalPath: '/', robots: 'noindex,nofollow', footerVariant: 'compact', body });
return renderPage({ title, description: message || title, canonicalPath: '/', robots: 'noindex,nofollow', footerVariant: 'compact', body });
}
}
-module.exports = { homePage, errorPage };
+/**
+ * The update log (/updates): the shared update log every OpenVibe site has. The agent layer's twin renders
+ * the same page, signed out (no viewer), so the two cannot drift.
+ */
+function updatesPage({ config, viewer = null } = {}) {
+ const frame = require('openvibe-shared/frame');
+ const body = frame.updatesBody({ service: 'space', siteName: 'OpenVibe.Space' })
+ + `<script src="${require('openvibe-shared/serve').url('shipped.js')}" defer></script>`;
+ return renderPage({
+ title: 'What shipped on OpenVibe.Space',
+ description: 'Every change deployed to OpenVibe.Space, newest first, from the OpenVibe network\'s shared update log.',
+ canonicalPath: '/updates', body, viewer, config, path: '/updates', indexable: true,
+ agentLayer: true, llms: '/llms.txt',
+ });
+}
+
+module.exports = { homePage, updatesPage, errorPage };