openvibers/openvibe.blog Commits

Blog desks: cited items first; a weekly Crowdfunding digest (#60)

OP OpenVibers committed 29 days ago · 8251a00fb9 · 4 files, +24 −2

* Blog desks: a digest lists the items it cites, the rest folded below with the same numbers

* Blog desks: a weekly Crowdfunding digest from Open Collective (Sources' crowdfunding category)

Changes

ddiff --git a/README.md b/README.md
iindex f4b7e78..a027d98 100644
---- a/README.md
++++ b/README.md
@@@ -170,6 +170,7 @@ indexed (`noindex`, not in the sitemaps).
| Food safety digest | `fda-food-recalls` | daily, 06:00 UTC (1 item at least) |
| Deals digest | `dealnews-daily` | daily, 07:00 UTC (5 items at least) |
| Hosting digest | `linode-types`, `aws-lightsail-bundles`, `ovhcloud-vps`, `vultr-plans`, `lowendbox-posts`, `lowendtalk-offers` | Mondays, for the week before |
++| Crowdfunding digest | `opencollective-accounts` | Mondays, for the week before (3 items at least) |
| Developer digest | `nodejs-blog` | Mondays, for the week before |
| Is This Real? (satire) | none | daily, 14:00 UTC |
ddiff --git a/server/desks/catalog.js b/server/desks/catalog.js
iindex a432357..eb80695 100644
---- a/server/desks/catalog.js
++++ b/server/desks/catalog.js
@@@ -33,6 +33,11 @@ const DESKS = [
about: 'New and changed VPS and cloud plans in providers\' own price lists (Akamai/Linode, AWS Lightsail, OVHcloud, Vultr) and low-end hosting offers, week by week.',
categories: ['hosting'], sources: ['linode-types', 'aws-lightsail-bundles', 'ovhcloud-vps', 'vultr-plans', 'lowendbox-posts', 'lowendtalk-offers'],
},
++ {
++ key: 'crowdfunding', title: 'Crowdfunding digest', every: 'week', hour: 7, min: 3, max: 30,
++ about: 'New and growing open-source and community campaigns on Open Collective, week by week: what they raise money for and what they have received, as Open Collective states it.',
++ categories: ['crowdfunding'], sources: ['opencollective-accounts'],
++ },
{
key: 'dev', title: 'Developer digest', every: 'week', hour: 7, min: 1, max: 30,
about: 'Releases and news from the Node.js project blog, week by week.',
ddiff --git a/server/desks/render.js b/server/desks/render.js
iindex dd55099..2614998 100644
---- a/server/desks/render.js
++++ b/server/desks/render.js
@@@ -33,7 +33,7 @@ function index({ latest }) {
</li>`;
};
return String(h`<header class="blog-header"><h1>AI desks</h1>
--<p class="lede">Niche blogs written by AI from the data OpenVibe.Sources collects (government news, food recalls, deals, hosting plans, developer releases), and one satire column. Each digest links every item it was written from.</p></header>
++<p class="lede">Niche blogs written by AI from the data OpenVibe.Sources collects (government news, food recalls, deals, hosting plans, crowdfunding, developer releases), and one satire column. Each digest links every item it was written from.</p></header>
<p class="disclosure" role="note"><strong>Written by AI.</strong> ${AI_NOTE} The people's blogs are elsewhere on <a href="/">OpenVibe.Blog</a>.</p>
<ul class="desk-list">${catalog.ALL.map(card)}</ul>`);
}
@@@ -53,6 +53,21 @@ ${raw(ssr.paginationHtml(pager))}
${staff ? h`<p class="meta">Staff see hidden posts here too.</p>` : ''}`);
}
++/**
++ * The items under a digest: the ones it cites, numbered as the citations are; the rest of what the AI was given folded
++ * away below them (same numbers, so nothing renumbers).
++ */
++function sourcesList(post) {
++ const cited = new Set();
++ for (const s of post.body.sections || []) for (const p of (Array.isArray(s.paragraphs) ? s.paragraphs : [{ cites: s.cites || [] }])) for (const n of p.cites || []) cited.add(n);
++ const item = (s, i) => h`<li id="src-${i + 1}" value="${i + 1}">${s.url ? h`<a href="${s.url}" rel="nofollow noopener">${s.title || s.url}</a>` : (s.title || s.item_id)} <span class="meta">(${s.source_key}${s.published_at ? h`, ${String(s.published_at).slice(0, 10)}` : ''})</span></li>`;
++ const all = post.sources.map((s, i) => ({ s, i }));
++ const used = all.filter((x) => cited.has(x.i));
++ const rest = all.filter((x) => !cited.has(x.i));
++ return h`<section class="sources"><h2>The items it cites</h2><ol>${used.map((x) => item(x.s, x.i))}</ol>
++${rest.length ? h`<details class="desk-gaps"><summary>${rest.length} more item${rest.length === 1 ? '' : 's'} from the period it was given</summary><ol>${rest.map((x) => item(x.s, x.i))}</ol></details>` : ''}</section>`;
++}
++
/** The article body (also the feeds' content). */
function articleBody(desk, post) {
if (desk.satire) {
@@@ -69,7 +84,7 @@ ${post.body.tags && post.body.tags.length ? h`<p class="tags">${post.body.tags.m
${post.dek ? h`<p class="lede">${post.dek}</p>` : ''}
<div class="post-body">${post.body.sections.map((s) => h`<section class="desk-section">${s.heading ? h`<h2>${s.heading}</h2>` : ''}${parasOf(s).map((p) => (p.markdown ? h`${md(p.text, 2)}<p>${cites(p.cites)}</p>` : h`<p>${p.text}${cites(p.cites)}</p>`))}</section>`)}</div>
${post.body.gaps && post.body.gaps.length ? h`<details class="desk-gaps"><summary>What the AI could not tell from the items</summary><ul>${post.body.gaps.map((g) => h`<li>${g}</li>`)}</ul></details>` : ''}
--<section class="sources"><h2>The items it was written from</h2><ol>${post.sources.map((s, i) => h`<li id="src-${i + 1}">${s.url ? h`<a href="${s.url}" rel="nofollow noopener">${s.title || s.url}</a>` : (s.title || s.item_id)} <span class="meta">(${s.source_key}${s.published_at ? h`, ${String(s.published_at).slice(0, 10)}` : ''})</span></li>`)}</ol></section>`);
++${sourcesList(post)}`);
}
function postPage({ desk, post, staff, csrf }) {
ddiff --git a/test/desks.test.js b/test/desks.test.js
iindex 2269a90..8a891fa 100644
---- a/test/desks.test.js
++++ b/test/desks.test.js
@@@ -181,6 +181,7 @@ function stub(handler) {
assert.ok(page.text.includes('href="https://example.gov/n2" rel="nofollow noopener"'));
assert.ok(page.text.includes('<p>Two agencies announced things. <span class="desk-cites">[<a href="#src-1" aria-label="Item 1">1</a>, <a href="#src-2" aria-label="Item 2">2</a>]</span></p>') && page.text.includes('id="src-3"'));
assert.ok(!page.text.includes('Something the AI made up'));
++ assert.ok(page.text.includes('<h2>The items it cites</h2><ol><li id="src-1" value="1">'), 'cited items first, numbered as cited');
assert.match(page.headers.get('cache-control'), /max-age=60/);
const s = await t.get(`/desks/satire/${satire.slug}`);
assert.ok(s.text.includes('<strong>Satire.</strong> This is made up'));