openvibers/openvibe.mediahub Commits
/llms-full.txt on openvibe.pics and openvibe.video (SEO-33) (#275)
Changes
@@@ -623,7 +623,8 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res.send(seo.sitemapXml(['/', '/updates', ...legal.PATHS, ...POLICY_PATHS].map((p) => ({ loc: `${base}${p}` })))));// RFC 9116, canonical to this host (openvibe.pics).r.get('/.well-known/security.txt', securityTxt.handler(base));-- r.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({++ // The site as llms.txt describes it, shared by /llms.txt and /llms-full.txt (SEO-33).++ const llmsSite = () => ({name: SITE_NAME,summary: 'Picture hosting on the OpenVibe network: upload a picture, get a link to share or embed, albums, any size or format.',details: 'Pictures are unlisted (reachable only by their link). Each one is re-encoded with its metadata removed, and can be had resized and converted at /i/<slug>/w/<320|640|1280|1920|2560>.<webp|avif|jpg|png>. Alt text: the uploader writes it or asks OpenVibe.AI for a draft (POST /api/v1/images/<slug>/describe). API: POST /api/v1/images with the picture as the body (an OpenVibe sign-in token as a Bearer).',@@@ -635,7 +636,19 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res{ title: 'Pictures and AI', url: `${base}/ai-training` },] },],-- })));++ });++ r.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt(llmsSite())));++ // The same, in full (SEO-33): what the site is, then every page llms.txt lists with what it holds.++ r.get('/llms-full.txt', (_req, res) => {++ const d = llmsSite();++ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsFull({++ site: d.name, summary: d.summary, base: `${base}/`,++ sections: [++ { title: 'About', pages: [{ title: d.name, url: `${base}/`, text: [d.summary, Array.isArray(d.details) ? d.details.join('\n') : d.details].filter(Boolean).join('\n\n') }] },++ ...(d.sections || []).map((x) => ({ title: x.title, pages: (x.links || []).map((l) => ({ title: l.title, url: l.url, text: l.note || '' })) })),++ ],++ }));++ });r.get('/updates', (req, res) => page(req, res, {index: true, title: 'What shipped on OpenVibe.Pics', description: 'Every change deployed to OpenVibe.Pics, newest first.',body: raw(frame.updatesBody({ service: 'media-hub', siteName: SITE_NAME }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`),@@@ -1852,15 +1852,16 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down// RFC 9116, canonical to this host (openvibe.video), not the drive's.r.get('/.well-known/security.txt', securityTxt.handler(base));-- r.get('/llms.txt', async (req, res) => {++ // The site as llms.txt describes it, with the latest recordings and clips (shared by /llms-full.txt, SEO-33).++ async function llmsSite() {const [vods, clips] = await Promise.all([live.feed({ type: 'vods', limit: SITEMAP_LIMIT }),live.feed({ type: 'clips', limit: SITEMAP_LIMIT }),]);const vodItems = (vods && vods.items) || [];const clipItems = (clips && clips.items) || [];-- const links = (items, kind) => items.map((it) => ({ title: it.title || (kind === 'vod' ? 'Recording' : 'Clip'), url: `${base}/${kind === 'vod' ? 'v' : 'c'}/${it.id}` }));-- return res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({++ const links = (items, kind) => items.map((it) => ({ title: it.title || (kind === 'vod' ? 'Recording' : 'Clip'), url: `${base}/${kind === 'vod' ? 'v' : 'c'}/${it.id}`, note: String(it.description || it.ai_summary || it.summary || '').replace(/\s+/g, ' ').trim().slice(0, 600) }));++ return {name: SITE_NAME,summary: 'Videos people upload or record in the browser, and stream recordings and clips from OpenVibe.Live, each with a watch page.',details: 'AI captions, chapters and a summary are made for uploads (OpenVibe.AI, on OpenVibe\'s own servers) and rendered on the watch pages, as are the AI chapters and captions of stream recordings. The AI cut makes an edited copy of an upload: the pauses cut out, or a highlights reel. Uploading needs a signed-in OpenVibe account; /upload and /record.',@@@ -1869,6 +1870,24 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down{ title: 'Latest recordings', links: links(vodItems, 'vod') },{ title: 'Latest clips', links: links(clipItems, 'clip') },],++ };++ }++ r.get('/llms.txt', async (req, res) => {++ const d = await llmsSite();++ // llms.txt stays a list of links: the notes are for /llms-full.txt.++ const brief = { ...d, sections: d.sections.map((x) => ({ ...x, links: x.links.map(({ note, ...l }) => l) })) };++ return res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt(brief));++ });++ // The same, in full (SEO-33): what the site is, then every recording and clip with what it holds.++ r.get('/llms-full.txt', async (req, res) => {++ const d = await llmsSite();++ return res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsFull({++ site: d.name, summary: d.summary, base: `${base}/`,++ sections: [++ { title: 'About', pages: [{ title: d.name, url: `${base}/`, text: [d.summary, d.details].filter(Boolean).join('\n\n') }] },++ ...d.sections.map((x) => ({ title: x.title, pages: x.links.map((l) => ({ title: l.title, url: l.url, text: l.note || '' })) })),++ ],++ maxBytes: 200000,}));});@@@ -103,6 +103,11 @@ const PAGES = ['/exif', '/content-policy', '/ai-training'];for (const path of PAGES) assert.ok(map.includes(`<loc>https://openvibe.pics${path}</loc>`), `the sitemap lists ${path}`);const llms = (await t.get('/llms.txt', { headers: PICS })).text;for (const path of PAGES) assert.ok(llms.includes(`https://openvibe.pics${path}`), `llms.txt names ${path}`);++ // SEO-33: llms-full.txt answers on the pics host, from the same descriptor.++ const full = await t.get('/llms-full.txt', { headers: PICS });++ assert.strictEqual(full.status, 200);++ assert.match(full.text, /## About\n\n### /);++ for (const path of PAGES) assert.ok(full.text.includes(`URL: https://openvibe.pics${path}`), `llms-full.txt names ${path}`);const robots = (await t.get('/robots.txt', { headers: PICS })).text;for (const path of PAGES) assert.ok(!robots.includes(`Disallow: ${path}`), `robots.txt does not block ${path}`);});@@@ -606,6 +606,12 @@ function countOccurrences(haystack, needle) {const llms = await getVideo('/llms.txt');assert.strictEqual(llms.status, 200);assert.ok(llms.text.includes('# OpenVibe.Video'));++ // SEO-33: the same, in full.++ const full = await getVideo('/llms-full.txt');++ assert.strictEqual(full.status, 200);++ assert.ok(full.text.startsWith('# OpenVibe.Video'));++ assert.match(full.text, /## About\n\n### OpenVibe\.Video\n/);++ assert.ok(full.text.length > 500, `${full.text.length} characters`);});await check('legal pages on the video host name openvibe.video', async () => {
@@ -623,7 +623,8 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res
@@ -623,7 +623,8 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res
.send(seo.sitemapXml(['/', '/updates', ...legal.PATHS, ...POLICY_PATHS].map((p) => ({ loc: `${base}${p}` })))));
.send(seo.sitemapXml(['/', '/updates', ...legal.PATHS, ...POLICY_PATHS].map((p) => ({ loc: `${base}${p}` })))));
// RFC 9116, canonical to this host (openvibe.pics).
// RFC 9116, canonical to this host (openvibe.pics).
r.get('/.well-known/security.txt', securityTxt.handler(base));
r.get('/.well-known/security.txt', securityTxt.handler(base));
- r.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({
+ // The site as llms.txt describes it, shared by /llms.txt and /llms-full.txt (SEO-33).
+ const llmsSite = () => ({
name: SITE_NAME,
name: SITE_NAME,
summary: 'Picture hosting on the OpenVibe network: upload a picture, get a link to share or embed, albums, any size or format.',
summary: 'Picture hosting on the OpenVibe network: upload a picture, get a link to share or embed, albums, any size or format.',
details: 'Pictures are unlisted (reachable only by their link). Each one is re-encoded with its metadata removed, and can be had resized and converted at /i/<slug>/w/<320|640|1280|1920|2560>.<webp|avif|jpg|png>. Alt text: the uploader writes it or asks OpenVibe.AI for a draft (POST /api/v1/images/<slug>/describe). API: POST /api/v1/images with the picture as the body (an OpenVibe sign-in token as a Bearer).',
details: 'Pictures are unlisted (reachable only by their link). Each one is re-encoded with its metadata removed, and can be had resized and converted at /i/<slug>/w/<320|640|1280|1920|2560>.<webp|avif|jpg|png>. Alt text: the uploader writes it or asks OpenVibe.AI for a draft (POST /api/v1/images/<slug>/describe). API: POST /api/v1/images with the picture as the body (an OpenVibe sign-in token as a Bearer).',
@@ -635,7 +636,19 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res
@@ -635,7 +636,19 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res
{ title: 'Pictures and AI', url: `${base}/ai-training` },
{ title: 'Pictures and AI', url: `${base}/ai-training` },
] },
] },
],
],
- })));
+ });
+ r.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt(llmsSite())));
+ // The same, in full (SEO-33): what the site is, then every page llms.txt lists with what it holds.
+ r.get('/llms-full.txt', (_req, res) => {
+ const d = llmsSite();
+ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsFull({
+ site: d.name, summary: d.summary, base: `${base}/`,
+ sections: [
+ { title: 'About', pages: [{ title: d.name, url: `${base}/`, text: [d.summary, Array.isArray(d.details) ? d.details.join('\n') : d.details].filter(Boolean).join('\n\n') }] },
+ ...(d.sections || []).map((x) => ({ title: x.title, pages: (x.links || []).map((l) => ({ title: l.title, url: l.url, text: l.note || '' })) })),
+ ],
+ }));
+ });
r.get('/updates', (req, res) => page(req, res, {
r.get('/updates', (req, res) => page(req, res, {
index: true, title: 'What shipped on OpenVibe.Pics', description: 'Every change deployed to OpenVibe.Pics, newest first.',
index: true, title: 'What shipped on OpenVibe.Pics', description: 'Every change deployed to OpenVibe.Pics, newest first.',
body: raw(frame.updatesBody({ service: 'media-hub', siteName: SITE_NAME }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`),
body: raw(frame.updatesBody({ service: 'media-hub', siteName: SITE_NAME }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`),
@@ -1852,15 +1852,16 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down
@@ -1852,15 +1852,16 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down
// RFC 9116, canonical to this host (openvibe.video), not the drive's.
// RFC 9116, canonical to this host (openvibe.video), not the drive's.
r.get('/.well-known/security.txt', securityTxt.handler(base));
r.get('/.well-known/security.txt', securityTxt.handler(base));
- r.get('/llms.txt', async (req, res) => {
+ // The site as llms.txt describes it, with the latest recordings and clips (shared by /llms-full.txt, SEO-33).
+ async function llmsSite() {
const [vods, clips] = await Promise.all([
const [vods, clips] = await Promise.all([
live.feed({ type: 'vods', limit: SITEMAP_LIMIT }),
live.feed({ type: 'vods', limit: SITEMAP_LIMIT }),
live.feed({ type: 'clips', limit: SITEMAP_LIMIT }),
live.feed({ type: 'clips', limit: SITEMAP_LIMIT }),
]);
]);
const vodItems = (vods && vods.items) || [];
const vodItems = (vods && vods.items) || [];
const clipItems = (clips && clips.items) || [];
const clipItems = (clips && clips.items) || [];
- const links = (items, kind) => items.map((it) => ({ title: it.title || (kind === 'vod' ? 'Recording' : 'Clip'), url: `${base}/${kind === 'vod' ? 'v' : 'c'}/${it.id}` }));
- return res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({
+ const links = (items, kind) => items.map((it) => ({ title: it.title || (kind === 'vod' ? 'Recording' : 'Clip'), url: `${base}/${kind === 'vod' ? 'v' : 'c'}/${it.id}`, note: String(it.description || it.ai_summary || it.summary || '').replace(/\s+/g, ' ').trim().slice(0, 600) }));
+ return {
name: SITE_NAME,
name: SITE_NAME,
summary: 'Videos people upload or record in the browser, and stream recordings and clips from OpenVibe.Live, each with a watch page.',
summary: 'Videos people upload or record in the browser, and stream recordings and clips from OpenVibe.Live, each with a watch page.',
details: 'AI captions, chapters and a summary are made for uploads (OpenVibe.AI, on OpenVibe\'s own servers) and rendered on the watch pages, as are the AI chapters and captions of stream recordings. The AI cut makes an edited copy of an upload: the pauses cut out, or a highlights reel. Uploading needs a signed-in OpenVibe account; /upload and /record.',
details: 'AI captions, chapters and a summary are made for uploads (OpenVibe.AI, on OpenVibe\'s own servers) and rendered on the watch pages, as are the AI chapters and captions of stream recordings. The AI cut makes an edited copy of an upload: the pauses cut out, or a highlights reel. Uploading needs a signed-in OpenVibe account; /upload and /record.',
@@ -1869,6 +1870,24 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down
@@ -1869,6 +1870,24 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down
{ title: 'Latest recordings', links: links(vodItems, 'vod') },
{ title: 'Latest recordings', links: links(vodItems, 'vod') },
{ title: 'Latest clips', links: links(clipItems, 'clip') },
{ title: 'Latest clips', links: links(clipItems, 'clip') },
],
],
+ };
+ }
+ r.get('/llms.txt', async (req, res) => {
+ const d = await llmsSite();
+ // llms.txt stays a list of links: the notes are for /llms-full.txt.
+ const brief = { ...d, sections: d.sections.map((x) => ({ ...x, links: x.links.map(({ note, ...l }) => l) })) };
+ return res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt(brief));
+ });
+ // The same, in full (SEO-33): what the site is, then every recording and clip with what it holds.
+ r.get('/llms-full.txt', async (req, res) => {
+ const d = await llmsSite();
+ return res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsFull({
+ site: d.name, summary: d.summary, base: `${base}/`,
+ sections: [
+ { title: 'About', pages: [{ title: d.name, url: `${base}/`, text: [d.summary, d.details].filter(Boolean).join('\n\n') }] },
+ ...d.sections.map((x) => ({ title: x.title, pages: x.links.map((l) => ({ title: l.title, url: l.url, text: l.note || '' })) })),
+ ],
+ maxBytes: 200000,
}));
}));
});
});
@@ -103,6 +103,11 @@ const PAGES = ['/exif', '/content-policy', '/ai-training'];
@@ -103,6 +103,11 @@ const PAGES = ['/exif', '/content-policy', '/ai-training'];
for (const path of PAGES) assert.ok(map.includes(`<loc>https://openvibe.pics${path}</loc>`), `the sitemap lists ${path}`);
for (const path of PAGES) assert.ok(map.includes(`<loc>https://openvibe.pics${path}</loc>`), `the sitemap lists ${path}`);
const llms = (await t.get('/llms.txt', { headers: PICS })).text;
const llms = (await t.get('/llms.txt', { headers: PICS })).text;
for (const path of PAGES) assert.ok(llms.includes(`https://openvibe.pics${path}`), `llms.txt names ${path}`);
for (const path of PAGES) assert.ok(llms.includes(`https://openvibe.pics${path}`), `llms.txt names ${path}`);
+ // SEO-33: llms-full.txt answers on the pics host, from the same descriptor.
+ const full = await t.get('/llms-full.txt', { headers: PICS });
+ assert.strictEqual(full.status, 200);
+ assert.match(full.text, /## About\n\n### /);
+ for (const path of PAGES) assert.ok(full.text.includes(`URL: https://openvibe.pics${path}`), `llms-full.txt names ${path}`);
const robots = (await t.get('/robots.txt', { headers: PICS })).text;
const robots = (await t.get('/robots.txt', { headers: PICS })).text;
for (const path of PAGES) assert.ok(!robots.includes(`Disallow: ${path}`), `robots.txt does not block ${path}`);
for (const path of PAGES) assert.ok(!robots.includes(`Disallow: ${path}`), `robots.txt does not block ${path}`);
});
});
@@ -606,6 +606,12 @@ function countOccurrences(haystack, needle) {
@@ -606,6 +606,12 @@ function countOccurrences(haystack, needle) {
const llms = await getVideo('/llms.txt');
const llms = await getVideo('/llms.txt');
assert.strictEqual(llms.status, 200);
assert.strictEqual(llms.status, 200);
assert.ok(llms.text.includes('# OpenVibe.Video'));
assert.ok(llms.text.includes('# OpenVibe.Video'));
+ // SEO-33: the same, in full.
+ const full = await getVideo('/llms-full.txt');
+ assert.strictEqual(full.status, 200);
+ assert.ok(full.text.startsWith('# OpenVibe.Video'));
+ assert.match(full.text, /## About\n\n### OpenVibe\.Video\n/);
+ assert.ok(full.text.length > 500, `${full.text.length} characters`);
});
});
await check('legal pages on the video host name openvibe.video', async () => {
await check('legal pages on the video host name openvibe.video', async () => {