openvibers/openvibe.meme Commits

Home: the citation and rise scenes read the first real entry (#99)

OP OpenVibers committed 6 days ago · 1fe57cccf7 · 5 files, +69 −9

Changes

ddiff --git a/server/agent-pages.js b/server/agent-pages.js
iindex 3ee93f0..33381ea 100644
---- a/server/agent-pages.js
++++ b/server/agent-pages.js
@@@ -22,11 +22,12 @@ function createAgentPages(ctx) {
async function home() {
const { week } = currentWeek(s.now());
const [trending, weekOut, c] = await Promise.all([entries.trending({ limit: 8 }), entries.week({ week }), counts()]);
++ const feature = await entries.showcase(trending[0]);
return page('/', {
index: true, title: null,
description: 'The open meme database, rebuilt with AI: every entry cites the moment it happened, follows its later uses, and people vote for meme of the week.',
jsonLd: homeJsonLd(config),
-- body: raw(views.home({ trending, week: weekOut, counts: c })),
++ body: raw(views.home({ trending, week: weekOut, counts: c, feature })),
});
}
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 1a19f8b..178404f 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -47,12 +47,13 @@ function createPageRoutes(ctx) {
r.get('/', async (req, res) => {
const { week } = currentWeek(s.now());
const [trending, weekOut, c] = await Promise.all([entries.trending({ limit: 8 }), entries.week({ week }), counts()]);
++ const feature = await entries.showcase(trending[0]);
return page(req, res, {
index: true, cache: PUBLIC_CACHE, agentLayer: true,
title: null,
description: 'The open meme database, rebuilt with AI: every entry cites the moment it happened, follows its later uses, and people vote for meme of the week.',
jsonLd: homeJsonLd(config),
-- body: raw(views.home({ trending, week: weekOut, counts: c })),
++ body: raw(views.home({ trending, week: weekOut, counts: c, feature })),
});
});
ddiff --git a/server/store.js b/server/store.js
iindex b82242a..ce0f47a 100644
---- a/server/store.js
++++ b/server/store.js
@@@ -658,6 +658,13 @@ function createStore(s, { search }) {
return { days: n, from: start.toISOString(), to: now.toISOString(), series: out };
}
++ /** The home's first real entry, with the citations and ten-day use series its scenes show. Null when none. */
++ async function showcase(entry) {
++ if (!entry) return null;
++ const [cites, uses] = await Promise.all([citations(entry.id), series(entry.id, { days: 10 })]);
++ return { entry, citations: cites, series: uses };
++ }
++
/** The id for a slug, appending -2, -3… when one is already taken. */
async function uniqueSlug(base, t) {
const root = String(base || 'meme').slice(0, 70).replace(/^-+|-+$/g, '') || 'meme';
@@@ -1309,7 +1316,7 @@ function createStore(s, { search }) {
wireEntry, wireCitation, wireOrigin, wireSpread, wireVariation, wireExample, wireRelated, wireRevision,
wireTemplate, wireCaption, wireReport, wireJudgement,
bySlug, byId, citations, liveCitations, originOf, spreadOf, variationsOf, examplesOf, relatedOf, revisionsOf, detail,
-- list, trending, week, series, uniqueSlug,
++ list, trending, week, series, showcase, uniqueSlug,
createCandidate, createEntry, addCitations, enrich, reindex, publish, hide, patch,
createProposal, addRevision, reviewRevision, pendingRevisions,
templates, templateById, addTemplate, createCaption, captionById, captionCountForTemplate,
ddiff --git a/server/views/index.js b/server/views/index.js
iindex 72056c8..011f593 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -5,7 +5,7 @@
* plain form or link. public/js/meme.js is a one-comment placeholder the main session replaces; the preview and
* the vote work without it.
*
-- * home({ trending, week }) trending, meme of the week, the generator teaser
++ * home({ trending, week, feature }) trending, meme of the week, the first entry's real origin and uses
* entry({ e, citations, signedIn, staff }) the origin with its citation link, the use timeline, examples
* week({ out, signedIn }) this week's candidates and the vote form
* make({ templates, values, preview, signedIn }) the caption generator form
@@@ -77,7 +77,30 @@ function treeMotif() {
</div>`;
}
--function home({ trending = [], week = null, counts = {} } = {}) {
++/** The origin scene: the first real entry's own cited moment — quote, source, its second and date, linked. */
++function homeCitation(feature) {
++ const cites = feature && Array.isArray(feature.citations) ? feature.citations : [];
++ const c = cites.find((x) => x.role === 'origin') || cites[0] || null;
++ if (!c) {
++ return `<div class="mm-empty"><p>No cited moment yet.</p><a class="mm-btn mm-btn-quiet" href="/developers">How entries are built</a></div>`;
++ }
++ const at = clock(c.offset_seconds);
++ const when = String(c.observed_at || '').slice(0, 10);
++ return `<blockquote class="mm-cite mm-cite-origin mm-demo-cite"><p class="mm-quote">${esc(c.quote_text)}</p><footer class="mm-cite-foot"><span class="mm-cite-source">${esc(SOURCE_LABEL[c.source_type] || c.source_type)}</span>${at ? ` · at ${esc(at)}` : ''}${when ? ` · ${esc(when)}` : ''} · <a class="mm-cite-link" href="${esc(c.url)}" rel="noopener">the moment</a></footer></blockquote>`;
++}
++
++/** The rise scene: the first real entry's own cited uses per day, scaled to its busiest day in the last ten. */
++function homeRise(feature) {
++ const rows = feature && feature.series && Array.isArray(feature.series.series) ? feature.series.series : [];
++ const max = Math.max(0, ...rows.map((r) => Number(r.uses) || 0));
++ if (!rows.length || max === 0) {
++ return `<div class="mm-empty"><p>No uses cited in the last 10 days.</p><a class="mm-btn mm-btn-quiet" href="/week">This week's ballot</a></div>`;
++ }
++ const bars = rows.map((r) => `<i style="--h:${Math.max(1, Math.round((Number(r.uses) || 0) / max * 14))}" title="${Number(r.uses) || 0} on ${esc(r.day)}"></i>`).join('');
++ return `<div class="mm-demo-rise"><span class="mm-status mm-status-${esc(feature.entry.status)}">${esc(STATUS_LABEL[feature.entry.status] || feature.entry.status)}</span><span class="mm-demo-bars">${bars}</span><small>uses per day, last ${rows.length} days · <a href="/m/${esc(feature.entry.slug)}">${esc(feature.entry.title)}</a></small></div>`;
++}
++
++function home({ trending = [], week = null, counts = {}, feature = null } = {}) {
const candidates = week ? week.candidates.slice(0, 3) : [];
// Real first (the owner, 10-31: empty sections made the rooms look unfinished). Trending and the week's teaser show only
// when they have entries; while neither does, one short "be the first" row sits below the scenes instead.
@@@ -107,13 +130,13 @@ function home({ trending = [], week = null, counts = {} } = {}) {
},
scenes: [
{ title: 'A stream moment becomes an entry', text: 'An entry starts where the joke did: the chat line or the second of the VOD, quoted word for word and linked. A source that disappears is marked, and an entry without its origin goes back into hiding.',
-- html: `<blockquote class="mm-cite mm-cite-origin mm-demo-cite"><p class="mm-quote">but yeah and Cosmo is broken now</p><footer class="mm-cite-foot"><span class="mm-cite-source">VOD</span> · at 25:41 · Oct 26 · <span class="mm-cite-link">the moment</span></footer></blockquote>`,
-- action: { href: '/developers', label: 'How entries are built' } },
++ html: homeCitation(feature),
++ action: feature ? { href: `/m/${feature.entry.slug}`, label: 'Open the entry' } : { href: '/developers', label: 'How entries are built' } },
{ title: 'Watch a joke rise', text: 'Every use is dated, so you can see a joke catch on, peak and become a classic. Rising first on the trending list.',
-- html: `<div class="mm-demo-rise"><span class="mm-status mm-status-rising">Rising</span><span class="mm-demo-bars">${[2, 3, 2, 4, 6, 5, 8, 11, 9, 14].map((h) => `<i style="--h:${h}"></i>`).join('')}</span><small>uses per day, last 10 days</small></div>`,
++ html: homeRise(feature),
action: { href: '/week', label: 'See this week\'s ballot' } },
{ title: 'Caption it', text: 'Pick a template, type up to four lines and see them over the picture. A caption is saved as text, so anyone can remix it.',
-- html: `<figure class="mm-caption-card mm-demo-caption"><img src="/img/template-classic.svg" alt=""><figcaption><span>when the robot is offline</span><span>and chat has to cope</span></figcaption></figure>`,
++ html: `<div><p class="mm-kicker">Example caption</p><figure class="mm-caption-card mm-demo-caption"><img src="/img/template-classic.svg" alt="An example caption on a template"><figcaption><span>when the robot is offline</span><span>and chat has to cope</span></figcaption></figure></div>`,
action: { href: '/make', label: 'Open the generator' } },
],
faq: { items: [
ddiff --git a/test/home-real-content.test.js b/test/home-real-content.test.js
nnew file mode 100644
iindex 0000000..5d85027
---- /dev/null
++++ b/test/home-real-content.test.js
@@@ -0,0 +1,28 @@
++'use strict';
++// The home's citation and use scenes show the first real trending entry: the quote, source, second and date of its
++// cited moment, and its own cited uses per day. Without one they say so; the caption stays a generator preview and is
++// labelled Example. No written-in quote, use series or cast.
++const assert = require('assert');
++const views = require('../server/views');
++
++const feature = {
++ entry: { slug: 'cosmo', title: 'Cozmo is offline', status: 'rising' },
++ citations: [{ role: 'origin', source_type: 'vod', offset_seconds: 1541, quote_text: 'a real quoted line', observed_at: '2026-11-01T10:00:00Z', url: 'https://openvibe.live/v/1' }],
++ series: { series: [{ day: '2026-10-29', uses: 0 }, { day: '2026-10-30', uses: 2 }, { day: '2026-10-31', uses: 1 }] },
++};
++const page = views.home({ feature });
++assert.ok(page.includes('a real quoted line'), 'the real quote');
++assert.ok(page.includes('VOD') && page.includes('at 25:41') && page.includes('2026-11-01'), 'its real source, second and date');
++assert.ok(page.includes('href="https://openvibe.live/v/1"'), 'the citation links its moment');
++assert.ok(page.includes('uses per day, last 3 days') && page.includes('Cozmo is offline'), 'the real use series');
++assert.ok(page.includes('href="/m/cosmo"'), 'the entry is linked');
++assert.ok(page.includes('Example caption'), 'the caption preview says Example');
++assert.ok(!/but yeah and Cosmo is broken now/.test(page), 'no written-in quote');
++
++const empty = views.home({});
++assert.ok(empty.includes('No cited moment yet'), 'no citation: says so');
++assert.ok(empty.includes('No uses cited in the last 10 days'), 'no uses: says so');
++assert.ok(!/but yeah and Cosmo is broken now|\[2, 3, 2, 4, 6, 5, 8, 11, 9, 14\]/.test(empty), 'no written-in quote or series');
++assert.ok(empty.includes('Example caption'), 'the caption preview still says Example');
++
++console.log('meme home: the citation and rise scenes read the first real entry, or say there is none');