openvibers/openvibe.today Commits

SEO-13: each dated row on the public schedule is an Event node (#5)

OP OpenVibers committed 16 days ago · 618208ee92 · 3 files, +32 −3

* Today: each dated row on the public schedule is an Event node for the event it links (SEO-13)

* Today: the schedule's JSON-LD is one script, one @graph (SEO-13)

Changes

ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 9f50276..7300608 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -30,6 +30,15 @@ const DAY = 24 * 60 * 60_000;
/** One event's end: its own, or an hour after it starts. */
const eventEnd = (e) => e.ends_at || new Date(new Date(e.starts_at).getTime() + 60 * 60_000).toISOString();
++/**
++ * The page's JSON-LD in one script: every node in one @graph, so a page carries exactly one JSON-LD script.
++ * The nodes keep their own fields; only their own @context goes, because the graph carries it once.
++ */
++const oneGraph = (nodes) => {
++ const graph = nodes.filter(Boolean).map((n) => { const c = { ...n }; delete c['@context']; return c; });
++ return [{ '@context': 'https://schema.org', '@graph': graph }];
++};
++
/** schema.org Event for one of this site's events. */
function eventJsonLd(e, config) {
const base = String(config.baseUrl).replace(/\/+$/, '');
@@@ -69,7 +78,8 @@ function createPublicPageViews(ctx) {
title: null,
body: raw(views.home({ upcoming: rows, now })),
description: 'Streams, events and reminders across OpenVibe on one calendar: schedule a stream, get reminded, and subscribe in your own calendar app.',
-- jsonLd: homeJsonLd(config),
++ // Every row on the page is a dated, public event the reader can open: that is exactly one Event node.
++ jsonLd: oneGraph([...homeJsonLd(config), ...rows.map((e) => eventJsonLd(e, config)), seo.jsonLd.organization()]),
};
}
ddiff --git a/test/discovery.test.js b/test/discovery.test.js
iindex 7fb73c6..df4d43b 100644
---- a/test/discovery.test.js
++++ b/test/discovery.test.js
@@@ -61,7 +61,10 @@ const PRIVATE = ['/auth/', '/api/', '/me', '/brief', '/schedule', '/internal'];
await check('home page: WebSite, WebApplication and WebPage JSON-LD', async () => {
const r = await t.get('/');
-- const blocks = [...r.text.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
++ // One JSON-LD script per page: every node lives in the page's @graph.
++ const scripts = [...r.text.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
++ assert.strictEqual(scripts.length, 1, 'the page carries exactly one JSON-LD script');
++ const blocks = scripts.flatMap((x) => (Array.isArray(x['@graph']) ? x['@graph'] : [x]));
const types = blocks.map((b) => b['@type']);
assert.ok(types.includes('WebSite'), types.join(','));
assert.ok(types.includes('WebApplication'), types.join(','));
ddiff --git a/test/pages.test.js b/test/pages.test.js
iindex ce5b80e..bf2f564 100644
---- a/test/pages.test.js
++++ b/test/pages.test.js
@@@ -7,9 +7,13 @@ const assert = require('assert');
const { boot, check, done } = require('./helpers/boot');
const { assetVersion } = require('../server/render/layout');
--function jsonLd(html) {
++// One JSON-LD script per page: every node lives in the page's @graph.
++function scripts(html) {
return [...String(html).matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
}
++function jsonLd(html) {
++ return scripts(html).flatMap((x) => (Array.isArray(x['@graph']) ? x['@graph'] : [x]));
++}
(async () => {
const t = await boot();
@@@ -26,9 +30,21 @@ function jsonLd(html) {
assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=60');
assert.strictEqual(r.headers.get('vary'), 'Cookie');
for (const marker of ['id="td-home"', 'id="td-upcoming"', 'id="main"', '<noscript><nav', '<footer id="ov-footer"', 'Public stream']) assert.ok(r.text.includes(marker), marker);
++ assert.strictEqual(scripts(r.text).length, 1, 'the page carries exactly one JSON-LD script');
const types = jsonLd(r.text).map((x) => x['@type']);
assert.ok(types.includes('WebSite'), types.join(','));
assert.ok(types.includes('WebApplication'), types.join(','));
++ // The day's list is dated, so each row is an Event node describing the event it links to.
++ const events = jsonLd(r.text).filter((x) => x['@type'] === 'Event');
++ assert.strictEqual(events.length, 1, 'one Event for the one event on the page');
++ assert.strictEqual(events[0].name, 'Public stream');
++ assert.strictEqual(events[0].startDate, event.starts_at);
++ assert.strictEqual(events[0].url, `https://openvibe.today/e/${event.id}`);
++ assert.strictEqual(events[0].organizer.name, '@alice');
++ assert.ok(r.text.includes(`href="/e/${event.id}"`), "the Event's url is the row's link");
++ // A page with nothing dated says so by having no Event node.
++ const developers = jsonLd((await t.get('/developers')).text);
++ assert.ok(!developers.some((x) => x['@type'] === 'Event'));
});
await check('a page drawn for a signed-in person is private, no-store', async () => {