openvibers/openvibe.community Commits

Answer the docs Markdown twins and stop linking templated API routes (#302)

OP OpenVibers committed 4 days ago · 0fb22276d1 · 3 files, +28 −13

llms.txt links /docs/sharing.md and /docs/locked-pastes.md, but only the
extensionless pages existed. Serve the same Markdown at the .md addresses
with text/markdown, and keep the extensionless pages text/plain.

llms.txt also linked /api/v1/paste/{slug}/share and its transfer route.
Those are routes with a variable, not pages, so they are written as code
in the details instead of links; a checker no longer GETs the literal
address.

Changes

ddiff --git a/server/app.js b/server/app.js
iindex 94afdae..51ef256 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -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(); }
ddiff --git a/server/discovery.js b/server/discovery.js
iindex 5e3530a..f81862c 100644
---- a/server/discovery.js
++++ b/server/discovery.js
@@@ -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 };
ddiff --git a/test/seo.test.js b/test/seo.test.js
iindex 9418a9b..7855fa3 100644
---- a/test/seo.test.js
++++ b/test/seo.test.js
@@@ -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);