openvibers/openvibe.news Commits
News: the front page shows the latest AI briefs (labelled AI) above the wire (#69)
Changes
@@@ -116,7 +116,7 @@ function createPublicRoutes(ctx) {// The front page opens with what OpenVibe.News is for (openvibe-shared/showcase), then the stories....(!topic && pager.page === 1 ? { styles: [showcase.STYLESHEET] } : {}),body: topic ? pages.topicPage({ topic, items, pager, feeds, breadcrumbs: crumbs })-- : (pager.page === 1 ? newsShowcase() : '') + pages.home({ items, pager, topics: await topics.list(), feeds, showcase: pager.page === 1, wire: pages.wireSection(wire) }),++ : (pager.page === 1 ? newsShowcase() : '') + pages.home({ items, pager, topics: await topics.list(), feeds, showcase: pager.page === 1, wire: pages.wireSection(wire), briefs: pager.page === 1 && briefs ? (await briefs.latest(12)).filter((b) => b.kind === 'summary' && !b.stub && b.paragraphs.length).slice(0, 4) : [] }),}, { cacheable: true });}@@@ -37,7 +37,7 @@ function feedLinksHtml(feeds) {// With the showcase above it (page 1), the showcase hero is the page's h1 and says what the site is,// so the list header is a plain "Latest stories" h2; later pages keep the site's own h1 and lede.--function home({ items, pager, topics, feeds, showcase = false, wire = '' }) {++function home({ items, pager, topics, feeds, showcase = false, wire = '', briefs = [] }) {return String(h`${showcase ? h`<header class="site-header"><h2>Latest stories</h2>${raw(feedLinksHtml(feeds))}</header>` : h`<header class="site-header">@@@ -45,12 +45,23 @@ ${raw(feedLinksHtml(feeds))}<p class="lede">Source-backed stories. Every paragraph cites the reports it rests on; corrections and retractions stay on the record.</p>${raw(feedLinksHtml(feeds))}</header>`}--<section id="stories" aria-label="Stories">${storyList(items, wire ? 'No stories have been published yet: editors write them from the sources, and every paragraph cites them. Until then, the wire below shows what the sources report.' : 'No stories have been published yet. OpenVibe.News publishes only what an editor has written from its sources.')}</section>++<section id="stories" aria-label="Stories">${storyList(items, wire || briefs.length ? `No stories have been published yet: editors write them from the sources, and every paragraph cites them. Until then, ${briefs.length ? 'the AI briefs and the wire below show' : 'the wire below shows'} what the sources report.` : 'No stories have been published yet. OpenVibe.News publishes only what an editor has written from its sources.')}</section>${raw(ssr.paginationHtml(pager))}++${raw(briefsSection(briefs))}${raw(wire)}${topics.length ? h`<section class="side" aria-label="Topics"><h2>Topics</h2><ul class="topic-list">${topics.map((t) => h`<li><a href="/topics/${t.slug}">${t.name}</a></li>`)}</ul></section>` : ''}`);}++/** The front page's latest AI briefs (server/domain/briefs.js), labelled as AI, each opening on its first paragraph. */++function briefsSection(briefs) {++ if (!briefs.length) return '';++ return String(h`<section class="home-briefs" aria-labelledby="briefs-h"><h2 id="briefs-h">AI briefs</h2>++<p class="meta">What the reports on a subject say, summarized by OpenVibe.AI with every paragraph linked to its reports. Written by AI, not by an editor.</p>++<ol class="story-list">${briefs.map((b) => h`<li class="story-item"><h3 class="story-item-title"><a href="/briefs/${b.cluster_id}">${b.headline || b.label}</a> <span class="badge">AI</span></h3>++<p class="meta">${time(b.window_end)} · from ${b.item_count} reports</p>${b.paragraphs[0] ? h`<p class="summary">${b.paragraphs[0].text.length > 240 ? `${b.paragraphs[0].text.slice(0, 237)}…` : b.paragraphs[0].text}</p>` : ''}</li>`)}</ol>++<p><a href="/briefs">All AI briefs</a></p></section>`);++}++// The wire: what the sources report, grouped, before (or without) an editor's story. Each line is the outlet's own// headline and link; nothing here is written by News.const WIRE_NOTE = 'Grouped automatically from the reports OpenVibe.Sources collects. Nothing here is written by OpenVibe.News: each line is the outlet\'s own headline and links to its report.';@@@ -45,6 +45,9 @@ const SAME = { 'sec-fetch-site': 'same-origin' };assert.ok(!r.text.includes('A claim nobody reported') || r.text.includes('What the AI left out'));const list = await t.get('/briefs');assert.ok(list.text.includes(`/briefs/${clusterId}`));++ const home = await t.get('/');++ assert.ok(home.text.includes('<h2 id="briefs-h">AI briefs</h2>') && home.text.includes(`href="/briefs/${clusterId}"`), 'the front page shows the latest briefs');++ assert.ok(home.text.includes('Written by AI, not by an editor.') && home.text.includes('the AI briefs and the wire below show'));});await check('a reader asks for another tone with a POST from this site; a GET never runs AI', async () => {
@@ -116,7 +116,7 @@ function createPublicRoutes(ctx) {
@@ -116,7 +116,7 @@ function createPublicRoutes(ctx) {
// The front page opens with what OpenVibe.News is for (openvibe-shared/showcase), then the stories.
// The front page opens with what OpenVibe.News is for (openvibe-shared/showcase), then the stories.
...(!topic && pager.page === 1 ? { styles: [showcase.STYLESHEET] } : {}),
...(!topic && pager.page === 1 ? { styles: [showcase.STYLESHEET] } : {}),
body: topic ? pages.topicPage({ topic, items, pager, feeds, breadcrumbs: crumbs })
body: topic ? pages.topicPage({ topic, items, pager, feeds, breadcrumbs: crumbs })
- : (pager.page === 1 ? newsShowcase() : '') + pages.home({ items, pager, topics: await topics.list(), feeds, showcase: pager.page === 1, wire: pages.wireSection(wire) }),
+ : (pager.page === 1 ? newsShowcase() : '') + pages.home({ items, pager, topics: await topics.list(), feeds, showcase: pager.page === 1, wire: pages.wireSection(wire), briefs: pager.page === 1 && briefs ? (await briefs.latest(12)).filter((b) => b.kind === 'summary' && !b.stub && b.paragraphs.length).slice(0, 4) : [] }),
}, { cacheable: true });
}, { cacheable: true });
}
}
@@ -37,7 +37,7 @@ function feedLinksHtml(feeds) {
@@ -37,7 +37,7 @@ function feedLinksHtml(feeds) {
// With the showcase above it (page 1), the showcase hero is the page's h1 and says what the site is,
// With the showcase above it (page 1), the showcase hero is the page's h1 and says what the site is,
// so the list header is a plain "Latest stories" h2; later pages keep the site's own h1 and lede.
// so the list header is a plain "Latest stories" h2; later pages keep the site's own h1 and lede.
-function home({ items, pager, topics, feeds, showcase = false, wire = '' }) {
+function home({ items, pager, topics, feeds, showcase = false, wire = '', briefs = [] }) {
return String(h`${showcase ? h`<header class="site-header"><h2>Latest stories</h2>
return String(h`${showcase ? h`<header class="site-header"><h2>Latest stories</h2>
${raw(feedLinksHtml(feeds))}
${raw(feedLinksHtml(feeds))}
</header>` : h`<header class="site-header">
</header>` : h`<header class="site-header">
@@ -45,12 +45,23 @@ ${raw(feedLinksHtml(feeds))}
@@ -45,12 +45,23 @@ ${raw(feedLinksHtml(feeds))}
<p class="lede">Source-backed stories. Every paragraph cites the reports it rests on; corrections and retractions stay on the record.</p>
<p class="lede">Source-backed stories. Every paragraph cites the reports it rests on; corrections and retractions stay on the record.</p>
${raw(feedLinksHtml(feeds))}
${raw(feedLinksHtml(feeds))}
</header>`}
</header>`}
-<section id="stories" aria-label="Stories">${storyList(items, wire ? 'No stories have been published yet: editors write them from the sources, and every paragraph cites them. Until then, the wire below shows what the sources report.' : 'No stories have been published yet. OpenVibe.News publishes only what an editor has written from its sources.')}</section>
+<section id="stories" aria-label="Stories">${storyList(items, wire || briefs.length ? `No stories have been published yet: editors write them from the sources, and every paragraph cites them. Until then, ${briefs.length ? 'the AI briefs and the wire below show' : 'the wire below shows'} what the sources report.` : 'No stories have been published yet. OpenVibe.News publishes only what an editor has written from its sources.')}</section>
${raw(ssr.paginationHtml(pager))}
${raw(ssr.paginationHtml(pager))}
+${raw(briefsSection(briefs))}
${raw(wire)}
${raw(wire)}
${topics.length ? h`<section class="side" aria-label="Topics"><h2>Topics</h2><ul class="topic-list">${topics.map((t) => h`<li><a href="/topics/${t.slug}">${t.name}</a></li>`)}</ul></section>` : ''}`);
${topics.length ? h`<section class="side" aria-label="Topics"><h2>Topics</h2><ul class="topic-list">${topics.map((t) => h`<li><a href="/topics/${t.slug}">${t.name}</a></li>`)}</ul></section>` : ''}`);
}
}
+/** The front page's latest AI briefs (server/domain/briefs.js), labelled as AI, each opening on its first paragraph. */
+function briefsSection(briefs) {
+ if (!briefs.length) return '';
+ return String(h`<section class="home-briefs" aria-labelledby="briefs-h"><h2 id="briefs-h">AI briefs</h2>
+<p class="meta">What the reports on a subject say, summarized by OpenVibe.AI with every paragraph linked to its reports. Written by AI, not by an editor.</p>
+<ol class="story-list">${briefs.map((b) => h`<li class="story-item"><h3 class="story-item-title"><a href="/briefs/${b.cluster_id}">${b.headline || b.label}</a> <span class="badge">AI</span></h3>
+<p class="meta">${time(b.window_end)} · from ${b.item_count} reports</p>${b.paragraphs[0] ? h`<p class="summary">${b.paragraphs[0].text.length > 240 ? `${b.paragraphs[0].text.slice(0, 237)}…` : b.paragraphs[0].text}</p>` : ''}</li>`)}</ol>
+<p><a href="/briefs">All AI briefs</a></p></section>`);
+}
+
// The wire: what the sources report, grouped, before (or without) an editor's story. Each line is the outlet's own
// The wire: what the sources report, grouped, before (or without) an editor's story. Each line is the outlet's own
// headline and link; nothing here is written by News.
// headline and link; nothing here is written by News.
const WIRE_NOTE = 'Grouped automatically from the reports OpenVibe.Sources collects. Nothing here is written by OpenVibe.News: each line is the outlet\'s own headline and links to its report.';
const WIRE_NOTE = 'Grouped automatically from the reports OpenVibe.Sources collects. Nothing here is written by OpenVibe.News: each line is the outlet\'s own headline and links to its report.';
@@ -45,6 +45,9 @@ const SAME = { 'sec-fetch-site': 'same-origin' };
@@ -45,6 +45,9 @@ const SAME = { 'sec-fetch-site': 'same-origin' };
assert.ok(!r.text.includes('A claim nobody reported') || r.text.includes('What the AI left out'));
assert.ok(!r.text.includes('A claim nobody reported') || r.text.includes('What the AI left out'));
const list = await t.get('/briefs');
const list = await t.get('/briefs');
assert.ok(list.text.includes(`/briefs/${clusterId}`));
assert.ok(list.text.includes(`/briefs/${clusterId}`));
+ const home = await t.get('/');
+ assert.ok(home.text.includes('<h2 id="briefs-h">AI briefs</h2>') && home.text.includes(`href="/briefs/${clusterId}"`), 'the front page shows the latest briefs');
+ assert.ok(home.text.includes('Written by AI, not by an editor.') && home.text.includes('the AI briefs and the wire below show'));
});
});
await check('a reader asks for another tone with a POST from this site; a GET never runs AI', async () => {
await check('a reader asks for another tone with a POST from this site; a GET never runs AI', async () => {