openvibers/openvibe.chat Commits

/vibe, /developers and /updates have the Markdown twins they name (#347)

OP OpenVibers committed 11 days ago · d37e35b22a · 3 files, +20 −8

Each named a twin in its head and Link header that answered 404 (11-03, tools/llms-twins-probe.js): the twin renderer
allowed only the home, servers, rooms and their pages. llms.txt now links them at their twins too.

Changes

ddiff --git a/server/web/agent-pages.js b/server/web/agent-pages.js
iindex d8e6922..039a8ba 100644
---- a/server/web/agent-pages.js
++++ b/server/web/agent-pages.js
@@@ -3,7 +3,9 @@
// page's own route in this router, anonymously and in-process, so a twin cannot drift from its page and a
// signed-in view is never used. Only pages whose rendered robots tag starts with "index" get a twin: private
// servers and rooms, /messages, /settings and the SEO-7 gated pages stay noindex and have none.
--const PUBLIC_PATH_RE = /^\/(?:$|servers$|rooms$|s\/[a-z0-9-]+(?:\/[a-z0-9-]+)?$|r\/[a-z0-9-]+$)/i;
++// /vibe, /developers and /updates are public pages too: each named a twin in its head and Link header that answered
++// 404 (11-03, tools/llms-twins-probe.js).
++const PUBLIC_PATH_RE = /^\/(?:$|servers$|rooms$|vibe$|developers$|updates$|s\/[a-z0-9-]+(?:\/[a-z0-9-]+)?$|r\/[a-z0-9-]+$)/i;
/** An anonymous GET request for one path, as the router's handlers expect it. */
function anonymousRequest(path, query = '') {
ddiff --git a/server/web/discovery.js b/server/web/discovery.js
iindex fed785d..9788665 100644
---- a/server/web/discovery.js
++++ b/server/web/discovery.js
@@@ -121,25 +121,24 @@ function createDiscoveryRoutes({ config }) {
router.get('/llms.txt', (req, res) => {
res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({
name: 'OpenVibe.Chat',
-- // AGT-5: own pages link their Markdown twins (openvibe-shared 4.108.0); Vibe, Developers and Updates have none
-- // and are marked HTML. ## Optional is what an agent may skip.
++ // AGT-5: own pages link their Markdown twins (openvibe-shared 4.108.0). ## Optional is what an agent may skip.
markdown: true,
base: site,
summary: 'OpenVibe.Chat: Vibe, OpenVibe\'s free assistant, global chat for the whole OpenVibe network, the chat of every stream on OpenVibe.Live, servers with text and voice channels, rooms, direct messages, calls and AI members.',
details: 'One OpenVibe account works here and on every other OpenVibe site. Anyone can read the global chat and the public rooms without an account; posting needs a signed-in person. The global chat and the public room pages are readable as plain HTML without JavaScript and update live over the site\'s own WebSocket. Direct messages, room settings and the API are per-person and are never listed here.',
sections: [
{ title: 'Start here', links: [
-- { title: 'Vibe', url: abs('/vibe'), html: true, note: 'OpenVibe\'s assistant: searches the web and OpenVibe, reads pages and pictures, remembers what it is asked to, sets up recurring work; chats are private to the signed-in person' },
++ { title: 'Vibe', url: abs('/vibe'), note: 'OpenVibe\'s assistant: searches the web and OpenVibe, reads pages and pictures, remembers what it is asked to, sets up recurring work; chats are private to the signed-in person' },
{ title: 'Global chat', url: abs('/'), note: 'one room for everyone on OpenVibe, newest messages at the end' },
{ title: 'Rooms', url: abs('/rooms'), note: 'every public room, with its topic and activity' },
{ title: 'Servers', url: abs('/servers'), note: 'public servers: groups with text and voice channels, roles and invites' },
] },
{ title: 'Build with it', links: [
-- { title: 'Developers', url: abs('/developers'), html: true, note: 'a support inbox or a room on any website with one tag, the visitor API, openvibe-sdk/chat, feeds from Claude Code, signed webhooks, the limits' },
++ { title: 'Developers', url: abs('/developers'), note: 'a support inbox or a room on any website with one tag, the visitor API, openvibe-sdk/chat, feeds from Claude Code, signed webhooks, the limits' },
] },
],
optional: [
-- { title: 'What shipped on OpenVibe.Chat', url: abs('/updates'), html: true },
++ { title: 'What shipped on OpenVibe.Chat', url: abs('/updates') },
{ title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages above' },
{ title: 'robots.txt', url: abs('/robots.txt') },
{ title: 'Full text for language models', url: abs('/llms-full.txt') },
ddiff --git a/test/discovery.test.js b/test/discovery.test.js
iindex f53204f..081f3c1 100644
---- a/test/discovery.test.js
++++ b/test/discovery.test.js
@@@ -43,12 +43,23 @@ t('/llms.txt describes the site, its public pages and its machine-readable endpo
assert.ok(r.text.includes(of_), `mentions ${of_}`);
}
for (const re of PRIVATE) assert.ok(!re.test(r.text), `no private path in llms.txt (${re})`);
-- // AGT-5: a page with a twin is linked at it; Vibe, Developers and Updates have none and are marked HTML; Optional last.
++ // AGT-5: every page is linked at its twin; Optional last.
assert.ok(r.text.includes(`(${SITE}/index.md)`) && r.text.includes(`(${SITE}/rooms.md)`), 'own pages link their twins');
-- assert.ok(r.text.includes(`(${SITE}/vibe) (HTML)`) && r.text.includes(`(${SITE}/updates) (HTML)`));
++ assert.ok(r.text.includes(`(${SITE}/vibe.md)`) && r.text.includes(`(${SITE}/updates.md)`) && r.text.includes(`(${SITE}/developers.md)`), 'Vibe, Developers and Updates have twins now');
assert.ok(r.text.includes('\n## Optional\n') && r.text.lastIndexOf('\n## ') === r.text.indexOf('\n## Optional\n'), 'Optional is last');
});
++t('every page that names a twin has one: /vibe, /developers and /updates too (11-03)', async () => {
++ for (const p of ['/vibe', '/developers', '/updates', '/']) {
++ const page = await fetch(`${h.base}${p}`, { headers: { accept: 'text/html' }, redirect: 'manual' });
++ const named = /text\/markdown/.test(page.headers.get('link') || '');
++ const twin = await req(p === '/' ? '/index.md' : `${p}.md`);
++ assert.ok(named, `${p} names a twin`);
++ assert.strictEqual(twin.status, 200, `${p}'s twin answers`);
++ assert.match(twin.type, /^text\/markdown/);
++ }
++});
++
t('/llms-full.txt carries every public page and what it is, at least 500 characters', async () => {
const r = await req('/llms-full.txt');
assert.strictEqual(r.status, 200);