openvibers/openvibe.community Commits
Pulse: a filter tab only for an origin that has items (AI and System had none and showed "Nothing here yet"), and an empty page is noindex (#110)
Changes
@@@ -457,7 +457,7 @@ async function createApp(opts = {}) {let out;try { out = await pulse.list({ origin, after: req.query.after }); }catch (err) { if (err instanceof v1.ApiError) return res.redirect(302, origin ? `/pulse?origin=${origin}` : '/pulse'); throw err; }-- html(res, pulsePage({ items: out.items, origin, after: req.query.after || null, nextCursor: out.next_cursor }));++ html(res, pulsePage({ items: out.items, origin, after: req.query.after || null, nextCursor: out.next_cursor, origins: await pulse.origins() }));}));app.get('/my', withUser, wrap(async (req, res) => {@@@ -31,6 +31,7 @@ const serviceNameOf = (sub) => String(sub || '').replace(/^(svc|app|mod):/, '');function createPulse({ db, network = null, config = {} } = {}) {const authors = createAuthors({ db, network });const base = (config.baseUrl || '').replace(/\/$/, '');++ let originsCache = { at: 0, list: null };/** Record one of Community's own items; never lets a Pulse problem fail the write that caused it. */async function recordLocal(type, id, { title, path, actor = null, origin = 'user', at = null }) {@@@ -131,6 +132,17 @@ function createPulse({ db, network = null, config = {} } = {}) {return { removed: await store.removeItem(db, service, type, id) };},++ /** The origins that have at least one listable item (the page offers a filter only for those), cached a minute. */++ async origins() {++ if (originsCache.list && Date.now() - originsCache.at < 60_000) return originsCache.list;++ const list = [];++ for (const origin of ORIGINS) {++ if ((await store.listItems(db, { origin, limit: 1 })).rows.length) list.push(origin);++ }++ originsCache = { at: Date.now(), list };++ return list;++ },++/** GET /api/v1/pulse ?origin=user|ai|system&after=<cursor>&limit= → { items, next_cursor } */async list(q = {}) {const origin = q.origin == null || q.origin === '' || q.origin === 'all' ? null : String(q.origin);@@@ -3,7 +3,8 @@/*** /pulse — what is happening across the OpenVibe network, newest first, from Pulse* (server/pulse). Filtering by origin and paging ("Older") are links, so it works without-- * JavaScript. AI items carry an AI label and no person; system items name no one either.++ * JavaScript. AI items carry an AI label and no person; system items name no one either. A filter is offered only for an++ * origin that has items (or the one asked for), and an empty page is not indexed: "Nothing here yet" is not a page.*/const ld = require('./jsonld');const { renderPage } = require('./layout');@@@ -38,8 +39,9 @@ function itemHtml(it) {</li>`;}--function pulsePage({ items, origin = '', after = null, nextCursor = null }) {-- const tabs = FILTERS.map(([id, label]) => `<a class="tab${id === origin ? ' active' : ''}" href="${esc(href(id))}"${id === origin ? ' aria-current="page"' : ''}>${label}</a>`).join('');++function pulsePage({ items, origin = '', after = null, nextCursor = null, origins = null }) {++ const offered = FILTERS.filter(([id]) => !id || id === origin || !origins || origins.includes(id));++ const tabs = offered.map(([id, label]) => `<a class="tab${id === origin ? ' active' : ''}" href="${esc(href(id))}"${id === origin ? ' aria-current="page"' : ''}>${label}</a>`).join('');const body = `<header class="page-head"><nav class="crumbs" aria-label="Breadcrumb"><a href="/">Home</a> › <span aria-current="page">Pulse</span></nav>@@@ -56,8 +58,8 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null }) {title: `Pulse${origin ? ` — ${FILTERS.find(([id]) => id === origin)[1]}` : ''}`,description: 'The pulse of the OpenVibe network: new public pastes, threads, replies and what the other OpenVibe sites share, with AI-made items labelled as AI.',canonicalPath: href(origin, after),-- // The first page of each filter is worth indexing; deep cursors are not.-- robots: after ? 'noindex,follow' : 'index,follow',++ // The first page of each filter is worth indexing; deep cursors and empty pages are not.++ robots: after || !items.length ? 'noindex,follow' : 'index,follow',active: 'pulse',feeds: [{ title: 'OpenVibe.Community — latest threads', href: '/s/feed.xml' }],jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pulse', url: '/pulse' }])],@@@ -174,6 +174,20 @@ const { boot, check, done } = require('./helpers/app');assert.strictEqual(bad.status, 302);});++ await check('/pulse offers a filter only for an origin with items, and an empty page is noindex', async () => {++ const { pulsePage } = require('../server/render/pulse');++ const item = { id: 1, source: { service: 'live', type: 'clip', id: 'x' }, title: 'A clip', url: 'https://openvibe.live/c/1', origin: 'user', actor: null, occurred_at: '2026-10-20T00:00:00.000Z' };++ const people = pulsePage({ items: [item], origins: ['user'] });++ assert.ok(people.includes('href="/pulse?origin=user"') && !people.includes('href="/pulse?origin=ai"') && !people.includes('href="/pulse?origin=system"'), 'no AI or System tab when neither has items');++ assert.ok(people.includes('<meta name="robots" content="index,follow">'));++ const empty = pulsePage({ items: [], origin: 'system', origins: ['user'] });++ assert.ok(empty.includes('aria-current="page">System</a>'), 'the filter asked for keeps its tab');++ assert.ok(empty.includes('Nothing here yet.') && empty.includes('<meta name="robots" content="noindex,follow">'));++ assert.ok(pulsePage({ items: [item] }).includes('href="/pulse?origin=system"'), 'without the origins list every filter shows');++ const r = await t.get('/pulse');++ assert.ok(r.text.includes('href="/pulse?origin=system"'), 'system items exist here, so the tab shows');++ });++await t.close();done();})();
@@ -457,7 +457,7 @@ async function createApp(opts = {}) {
@@ -457,7 +457,7 @@ async function createApp(opts = {}) {
let out;
let out;
try { out = await pulse.list({ origin, after: req.query.after }); }
try { out = await pulse.list({ origin, after: req.query.after }); }
catch (err) { if (err instanceof v1.ApiError) return res.redirect(302, origin ? `/pulse?origin=${origin}` : '/pulse'); throw err; }
catch (err) { if (err instanceof v1.ApiError) return res.redirect(302, origin ? `/pulse?origin=${origin}` : '/pulse'); throw err; }
- html(res, pulsePage({ items: out.items, origin, after: req.query.after || null, nextCursor: out.next_cursor }));
+ html(res, pulsePage({ items: out.items, origin, after: req.query.after || null, nextCursor: out.next_cursor, origins: await pulse.origins() }));
}));
}));
app.get('/my', withUser, wrap(async (req, res) => {
app.get('/my', withUser, wrap(async (req, res) => {
@@ -31,6 +31,7 @@ const serviceNameOf = (sub) => String(sub || '').replace(/^(svc|app|mod):/, '');
@@ -31,6 +31,7 @@ const serviceNameOf = (sub) => String(sub || '').replace(/^(svc|app|mod):/, '');
function createPulse({ db, network = null, config = {} } = {}) {
function createPulse({ db, network = null, config = {} } = {}) {
const authors = createAuthors({ db, network });
const authors = createAuthors({ db, network });
const base = (config.baseUrl || '').replace(/\/$/, '');
const base = (config.baseUrl || '').replace(/\/$/, '');
+ let originsCache = { at: 0, list: null };
/** Record one of Community's own items; never lets a Pulse problem fail the write that caused it. */
/** Record one of Community's own items; never lets a Pulse problem fail the write that caused it. */
async function recordLocal(type, id, { title, path, actor = null, origin = 'user', at = null }) {
async function recordLocal(type, id, { title, path, actor = null, origin = 'user', at = null }) {
@@ -131,6 +132,17 @@ function createPulse({ db, network = null, config = {} } = {}) {
@@ -131,6 +132,17 @@ function createPulse({ db, network = null, config = {} } = {}) {
return { removed: await store.removeItem(db, service, type, id) };
return { removed: await store.removeItem(db, service, type, id) };
},
},
+ /** The origins that have at least one listable item (the page offers a filter only for those), cached a minute. */
+ async origins() {
+ if (originsCache.list && Date.now() - originsCache.at < 60_000) return originsCache.list;
+ const list = [];
+ for (const origin of ORIGINS) {
+ if ((await store.listItems(db, { origin, limit: 1 })).rows.length) list.push(origin);
+ }
+ originsCache = { at: Date.now(), list };
+ return list;
+ },
+
/** GET /api/v1/pulse ?origin=user|ai|system&after=<cursor>&limit= → { items, next_cursor } */
/** GET /api/v1/pulse ?origin=user|ai|system&after=<cursor>&limit= → { items, next_cursor } */
async list(q = {}) {
async list(q = {}) {
const origin = q.origin == null || q.origin === '' || q.origin === 'all' ? null : String(q.origin);
const origin = q.origin == null || q.origin === '' || q.origin === 'all' ? null : String(q.origin);
@@ -3,7 +3,8 @@
@@ -3,7 +3,8 @@
/**
/**
* /pulse — what is happening across the OpenVibe network, newest first, from Pulse
* /pulse — what is happening across the OpenVibe network, newest first, from Pulse
* (server/pulse). Filtering by origin and paging ("Older") are links, so it works without
* (server/pulse). Filtering by origin and paging ("Older") are links, so it works without
- * JavaScript. AI items carry an AI label and no person; system items name no one either.
+ * JavaScript. AI items carry an AI label and no person; system items name no one either. A filter is offered only for an
+ * origin that has items (or the one asked for), and an empty page is not indexed: "Nothing here yet" is not a page.
*/
*/
const ld = require('./jsonld');
const ld = require('./jsonld');
const { renderPage } = require('./layout');
const { renderPage } = require('./layout');
@@ -38,8 +39,9 @@ function itemHtml(it) {
@@ -38,8 +39,9 @@ function itemHtml(it) {
</li>`;
</li>`;
}
}
-function pulsePage({ items, origin = '', after = null, nextCursor = null }) {
- const tabs = FILTERS.map(([id, label]) => `<a class="tab${id === origin ? ' active' : ''}" href="${esc(href(id))}"${id === origin ? ' aria-current="page"' : ''}>${label}</a>`).join('');
+function pulsePage({ items, origin = '', after = null, nextCursor = null, origins = null }) {
+ const offered = FILTERS.filter(([id]) => !id || id === origin || !origins || origins.includes(id));
+ const tabs = offered.map(([id, label]) => `<a class="tab${id === origin ? ' active' : ''}" href="${esc(href(id))}"${id === origin ? ' aria-current="page"' : ''}>${label}</a>`).join('');
const body = `
const body = `
<header class="page-head">
<header class="page-head">
<nav class="crumbs" aria-label="Breadcrumb"><a href="/">Home</a> › <span aria-current="page">Pulse</span></nav>
<nav class="crumbs" aria-label="Breadcrumb"><a href="/">Home</a> › <span aria-current="page">Pulse</span></nav>
@@ -56,8 +58,8 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null }) {
@@ -56,8 +58,8 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null }) {
title: `Pulse${origin ? ` — ${FILTERS.find(([id]) => id === origin)[1]}` : ''}`,
title: `Pulse${origin ? ` — ${FILTERS.find(([id]) => id === origin)[1]}` : ''}`,
description: 'The pulse of the OpenVibe network: new public pastes, threads, replies and what the other OpenVibe sites share, with AI-made items labelled as AI.',
description: 'The pulse of the OpenVibe network: new public pastes, threads, replies and what the other OpenVibe sites share, with AI-made items labelled as AI.',
canonicalPath: href(origin, after),
canonicalPath: href(origin, after),
- // The first page of each filter is worth indexing; deep cursors are not.
- robots: after ? 'noindex,follow' : 'index,follow',
+ // The first page of each filter is worth indexing; deep cursors and empty pages are not.
+ robots: after || !items.length ? 'noindex,follow' : 'index,follow',
active: 'pulse',
active: 'pulse',
feeds: [{ title: 'OpenVibe.Community — latest threads', href: '/s/feed.xml' }],
feeds: [{ title: 'OpenVibe.Community — latest threads', href: '/s/feed.xml' }],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pulse', url: '/pulse' }])],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pulse', url: '/pulse' }])],
@@ -174,6 +174,20 @@ const { boot, check, done } = require('./helpers/app');
@@ -174,6 +174,20 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(bad.status, 302);
assert.strictEqual(bad.status, 302);
});
});
+ await check('/pulse offers a filter only for an origin with items, and an empty page is noindex', async () => {
+ const { pulsePage } = require('../server/render/pulse');
+ const item = { id: 1, source: { service: 'live', type: 'clip', id: 'x' }, title: 'A clip', url: 'https://openvibe.live/c/1', origin: 'user', actor: null, occurred_at: '2026-10-20T00:00:00.000Z' };
+ const people = pulsePage({ items: [item], origins: ['user'] });
+ assert.ok(people.includes('href="/pulse?origin=user"') && !people.includes('href="/pulse?origin=ai"') && !people.includes('href="/pulse?origin=system"'), 'no AI or System tab when neither has items');
+ assert.ok(people.includes('<meta name="robots" content="index,follow">'));
+ const empty = pulsePage({ items: [], origin: 'system', origins: ['user'] });
+ assert.ok(empty.includes('aria-current="page">System</a>'), 'the filter asked for keeps its tab');
+ assert.ok(empty.includes('Nothing here yet.') && empty.includes('<meta name="robots" content="noindex,follow">'));
+ assert.ok(pulsePage({ items: [item] }).includes('href="/pulse?origin=system"'), 'without the origins list every filter shows');
+ const r = await t.get('/pulse');
+ assert.ok(r.text.includes('href="/pulse?origin=system"'), 'system items exist here, so the tab shows');
+ });
+
await t.close();
await t.close();
done();
done();
})();
})();