openvibers/openvibe.space Commits

Space: the agent layer (the manifest and the Markdown twins of the public pages) (#123)

OP OpenVibers committed 15 days ago · fd1804814b · 6 files, +111 −6

Changes

ddiff --git a/deploy/nginx/openvibe.space.conf b/deploy/nginx/openvibe.space.conf
iindex 9a5403c..a820ebd 100644
---- a/deploy/nginx/openvibe.space.conf
++++ b/deploy/nginx/openvibe.space.conf
@@@ -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;
}
ddiff --git a/server/agent-descriptor.js b/server/agent-descriptor.js
nnew file mode 100644
iindex 0000000..0f06dc4
---- /dev/null
++++ b/server/agent-descriptor.js
@@@ -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.',
++ },
++};
ddiff --git a/server/agent-pages.js b/server/agent-pages.js
nnew file mode 100644
iindex 0000000..598679d
---- /dev/null
++++ b/server/agent-pages.js
@@@ -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 };
ddiff --git a/server/app.js b/server/app.js
iindex 82f0959..c5fd8ac 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -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) => {
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex 03540dc..d96f70d 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -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,
ddiff --git a/server/render/pages.js b/server/render/pages.js
iindex 165c9a8..fd1d728 100644
---- a/server/render/pages.js
++++ b/server/render/pages.js
@@@ -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 };