openvibers/openvibe.services Commits

The docs pages have the Markdown twins they name (#209)

OP OpenVibers committed 10 days ago · e5c33a23f3 · 4 files, +51 −8

/docs/contracts, /docs/capabilities, /docs/events, /docs/services, /docs/tools, /docs/sdk and their item pages are
built inline by the docs router, which the twin renderer did not run, so each named twin answered 404 (11-03). The
renderer runs that router signed out and keeps a 200 HTML answer; llms.txt links those pages at their twins.

Changes

ddiff --git a/server/agent-pages.js b/server/agent-pages.js
iindex caed6ec..7707d42 100644
---- a/server/agent-pages.js
++++ b/server/agent-pages.js
@@@ -3,12 +3,39 @@
// view builders the routes use (http/pages.js and http/docs.js + render/layout.js's renderPage), so a twin
// cannot drift from its page. Anonymous only: /projects, /staff and the API are never rendered here.
const { createPageViews } = require('./http/pages');
--const { createDocsViews } = require('./http/docs');
++const { createDocsViews, createDocsRoutes } = require('./http/docs');
++
++// The docs pages the docs router builds inline (contracts, capabilities, events, services, tools, SDK, …) are twinned by
++// running that router signed out: each named a twin that answered 404 (11-03, tools/twin-named-check.js).
++const DOCS_ROUTER_RE = /^\/docs\/(?:contracts|capabilities|events|services|tools|billing|limits|export|sdk|adr|updates)(?:\/[^/]+)?$/;
++
++/** One GET through `router`, as a signed-out visitor; the HTML of a 200 answer, or null. */
++function renderThrough(router, mount, path, query) {
++ return new Promise((resolve) => {
++ const search = query ? query.toString() : '';
++ const sub = path.slice(mount.length) || '/';
++ const req = {
++ method: 'GET', url: sub + (search ? `?${search}` : ''), originalUrl: path + (search ? `?${search}` : ''), baseUrl: mount, path: sub,
++ params: {}, query: Object.fromEntries(new URLSearchParams(search)), headers: { accept: 'text/html', host: 'localhost' },
++ viewer: { kind: 'anonymous' }, get(name) { return this.headers[String(name).toLowerCase()]; },
++ };
++ const headers = {};
++ const res = {
++ statusCode: 200,
++ get: (k) => headers[String(k).toLowerCase()], set(k, v) { headers[String(k).toLowerCase()] = v; return this; },
++ status(code) { this.statusCode = code; return this; }, type(t) { headers['content-type'] = t; return this; },
++ send(body) { resolve(this.statusCode === 200 && /html/.test(String(headers['content-type'] || '')) && typeof body === 'string' ? body : null); return this; },
++ json() { resolve(null); return this; }, end() { resolve(null); return this; }, redirect() { resolve(null); return this; },
++ };
++ try { router.handle(req, res, () => resolve(null)); } catch { resolve(null); }
++ });
++}
const { renderPage } = require('./render/layout');
function createAgentPages(ctx) {
const pages = createPageViews(ctx);
const docs = createDocsViews(ctx);
++ const docsRouter = createDocsRoutes(ctx);
const twin = (o, path, query) => {
if (!o) return null;
@@@ -32,6 +59,10 @@ function createAgentPages(ctx) {
if (path === '/policy/transparency') return twin(pages.policyTransparency(), path);
if (path === '/docs') return twin(docs.index(), path);
if (path === '/docs/api') return twin(docs.apiExplorer(), path);
++ if (DOCS_ROUTER_RE.test(path)) {
++ const htmlOut = await renderThrough(docsRouter, '/docs', path, query);
++ return htmlOut ? { html: htmlOut } : null;
++ }
const api = /^\/docs\/api\/([^/]+)$/.exec(path);
if (api) {
const id = decodeURIComponent(api[1]);
ddiff --git a/server/http/discovery.js b/server/http/discovery.js
iindex dd5c883..4647ef3 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -224,12 +224,12 @@ function createDiscoveryRoutes(ctx) {
] },
{ title: 'Reference', links: [
{ title: 'API explorer', url: abs('/docs/api'), note: 'each service\'s OpenAPI document, per page as .json' },
-- { title: 'Contracts', url: abs('/docs/contracts'), html: true, note: 'schemas, field tables and fixtures, per page as .json' },
-- { title: 'Capabilities', url: abs('/docs/capabilities'), html: true },
-- { title: 'Events', url: abs('/docs/events'), html: true },
-- { title: 'Services', url: abs('/docs/services'), html: true },
-- { title: 'Tools', url: abs('/docs/tools'), html: true },
-- { title: 'SDK', url: abs('/docs/sdk'), html: true },
++ { title: 'Contracts', url: abs('/docs/contracts'), note: 'schemas, field tables and fixtures, per page as .json' },
++ { title: 'Capabilities', url: abs('/docs/capabilities') },
++ { title: 'Events', url: abs('/docs/events') },
++ { title: 'Services', url: abs('/docs/services') },
++ { title: 'Tools', url: abs('/docs/tools') },
++ { title: 'SDK', url: abs('/docs/sdk') },
] },
{ title: 'Extend OpenVibe', links: [
{ title: 'Mod the network', url: abs('/mod'), note: 'every way to extend OpenVibe today, and where packaged mods stand' },
ddiff --git a/test/discovery.test.js b/test/discovery.test.js
iindex da59b65..a499769 100644
---- a/test/discovery.test.js
++++ b/test/discovery.test.js
@@@ -61,7 +61,7 @@ const PRIVATE = ['/projects', '/staff', '/auth/', '/oauth/test-callback'];
assert.ok(/\n## Optional\n/.test(r.text), 'no Optional section');
assert.strictEqual(r.text.lastIndexOf('\n## '), r.text.indexOf('\n## Optional'), 'Optional is the last section');
// A page whose twin answers 404 keeps its URL and says (HTML); a file keeps its URL.
-- assert.ok(r.text.includes('(https://openvibe.services/docs/contracts) (HTML)'), 'the twin-less contracts page is marked (HTML)');
++ assert.ok(r.text.includes('(https://openvibe.services/docs/contracts.md)') && r.text.includes('(https://openvibe.services/oauth) (HTML)'), 'the docs pages link their twins; a page with none is marked (HTML)');
assert.ok(!/\]\(https:\/\/openvibe\.services\/mcp\)/.test(r.text), '/mcp must not be a page link');
assert.ok(/MCP server: POST https:\/\/openvibe\.services\/mcp/.test(r.text), 'the MCP endpoint is named in the details');
const own = [...r.text.matchAll(/\]\((https:\/\/openvibe\.services[^)\s]*)\)( \(HTML\))?/g)];
ddiff --git a/test/docs.test.js b/test/docs.test.js
iindex 258d3c7..41131c5 100644
---- a/test/docs.test.js
++++ b/test/docs.test.js
@@@ -28,6 +28,18 @@ const sdkPkg = require('openvibe-sdk/package.json');
assert.ok(contracts.resolve('services.app-manifest@1'));
});
++ await check('the docs pages have the Markdown twins they name (11-03: /docs/contracts and the rest answered 404)', async () => {
++ for (const p of ['/docs/contracts', '/docs/capabilities', '/docs/events', '/docs/sdk', '/docs/contracts/errors.problem', '/docs/sdk/core']) {
++ const page = await t.get(p);
++ assert.match(page.headers.get('link') || page.text, /text\/markdown/, `${p} names a twin`);
++ const twin = await t.get(`${p}.md`);
++ assert.strictEqual(twin.status, 200, `${p}.md`);
++ assert.match(twin.headers.get('content-type') || '', /^text\/markdown/, `${p}.md is Markdown`);
++ assert.ok(!/<\/?(div|table|td|tr|section)\b/i.test(twin.text), `${p}.md is Markdown, not HTML`);
++ }
++ assert.strictEqual((await t.get('/docs/contracts/no-such-contract.md')).status, 404, 'no page, no twin');
++ });
++
await check('every docs page states the versions it was generated from', async () => {
for (const p of ['/docs', '/docs/contracts', '/docs/capabilities', '/docs/events', '/docs/sdk', '/docs/contracts/errors.problem', '/docs/sdk/core']) {
const r = await t.get(p);