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)

OP OpenVibers committed 24 days ago · 7df7922daa · 4 files, +34 −6

Changes

ddiff --git a/server/app.js b/server/app.js
iindex 7e926cf..9705e5a 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -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) => {
ddiff --git a/server/pulse/service.js b/server/pulse/service.js
iindex 09b4f95..b3ee6e5 100644
---- a/server/pulse/service.js
++++ b/server/pulse/service.js
@@@ -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);
ddiff --git a/server/render/pulse.js b/server/render/pulse.js
iindex dcac3c8..8b6c442 100644
---- a/server/render/pulse.js
++++ b/server/render/pulse.js
@@@ -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' }])],
ddiff --git a/test/pulse.test.js b/test/pulse.test.js
iindex 0d08218..6a54da6 100644
---- a/test/pulse.test.js
++++ b/test/pulse.test.js
@@@ -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();
})();