openvibers/openvibe.meme Commits
Home: the citation and rise scenes read the first real entry (#99)
Changes
@@@ -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 })),});}@@@ -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 })),});});@@@ -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,@@@ -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: [nnew file mode 100644@@@ -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');
@@ -22,11 +22,12 @@ function createAgentPages(ctx) {
@@ -22,11 +22,12 @@ function createAgentPages(ctx) {
async function home() {
async function home() {
const { week } = currentWeek(s.now());
const { week } = currentWeek(s.now());
const [trending, weekOut, c] = await Promise.all([entries.trending({ limit: 8 }), entries.week({ week }), counts()]);
const [trending, weekOut, c] = await Promise.all([entries.trending({ limit: 8 }), entries.week({ week }), counts()]);
+ const feature = await entries.showcase(trending[0]);
return page('/', {
return page('/', {
index: true, title: null,
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.',
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),
jsonLd: homeJsonLd(config),
- body: raw(views.home({ trending, week: weekOut, counts: c })),
+ body: raw(views.home({ trending, week: weekOut, counts: c, feature })),
});
});
}
}
@@ -47,12 +47,13 @@ function createPageRoutes(ctx) {
@@ -47,12 +47,13 @@ function createPageRoutes(ctx) {
r.get('/', async (req, res) => {
r.get('/', async (req, res) => {
const { week } = currentWeek(s.now());
const { week } = currentWeek(s.now());
const [trending, weekOut, c] = await Promise.all([entries.trending({ limit: 8 }), entries.week({ week }), counts()]);
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, {
return page(req, res, {
index: true, cache: PUBLIC_CACHE, agentLayer: true,
index: true, cache: PUBLIC_CACHE, agentLayer: true,
title: null,
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.',
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),
jsonLd: homeJsonLd(config),
- body: raw(views.home({ trending, week: weekOut, counts: c })),
+ body: raw(views.home({ trending, week: weekOut, counts: c, feature })),
});
});
});
});
@@ -658,6 +658,13 @@ function createStore(s, { search }) {
@@ -658,6 +658,13 @@ function createStore(s, { search }) {
return { days: n, from: start.toISOString(), to: now.toISOString(), series: out };
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. */
/** The id for a slug, appending -2, -3… when one is already taken. */
async function uniqueSlug(base, t) {
async function uniqueSlug(base, t) {
const root = String(base || 'meme').slice(0, 70).replace(/^-+|-+$/g, '') || 'meme';
const root = String(base || 'meme').slice(0, 70).replace(/^-+|-+$/g, '') || 'meme';
@@ -1309,7 +1316,7 @@ function createStore(s, { search }) {
@@ -1309,7 +1316,7 @@ function createStore(s, { search }) {
wireEntry, wireCitation, wireOrigin, wireSpread, wireVariation, wireExample, wireRelated, wireRevision,
wireEntry, wireCitation, wireOrigin, wireSpread, wireVariation, wireExample, wireRelated, wireRevision,
wireTemplate, wireCaption, wireReport, wireJudgement,
wireTemplate, wireCaption, wireReport, wireJudgement,
bySlug, byId, citations, liveCitations, originOf, spreadOf, variationsOf, examplesOf, relatedOf, revisionsOf, detail,
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,
createCandidate, createEntry, addCitations, enrich, reindex, publish, hide, patch,
createProposal, addRevision, reviewRevision, pendingRevisions,
createProposal, addRevision, reviewRevision, pendingRevisions,
templates, templateById, addTemplate, createCaption, captionById, captionCountForTemplate,
templates, templateById, addTemplate, createCaption, captionById, captionCountForTemplate,
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
* plain form or link. public/js/meme.js is a one-comment placeholder the main session replaces; the preview and
* 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.
* 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
* 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
* week({ out, signedIn }) this week's candidates and the vote form
* make({ templates, values, preview, signedIn }) the caption generator form
* make({ templates, values, preview, signedIn }) the caption generator form
@@ -77,7 +77,30 @@ function treeMotif() {
@@ -77,7 +77,30 @@ function treeMotif() {
</div>`;
</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) : [];
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
// 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.
// 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 = {} } = {}) {
@@ -107,13 +130,13 @@ function home({ trending = [], week = null, counts = {} } = {}) {
},
},
scenes: [
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.',
{ 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.',
{ 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' } },
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.',
{ 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' } },
action: { href: '/make', label: 'Open the generator' } },
],
],
faq: { items: [
faq: { items: [
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -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');