openvibers/openvibe.meme Commits
Describe Meme entry pages for search and share cards (#55)
Changes
@@@ -56,6 +56,19 @@ function createPageRoutes(ctx) {});// ── One entry ────────────────────────────────────────────++ /** A search result and share-card line: the entry's own summary or first use, cut at a word to 155 characters. */++ function metaDescription(e, citations = []) {++ const origin = citations.find((c) => c.role === 'origin') || citations[0] || null;++ const own = [e.summary, origin && origin.quote_text]++ .map((v) => (typeof v === 'string' ? v.replace(/\s+/g, ' ').trim() : ''))++ .find((v) => v.length >= 40);++ if (!own) return `${e.title}: where the joke started, how it spread, and every use on OpenVibe.`;++ if (own.length <= 155) return own;++ const cut = own.slice(0, 154);++ const at = cut.lastIndexOf(' ');++ return `${(at > 40 ? cut.slice(0, at) : cut).replace(/[\s,;:.!?-]+$/, '')}…`;++ }++async function entryPage(req, res, { problem = null } = {}) {const slug = String(req.params.slug || '');const row = await entries.bySlug(slug);@@@ -63,17 +76,18 @@ function createPageRoutes(ctx) {if (!row || (row.visibility === 'hidden' && !staff)) return notFound(req, res);const citations = await entries.citations(row.id);const e = entries.wireEntry(row);++ const description = metaDescription(e, citations);const jsonLd = seo.jsonLd.webPage({name: e.title,url: `${base()}/m/${e.slug}`,-- description: (e.summary || `${e.title} on OpenVibe.Meme`).slice(0, 160),++ description,siteUrl: base(),});return page(req, res, {index: e.visibility === 'public',cache: e.visibility === 'public' ? PUBLIC_CACHE : null,title: e.title,-- description: (e.summary || `${e.title} on OpenVibe.Meme`).slice(0, 160),++ description,jsonLd: [jsonLd],body: raw(views.entry({ e, citations, signedIn: Boolean(signedIn(req)), staff, reported: req.query.reported === '1', problem })),});@@@ -26,6 +26,11 @@ function jsonLd(html) {{ role: 'use', source_service: 'live', source_type: 'vod', source_id: '9002', vod_id: 9002, offset_seconds: 120, quote_text: 'the robot is offline for good', url: `${t.upstreams.url}/v/9002?t=120`, observed_at: now },],});++ const describedEntry = await t.ctx.entries.createCandidate({++ slug: 'cat-typing', title: 'Cat typing', kind: 'meme', status: 'rising',++ summary: 'A cat at a keyboard, typed as though the cat is answering a work email one slow paw at a time.',++ first_seen_at: now, last_seen_at: now, visibility: 'public', citations: [],++ });const hiddenEntry = await t.ctx.entries.createCandidate({slug: 'not-yet', title: 'Not yet', kind: 'joke', summary: 'Hidden.', status: 'rising',first_seen_at: now, last_seen_at: now, visibility: 'hidden',@@@ -65,6 +70,13 @@ function jsonLd(html) {assert.ok(r.text.includes('/v/9002?t=120'));});++ await check('one entry page describes itself for search and share cards', async () => {++ const fallback = await t.get('/m/robot-is-offline');++ assert.ok(fallback.text.includes('<meta name="description" content="Robot is offline: where the joke started, how it spread, and every use on OpenVibe.">'), 'the fallback line');++ const own = await t.get('/m/cat-typing');++ assert.ok(own.text.includes('<meta name="description" content="A cat at a keyboard, typed as though the cat is answering a work email one slow paw at a time.">'), 'the entry\'s own summary');++ });++await check('a hidden entry is 404 to the public and visible to staff', async () => {assert.strictEqual((await t.get('/m/not-yet')).status, 404);assert.strictEqual((await t.get('/m/not-yet', { as: alice })).status, 404);
@@ -56,6 +56,19 @@ function createPageRoutes(ctx) {
@@ -56,6 +56,19 @@ function createPageRoutes(ctx) {
});
});
// ── One entry ────────────────────────────────────────────
// ── One entry ────────────────────────────────────────────
+ /** A search result and share-card line: the entry's own summary or first use, cut at a word to 155 characters. */
+ function metaDescription(e, citations = []) {
+ const origin = citations.find((c) => c.role === 'origin') || citations[0] || null;
+ const own = [e.summary, origin && origin.quote_text]
+ .map((v) => (typeof v === 'string' ? v.replace(/\s+/g, ' ').trim() : ''))
+ .find((v) => v.length >= 40);
+ if (!own) return `${e.title}: where the joke started, how it spread, and every use on OpenVibe.`;
+ if (own.length <= 155) return own;
+ const cut = own.slice(0, 154);
+ const at = cut.lastIndexOf(' ');
+ return `${(at > 40 ? cut.slice(0, at) : cut).replace(/[\s,;:.!?-]+$/, '')}…`;
+ }
+
async function entryPage(req, res, { problem = null } = {}) {
async function entryPage(req, res, { problem = null } = {}) {
const slug = String(req.params.slug || '');
const slug = String(req.params.slug || '');
const row = await entries.bySlug(slug);
const row = await entries.bySlug(slug);
@@ -63,17 +76,18 @@ function createPageRoutes(ctx) {
@@ -63,17 +76,18 @@ function createPageRoutes(ctx) {
if (!row || (row.visibility === 'hidden' && !staff)) return notFound(req, res);
if (!row || (row.visibility === 'hidden' && !staff)) return notFound(req, res);
const citations = await entries.citations(row.id);
const citations = await entries.citations(row.id);
const e = entries.wireEntry(row);
const e = entries.wireEntry(row);
+ const description = metaDescription(e, citations);
const jsonLd = seo.jsonLd.webPage({
const jsonLd = seo.jsonLd.webPage({
name: e.title,
name: e.title,
url: `${base()}/m/${e.slug}`,
url: `${base()}/m/${e.slug}`,
- description: (e.summary || `${e.title} on OpenVibe.Meme`).slice(0, 160),
+ description,
siteUrl: base(),
siteUrl: base(),
});
});
return page(req, res, {
return page(req, res, {
index: e.visibility === 'public',
index: e.visibility === 'public',
cache: e.visibility === 'public' ? PUBLIC_CACHE : null,
cache: e.visibility === 'public' ? PUBLIC_CACHE : null,
title: e.title,
title: e.title,
- description: (e.summary || `${e.title} on OpenVibe.Meme`).slice(0, 160),
+ description,
jsonLd: [jsonLd],
jsonLd: [jsonLd],
body: raw(views.entry({ e, citations, signedIn: Boolean(signedIn(req)), staff, reported: req.query.reported === '1', problem })),
body: raw(views.entry({ e, citations, signedIn: Boolean(signedIn(req)), staff, reported: req.query.reported === '1', problem })),
});
});
@@ -26,6 +26,11 @@ function jsonLd(html) {
@@ -26,6 +26,11 @@ function jsonLd(html) {
{ role: 'use', source_service: 'live', source_type: 'vod', source_id: '9002', vod_id: 9002, offset_seconds: 120, quote_text: 'the robot is offline for good', url: `${t.upstreams.url}/v/9002?t=120`, observed_at: now },
{ role: 'use', source_service: 'live', source_type: 'vod', source_id: '9002', vod_id: 9002, offset_seconds: 120, quote_text: 'the robot is offline for good', url: `${t.upstreams.url}/v/9002?t=120`, observed_at: now },
],
],
});
});
+ const describedEntry = await t.ctx.entries.createCandidate({
+ slug: 'cat-typing', title: 'Cat typing', kind: 'meme', status: 'rising',
+ summary: 'A cat at a keyboard, typed as though the cat is answering a work email one slow paw at a time.',
+ first_seen_at: now, last_seen_at: now, visibility: 'public', citations: [],
+ });
const hiddenEntry = await t.ctx.entries.createCandidate({
const hiddenEntry = await t.ctx.entries.createCandidate({
slug: 'not-yet', title: 'Not yet', kind: 'joke', summary: 'Hidden.', status: 'rising',
slug: 'not-yet', title: 'Not yet', kind: 'joke', summary: 'Hidden.', status: 'rising',
first_seen_at: now, last_seen_at: now, visibility: 'hidden',
first_seen_at: now, last_seen_at: now, visibility: 'hidden',
@@ -65,6 +70,13 @@ function jsonLd(html) {
@@ -65,6 +70,13 @@ function jsonLd(html) {
assert.ok(r.text.includes('/v/9002?t=120'));
assert.ok(r.text.includes('/v/9002?t=120'));
});
});
+ await check('one entry page describes itself for search and share cards', async () => {
+ const fallback = await t.get('/m/robot-is-offline');
+ assert.ok(fallback.text.includes('<meta name="description" content="Robot is offline: where the joke started, how it spread, and every use on OpenVibe.">'), 'the fallback line');
+ const own = await t.get('/m/cat-typing');
+ assert.ok(own.text.includes('<meta name="description" content="A cat at a keyboard, typed as though the cat is answering a work email one slow paw at a time.">'), 'the entry\'s own summary');
+ });
+
await check('a hidden entry is 404 to the public and visible to staff', async () => {
await check('a hidden entry is 404 to the public and visible to staff', async () => {
assert.strictEqual((await t.get('/m/not-yet')).status, 404);
assert.strictEqual((await t.get('/m/not-yet')).status, 404);
assert.strictEqual((await t.get('/m/not-yet', { as: alice })).status, 404);
assert.strictEqual((await t.get('/m/not-yet', { as: alice })).status, 404);