openvibers/openvibe.community Commits
Answer the docs Markdown twins and stop linking templated API routes (#302)
Changes
@@@ -391,17 +391,17 @@ async function createApp(opts = {}) {+ `# Agent manifest: ${config.baseUrl}/.well-known/agent.json\n`));const { sections: llmsSections, optional: llmsOptional } = discovery.llmsSections();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, markdown: true, base: config.baseUrl, sections: llmsSections, optional: llmsOptional })));++ .send(seo.llmsTxt({ name: SITE_NAME, summary: discovery.SUMMARY, details: `${discovery.CONTENT_LABELS}\n\n${discovery.API_ROUTES}`, markdown: true, base: config.baseUrl, sections: llmsSections, optional: llmsOptional })));app.get('/llms-full.txt', wrap(async (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsFull({ site: SITE_NAME, summary: discovery.SUMMARY, base: config.baseUrl, sections: await discovery.llmsFullSections(), maxBytes: 512 * 1024 }))));-- // The sharing page people and agents are sent to (llms.txt's "Sharing a paste"): Markdown, one source.-- app.get('/docs/sharing', (_req, res) => res.type('text/plain; charset=utf-8')++ // The sharing page people and agents are sent to (llms.txt's "Sharing a paste"): Markdown, one source. The .md++ // address is the twin llms.txt links; the page itself keeps answering text/plain for a browser.++ const docPage = (file) => (req, res) => res.type(req.path.endsWith('.md') ? 'text/markdown; charset=utf-8' : 'text/plain; charset=utf-8').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))-- .send(require('fs').readFileSync(path.join(__dirname, '..', 'docs', 'sharing.md'), 'utf8')));++ .send(require('fs').readFileSync(path.join(__dirname, '..', 'docs', file), 'utf8'));++ app.get(['/docs/sharing', '/docs/sharing.md'], docPage('sharing.md'));// Locked pastes (Locker design 2026-11-06): the format, the three locks and the agent path, one source.-- app.get('/docs/locked-pastes', (_req, res) => res.type('text/plain; charset=utf-8')-- .set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))-- .send(require('fs').readFileSync(path.join(__dirname, '..', 'docs', 'locked-pastes.md'), 'utf8')));++ app.get(['/docs/locked-pastes', '/docs/locked-pastes.md'], docPage('locked-pastes.md'));app.get('/sitemap.xml', async (_req, res) => {let rows;try { rows = await discovery.sitemapRows(); } catch { return res.status(503).end(); }@@@ -16,6 +16,11 @@ const SITEMAP_TTL_MS = 60 * 60 * 1000;const ROBOTS_DISALLOW = ['/api/', '/auth/', '/my', '/new', '/forum/*/new', '/*?sso='];const SUMMARY = 'The discussion side of the OpenVibe network: pastes (text and screenshots), forums with threads, and the Pulse feed of what is happening across the network.';const CONTENT_LABELS = 'Pastes made by the OpenVibe AI (moments caught from live streams) carry origin "ai" and are labelled as AI-generated; everything else was written by people.';++// A path with a variable is a route to call, not a page to fetch. It is written as code, never as a Markdown link,++// so an agent (or a link checker doing a GET) does not ask the server for the literal {slug} address.++const API_ROUTES = 'API routes with a variable in the path are code, not pages to fetch:\n'++ + '- `PUT /api/v1/paste/{slug}/share` with {"audience":"people","members":[{"name":"@maticus","role":"viewer"}]}\n'++ + '- `POST /api/v1/paste/{slug}/share/transfer/{accept,decline,cancel}` with {"transfer_id":"<uuid>"} as the recipient (accept, decline) or the owner (cancel)';// The forum service (server/forum/service.js), when the app has one: threads join the sitemap,// the feeds and /llms-full.txt.@@@ -35,15 +40,12 @@ function llmsSections() {] },// The API is a primary entry for an agent, not something to skip.{ title: 'API', links: [{ title: 'OpenAPI 3.1 document', url: `${u}/openapi.json`, note: 'every /api route, with its fields' }] },-- // Sharing (design 2026-11-06): one audience and a transfer, from the page or from an agent.++ // Sharing (design 2026-11-06): one audience and a transfer, from the page or from an agent. The two++ // routes with a {slug} are code in the llms.txt details, not links (API_ROUTES).{ title: 'Sharing a paste', links: [{ title: 'Who can open a paste', url: `${u}/docs/sharing`, note: 'Everyone, anyone with the link, specific people (@names), only me; transfer ownership to a person' },-- { title: 'Share one paste with a bearer token', url: `${u}/api/v1/paste/{slug}/share`,-- note: `PUT with {"audience":"people","members":[{"name":"@maticus","role":"viewer"}]}; curl -X PUT -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' ${u}/api/v1/paste/<slug>/share -d '{"audience":"people","members":[{"name":"@maticus"}]}'` },{ title: 'Make a paste only two people can open, and offer it, in one call', url: `${u}/api/pastes`,note: `curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' ${u}/api/pastes -d '{"content":"the notes","audience":"people","members":["@maticus"],"transfer_to":"@maticus","keep_access":false}'` },-- { title: 'Accept, decline or cancel a transfer', url: `${u}/api/v1/paste/{slug}/share/transfer/{accept,decline,cancel}`,-- note: `POST {"transfer_id":"<uuid>"} as the recipient (accept/decline) or the owner (cancel)` },] },// Locked pastes (Locker design 2026-11-06): sealed in the browser, never readable by us.{ title: 'Locked pastes', links: [@@@ -145,4 +147,4 @@ function postFeedItems(space, thread, posts) {function resetCaches() { _sitemap = null; _sitemapAt = 0; }--module.exports = { ROBOTS_DISALLOW, SUMMARY, CONTENT_LABELS, llmsSections, llmsFullSections, sitemapRows, pasteFeedItems, threadFeedItems, postFeedItems, useForum, resetCaches };++module.exports = { ROBOTS_DISALLOW, SUMMARY, CONTENT_LABELS, API_ROUTES, llmsSections, llmsFullSections, sitemapRows, pasteFeedItems, threadFeedItems, postFeedItems, useForum, resetCaches };@@@ -88,6 +88,19 @@ const { boot, check, done } = require('./helpers/app');}});++ await check('llms.txt writes templated API paths as code, and the docs twins answer Markdown with their headings', async () => {++ const r = await t.get('/llms.txt');++ assert.ok(!/\]\([^)]*\{slug\}/.test(r.text), 'no link carries a {slug}: an agent must not fetch the literal address');++ assert.ok(r.text.includes('`PUT /api/v1/paste/{slug}/share`'), 'the share route is code');++ assert.ok(r.text.includes('`POST /api/v1/paste/{slug}/share/transfer/{accept,decline,cancel}`'), 'the transfer route is code');++ for (const [path, heading] of [['/docs/sharing.md', '# Sharing a paste'], ['/docs/locked-pastes.md', '# Locked pastes on OpenVibe Community (Locker)']]) {++ const twin = await t.get(path);++ assert.strictEqual(twin.status, 200, `${path} answers 200`);++ assert.match(twin.headers.get('content-type'), /^text\/markdown/, `${path} is Markdown`);++ assert.ok(twin.text.includes(heading), `${path} names the page heading`);++ }++ });++await check('sitemap.xml lists the pages meant for search and never a link-shared paste (SEO-29)', async () => {const r = await t.get('/sitemap.xml');assert.strictEqual(r.status, 200);
@@ -391,17 +391,17 @@ async function createApp(opts = {}) {
@@ -391,17 +391,17 @@ async function createApp(opts = {}) {
+ `# Agent manifest: ${config.baseUrl}/.well-known/agent.json\n`));
+ `# Agent manifest: ${config.baseUrl}/.well-known/agent.json\n`));
const { sections: llmsSections, optional: llmsOptional } = discovery.llmsSections();
const { sections: llmsSections, optional: llmsOptional } = discovery.llmsSections();
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, markdown: true, base: config.baseUrl, sections: llmsSections, optional: llmsOptional })));
+ .send(seo.llmsTxt({ name: SITE_NAME, summary: discovery.SUMMARY, details: `${discovery.CONTENT_LABELS}\n\n${discovery.API_ROUTES}`, markdown: true, base: config.baseUrl, sections: llmsSections, optional: llmsOptional })));
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 }))
.send(seo.llmsFull({ site: SITE_NAME, summary: discovery.SUMMARY, base: config.baseUrl, sections: await discovery.llmsFullSections(), maxBytes: 512 * 1024 }))));
.send(seo.llmsFull({ site: SITE_NAME, summary: discovery.SUMMARY, base: config.baseUrl, sections: await discovery.llmsFullSections(), maxBytes: 512 * 1024 }))));
- // The sharing page people and agents are sent to (llms.txt's "Sharing a paste"): Markdown, one source.
- app.get('/docs/sharing', (_req, res) => res.type('text/plain; charset=utf-8')
+ // The sharing page people and agents are sent to (llms.txt's "Sharing a paste"): Markdown, one source. The .md
+ // address is the twin llms.txt links; the page itself keeps answering text/plain for a browser.
+ const docPage = (file) => (req, res) => res.type(req.path.endsWith('.md') ? 'text/markdown; charset=utf-8' : 'text/plain; charset=utf-8')
.set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))
.set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))
- .send(require('fs').readFileSync(path.join(__dirname, '..', 'docs', 'sharing.md'), 'utf8')));
+ .send(require('fs').readFileSync(path.join(__dirname, '..', 'docs', file), 'utf8'));
+ app.get(['/docs/sharing', '/docs/sharing.md'], docPage('sharing.md'));
// Locked pastes (Locker design 2026-11-06): the format, the three locks and the agent path, one source.
// Locked pastes (Locker design 2026-11-06): the format, the three locks and the agent path, one source.
- app.get('/docs/locked-pastes', (_req, res) => res.type('text/plain; charset=utf-8')
- .set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }))
- .send(require('fs').readFileSync(path.join(__dirname, '..', 'docs', 'locked-pastes.md'), 'utf8')));
+ app.get(['/docs/locked-pastes', '/docs/locked-pastes.md'], docPage('locked-pastes.md'));
app.get('/sitemap.xml', async (_req, res) => {
app.get('/sitemap.xml', async (_req, res) => {
let rows;
let rows;
try { rows = await discovery.sitemapRows(); } catch { return res.status(503).end(); }
try { rows = await discovery.sitemapRows(); } catch { return res.status(503).end(); }
@@ -16,6 +16,11 @@ const SITEMAP_TTL_MS = 60 * 60 * 1000;
@@ -16,6 +16,11 @@ const SITEMAP_TTL_MS = 60 * 60 * 1000;
const ROBOTS_DISALLOW = ['/api/', '/auth/', '/my', '/new', '/forum/*/new', '/*?sso='];
const ROBOTS_DISALLOW = ['/api/', '/auth/', '/my', '/new', '/forum/*/new', '/*?sso='];
const SUMMARY = 'The discussion side of the OpenVibe network: pastes (text and screenshots), forums with threads, and the Pulse feed of what is happening across the network.';
const SUMMARY = 'The discussion side of the OpenVibe network: pastes (text and screenshots), forums with threads, and the Pulse feed of what is happening across the network.';
const CONTENT_LABELS = 'Pastes made by the OpenVibe AI (moments caught from live streams) carry origin "ai" and are labelled as AI-generated; everything else was written by people.';
const CONTENT_LABELS = 'Pastes made by the OpenVibe AI (moments caught from live streams) carry origin "ai" and are labelled as AI-generated; everything else was written by people.';
+// A path with a variable is a route to call, not a page to fetch. It is written as code, never as a Markdown link,
+// so an agent (or a link checker doing a GET) does not ask the server for the literal {slug} address.
+const API_ROUTES = 'API routes with a variable in the path are code, not pages to fetch:\n'
+ + '- `PUT /api/v1/paste/{slug}/share` with {"audience":"people","members":[{"name":"@maticus","role":"viewer"}]}\n'
+ + '- `POST /api/v1/paste/{slug}/share/transfer/{accept,decline,cancel}` with {"transfer_id":"<uuid>"} as the recipient (accept, decline) or the owner (cancel)';
// The forum service (server/forum/service.js), when the app has one: threads join the sitemap,
// The forum service (server/forum/service.js), when the app has one: threads join the sitemap,
// the feeds and /llms-full.txt.
// the feeds and /llms-full.txt.
@@ -35,15 +40,12 @@ function llmsSections() {
@@ -35,15 +40,12 @@ function llmsSections() {
] },
] },
// The API is a primary entry for an agent, not something to skip.
// The API is a primary entry for an agent, not something to skip.
{ title: 'API', links: [{ title: 'OpenAPI 3.1 document', url: `${u}/openapi.json`, note: 'every /api route, with its fields' }] },
{ title: 'API', links: [{ title: 'OpenAPI 3.1 document', url: `${u}/openapi.json`, note: 'every /api route, with its fields' }] },
- // Sharing (design 2026-11-06): one audience and a transfer, from the page or from an agent.
+ // Sharing (design 2026-11-06): one audience and a transfer, from the page or from an agent. The two
+ // routes with a {slug} are code in the llms.txt details, not links (API_ROUTES).
{ title: 'Sharing a paste', links: [
{ title: 'Sharing a paste', links: [
{ title: 'Who can open a paste', url: `${u}/docs/sharing`, note: 'Everyone, anyone with the link, specific people (@names), only me; transfer ownership to a person' },
{ title: 'Who can open a paste', url: `${u}/docs/sharing`, note: 'Everyone, anyone with the link, specific people (@names), only me; transfer ownership to a person' },
- { title: 'Share one paste with a bearer token', url: `${u}/api/v1/paste/{slug}/share`,
- note: `PUT with {"audience":"people","members":[{"name":"@maticus","role":"viewer"}]}; curl -X PUT -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' ${u}/api/v1/paste/<slug>/share -d '{"audience":"people","members":[{"name":"@maticus"}]}'` },
{ title: 'Make a paste only two people can open, and offer it, in one call', url: `${u}/api/pastes`,
{ title: 'Make a paste only two people can open, and offer it, in one call', url: `${u}/api/pastes`,
note: `curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' ${u}/api/pastes -d '{"content":"the notes","audience":"people","members":["@maticus"],"transfer_to":"@maticus","keep_access":false}'` },
note: `curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' ${u}/api/pastes -d '{"content":"the notes","audience":"people","members":["@maticus"],"transfer_to":"@maticus","keep_access":false}'` },
- { title: 'Accept, decline or cancel a transfer', url: `${u}/api/v1/paste/{slug}/share/transfer/{accept,decline,cancel}`,
- note: `POST {"transfer_id":"<uuid>"} as the recipient (accept/decline) or the owner (cancel)` },
] },
] },
// Locked pastes (Locker design 2026-11-06): sealed in the browser, never readable by us.
// Locked pastes (Locker design 2026-11-06): sealed in the browser, never readable by us.
{ title: 'Locked pastes', links: [
{ title: 'Locked pastes', links: [
@@ -145,4 +147,4 @@ function postFeedItems(space, thread, posts) {
@@ -145,4 +147,4 @@ function postFeedItems(space, thread, posts) {
function resetCaches() { _sitemap = null; _sitemapAt = 0; }
function resetCaches() { _sitemap = null; _sitemapAt = 0; }
-module.exports = { ROBOTS_DISALLOW, SUMMARY, CONTENT_LABELS, llmsSections, llmsFullSections, sitemapRows, pasteFeedItems, threadFeedItems, postFeedItems, useForum, resetCaches };
+module.exports = { ROBOTS_DISALLOW, SUMMARY, CONTENT_LABELS, API_ROUTES, llmsSections, llmsFullSections, sitemapRows, pasteFeedItems, threadFeedItems, postFeedItems, useForum, resetCaches };
@@ -88,6 +88,19 @@ const { boot, check, done } = require('./helpers/app');
@@ -88,6 +88,19 @@ const { boot, check, done } = require('./helpers/app');
}
}
});
});
+ await check('llms.txt writes templated API paths as code, and the docs twins answer Markdown with their headings', async () => {
+ const r = await t.get('/llms.txt');
+ assert.ok(!/\]\([^)]*\{slug\}/.test(r.text), 'no link carries a {slug}: an agent must not fetch the literal address');
+ assert.ok(r.text.includes('`PUT /api/v1/paste/{slug}/share`'), 'the share route is code');
+ assert.ok(r.text.includes('`POST /api/v1/paste/{slug}/share/transfer/{accept,decline,cancel}`'), 'the transfer route is code');
+ for (const [path, heading] of [['/docs/sharing.md', '# Sharing a paste'], ['/docs/locked-pastes.md', '# Locked pastes on OpenVibe Community (Locker)']]) {
+ const twin = await t.get(path);
+ assert.strictEqual(twin.status, 200, `${path} answers 200`);
+ assert.match(twin.headers.get('content-type'), /^text\/markdown/, `${path} is Markdown`);
+ assert.ok(twin.text.includes(heading), `${path} names the page heading`);
+ }
+ });
+
await check('sitemap.xml lists the pages meant for search and never a link-shared paste (SEO-29)', async () => {
await check('sitemap.xml lists the pages meant for search and never a link-shared paste (SEO-29)', async () => {
const r = await t.get('/sitemap.xml');
const r = await t.get('/sitemap.xml');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);