openvibers/openvibe.search Commits
nginx: route the agent manifest and the twins to Search (#128)
Changes
@@@ -91,6 +91,10 @@ server {location = /llms.txt { proxy_pass http://127.0.0.1:4710; }location = /llms-full.txt { proxy_pass http://127.0.0.1:4710; }location = /sitemap.xml { proxy_pass http://127.0.0.1:4710; }++ # The agent layer (openvibe-shared/agent-layer): the manifest an agent reads first, and the Markdown twins of++ # the front page and /updates (a result page's twin answers on / with ?format=markdown, already proxied).++ location = /.well-known/agent.json { limit_except GET { deny all; } proxy_pass http://127.0.0.1:4710; }++ location ~ "^/(index|updates)\.md$" { limit_except GET { deny all; } proxy_pass http://127.0.0.1:4710; }# The IndexNow key file (8-128 letters and digits); Node answers 404 unless it is INDEXNOW_KEY.location ~ "^/[A-Za-z0-9]{8,128}\.txt$" { limit_except GET { deny all; } proxy_pass http://127.0.0.1:4710; }# This site's own pinned copy of the Frame's browser files (openvibe-shared/serve).@@@ -411,10 +411,11 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.networkconst origin = String(baseUrl).replace(/\/+$/, '');const router = express.Router();// Markdown twin requests and the agent manifest go to openvibe-shared/agent-layer, mounted after this router:-- // .md paths, ?format=markdown, an explicit text/markdown preference, and /.well-known/.++ // .md paths, ?format=markdown, an explicit text/markdown preference, and the manifest's own path (any other++ // /.well-known/ file stays this router's, exactly as security.txt does on Chat).router.use((req, res, next) => {const p = String(req.path || '');-- if (p.endsWith('.md') || p.startsWith('/.well-known/') || (req.query && req.query.format === 'markdown')++ if (p === '/.well-known/agent.json' || p.endsWith('.md') || (req.query && req.query.format === 'markdown')|| req.accepts(['html', 'text/markdown']) === 'text/markdown') return next('router');return next();});@@@ -424,6 +425,8 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.networkres.setHeader('Content-Security-Policy', CSP);res.setHeader('X-Frame-Options', 'DENY');res.setHeader('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));++ // This page has a Markdown twin (/updates.md), so its header names it, as the head tags do.++ res.setHeader('Link', seo.linkHeaderTags(`${origin}/updates`, `${origin}/llms.txt`).header);res.type('html').send(layout({ title: 'What shipped on OpenVibe.Search', q: '', owner: '', type: '', ownHeading: true, body: frame.updatesBody({ service: 'search', siteName: 'OpenVibe.Search' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`, canonical: `${origin}/updates` }));});@@@ -121,6 +121,34 @@ t('the route index is built from routes only: every entry gets a line of text, nassert.ok(!/saved-searches|\/api\/v1\/owners|\/internal\/events|\/api\/v1\/documents/.test(text));});++t('the agent layer: manifest, twins, and the pages that name them', async () => {++ const m = await request(svc.base, 'GET', '/.well-known/agent.json');++ assert.strictEqual(m.status, 200);++ assert.match(m.headers.get('content-type'), /application\/json/);++ const d = JSON.parse(m.text);++ assert.strictEqual(d.id, 'search');++ assert.strictEqual(d.home, 'https://search.openvibe.network');++ assert.strictEqual(d.api, 'https://search.openvibe.network/api/v1');++ assert.deepStrictEqual(d.policy.auth, ['none', 'Network OIDC']);++ // A twin of the front page and of the update log, from the pages' own render. The twin's canonical is the++ // manifest's home (the site's public address), not the test server's own base URL.++ for (const [p, canonical] of [['/index.md', 'https://search.openvibe.network/'], ['/updates.md', 'https://search.openvibe.network/updates']]) {++ const tw = await request(svc.base, 'GET', p);++ assert.strictEqual(tw.status, 200, `${p} answers`);++ assert.match(tw.headers.get('content-type'), /text\/markdown/, `${p} is markdown`);++ assert.ok(tw.text.includes(`canonical: "${canonical}"`), `${p} names its page`);++ }++ // The HTML page names its twin in the Link header and the head; a result page is noindex but still twinned.++ for (const p of ['/', '/updates']) {++ const page = await request(svc.base, 'GET', p, { headers: { accept: 'text/html' } });++ assert.strictEqual(page.status, 200);++ assert.match(String(page.headers.get('link')), /rel="alternate"; type="text\/markdown"/, `${p} names its twin`);++ assert.match(page.text, /<link rel="alternate" type="text\/markdown"/, `${p} carries the head tag`);++ }++ const results = await request(svc.base, 'GET', '/?q=page&format=markdown');++ assert.match(results.headers.get('content-type'), /text\/markdown/, 'a result twin answers for an agent');++});++t('shutdown', async () => { await svc.stop(); });t.run();
@@ -91,6 +91,10 @@ server {
@@ -91,6 +91,10 @@ server {
location = /llms.txt { proxy_pass http://127.0.0.1:4710; }
location = /llms.txt { proxy_pass http://127.0.0.1:4710; }
location = /llms-full.txt { proxy_pass http://127.0.0.1:4710; }
location = /llms-full.txt { proxy_pass http://127.0.0.1:4710; }
location = /sitemap.xml { proxy_pass http://127.0.0.1:4710; }
location = /sitemap.xml { proxy_pass http://127.0.0.1:4710; }
+ # The agent layer (openvibe-shared/agent-layer): the manifest an agent reads first, and the Markdown twins of
+ # the front page and /updates (a result page's twin answers on / with ?format=markdown, already proxied).
+ location = /.well-known/agent.json { limit_except GET { deny all; } proxy_pass http://127.0.0.1:4710; }
+ location ~ "^/(index|updates)\.md$" { limit_except GET { deny all; } proxy_pass http://127.0.0.1:4710; }
# The IndexNow key file (8-128 letters and digits); Node answers 404 unless it is INDEXNOW_KEY.
# The IndexNow key file (8-128 letters and digits); Node answers 404 unless it is INDEXNOW_KEY.
location ~ "^/[A-Za-z0-9]{8,128}\.txt$" { limit_except GET { deny all; } proxy_pass http://127.0.0.1:4710; }
location ~ "^/[A-Za-z0-9]{8,128}\.txt$" { limit_except GET { deny all; } proxy_pass http://127.0.0.1:4710; }
# This site's own pinned copy of the Frame's browser files (openvibe-shared/serve).
# This site's own pinned copy of the Frame's browser files (openvibe-shared/serve).
@@ -411,10 +411,11 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network
@@ -411,10 +411,11 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network
const origin = String(baseUrl).replace(/\/+$/, '');
const origin = String(baseUrl).replace(/\/+$/, '');
const router = express.Router();
const router = express.Router();
// Markdown twin requests and the agent manifest go to openvibe-shared/agent-layer, mounted after this router:
// Markdown twin requests and the agent manifest go to openvibe-shared/agent-layer, mounted after this router:
- // .md paths, ?format=markdown, an explicit text/markdown preference, and /.well-known/.
+ // .md paths, ?format=markdown, an explicit text/markdown preference, and the manifest's own path (any other
+ // /.well-known/ file stays this router's, exactly as security.txt does on Chat).
router.use((req, res, next) => {
router.use((req, res, next) => {
const p = String(req.path || '');
const p = String(req.path || '');
- if (p.endsWith('.md') || p.startsWith('/.well-known/') || (req.query && req.query.format === 'markdown')
+ if (p === '/.well-known/agent.json' || p.endsWith('.md') || (req.query && req.query.format === 'markdown')
|| req.accepts(['html', 'text/markdown']) === 'text/markdown') return next('router');
|| req.accepts(['html', 'text/markdown']) === 'text/markdown') return next('router');
return next();
return next();
});
});
@@ -424,6 +425,8 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network
@@ -424,6 +425,8 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network
res.setHeader('Content-Security-Policy', CSP);
res.setHeader('Content-Security-Policy', CSP);
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));
res.setHeader('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));
+ // This page has a Markdown twin (/updates.md), so its header names it, as the head tags do.
+ res.setHeader('Link', seo.linkHeaderTags(`${origin}/updates`, `${origin}/llms.txt`).header);
res.type('html').send(layout({ title: 'What shipped on OpenVibe.Search', q: '', owner: '', type: '', ownHeading: true, body: frame.updatesBody({ service: 'search', siteName: 'OpenVibe.Search' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`, canonical: `${origin}/updates` }));
res.type('html').send(layout({ title: 'What shipped on OpenVibe.Search', q: '', owner: '', type: '', ownHeading: true, body: frame.updatesBody({ service: 'search', siteName: 'OpenVibe.Search' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`, canonical: `${origin}/updates` }));
});
});
@@ -121,6 +121,34 @@ t('the route index is built from routes only: every entry gets a line of text, n
@@ -121,6 +121,34 @@ t('the route index is built from routes only: every entry gets a line of text, n
assert.ok(!/saved-searches|\/api\/v1\/owners|\/internal\/events|\/api\/v1\/documents/.test(text));
assert.ok(!/saved-searches|\/api\/v1\/owners|\/internal\/events|\/api\/v1\/documents/.test(text));
});
});
+t('the agent layer: manifest, twins, and the pages that name them', async () => {
+ const m = await request(svc.base, 'GET', '/.well-known/agent.json');
+ assert.strictEqual(m.status, 200);
+ assert.match(m.headers.get('content-type'), /application\/json/);
+ const d = JSON.parse(m.text);
+ assert.strictEqual(d.id, 'search');
+ assert.strictEqual(d.home, 'https://search.openvibe.network');
+ assert.strictEqual(d.api, 'https://search.openvibe.network/api/v1');
+ assert.deepStrictEqual(d.policy.auth, ['none', 'Network OIDC']);
+ // A twin of the front page and of the update log, from the pages' own render. The twin's canonical is the
+ // manifest's home (the site's public address), not the test server's own base URL.
+ for (const [p, canonical] of [['/index.md', 'https://search.openvibe.network/'], ['/updates.md', 'https://search.openvibe.network/updates']]) {
+ const tw = await request(svc.base, 'GET', p);
+ assert.strictEqual(tw.status, 200, `${p} answers`);
+ assert.match(tw.headers.get('content-type'), /text\/markdown/, `${p} is markdown`);
+ assert.ok(tw.text.includes(`canonical: "${canonical}"`), `${p} names its page`);
+ }
+ // The HTML page names its twin in the Link header and the head; a result page is noindex but still twinned.
+ for (const p of ['/', '/updates']) {
+ const page = await request(svc.base, 'GET', p, { headers: { accept: 'text/html' } });
+ assert.strictEqual(page.status, 200);
+ assert.match(String(page.headers.get('link')), /rel="alternate"; type="text\/markdown"/, `${p} names its twin`);
+ assert.match(page.text, /<link rel="alternate" type="text\/markdown"/, `${p} carries the head tag`);
+ }
+ const results = await request(svc.base, 'GET', '/?q=page&format=markdown');
+ assert.match(results.headers.get('content-type'), /text\/markdown/, 'a result twin answers for an agent');
+});
+
t('shutdown', async () => { await svc.stop(); });
t('shutdown', async () => { await svc.stop(); });
t.run();
t.run();