openvibers/openvibe.blog Commits

Share: one Share sheet under every post's title (#205)

OP OpenVibers committed 7 days ago · cfbf79e481 · 4 files, +38 −3

* Share: the one Share sheet under every post's title

Add the network's Share button (data-ov-share-sheet) below a post's
byline, with the post's canonical address, title, summary and cover
picture when it has one. Raise the CSS budget to the measured 48.1 KB.

* Share: the shared kit button, one look everywhere

Render the one Share button with openvibe-shared 4.114.0's kit.share
(or its exact markup where the page is built in the browser), so every
site shows the same quiet button with the share icon. Remove the
site-specific Share styles.

Changes

ddiff --git a/server/http/public.js b/server/http/public.js
iindex b0ca569..67fe2c6 100644
---- a/server/http/public.js
++++ b/server/http/public.js
@@@ -376,10 +376,11 @@ function createPublicRoutes(ctx) {
disclosure: rec ? authorship.disclosure(rec, review) : null,
decisionNote: preview ? `preview · ${decision.robots}` : null,
comments, canEdit: await access.canWrite(store, req.viewer, blog, 'edit', post),
-- urlFor: media.urlFor, breadcrumbs: crumbs, jsonUrl: `${path}.json`,
++ urlFor: media.urlFor, shareUrl: publication.postUrl(blog, post), breadcrumbs: crumbs, jsonUrl: `${path}.json`,
postPath: path, csrf, signedIn: req.viewer.kind === 'user' && Boolean(req.viewer.subject),
loginUrl: `/auth/login?next=${encodeURIComponent(path)}`,
}),
++ styles: ['kit.css'],
}, { cacheable });
}
ddiff --git a/server/render/pages.js b/server/render/pages.js
iindex dfea64e..6005767 100644
---- a/server/render/pages.js
++++ b/server/render/pages.js
@@@ -9,6 +9,7 @@
*/
const ssr = require('openvibe-publishing/ssr');
const { figureHtml } = require('openvibe-publishing/media');
++const kit = require('openvibe-shared/kit');
const { html: h, raw, escapeHtml: esc } = ssr;
const MEDIA_LINE = /^[ \t]*\[\[media:(med_[0-9A-HJKMNP-TV-Z]{26})\]\][ \t]*$/m;
@@@ -192,10 +193,12 @@ ${list}${more}${form}</section>`);
/** One post. */
function postPage(o) {
-- const { blog, blogUrl, post, rev, author, tags, categories, series, attachments, citations, disclosure, decisionNote, comments, canEdit, urlFor } = o;
++ const { blog, blogUrl, post, rev, author, tags, categories, series, attachments, citations, disclosure, decisionNote, comments, canEdit, urlFor, shareUrl } = o;
const rel = blog.kind === 'official' ? 'noopener' : 'nofollow ugc noopener';
const cover = attachments.filter((a) => a.role === 'cover');
const gallery = attachments.filter((a) => a.role === 'gallery');
++ const shareImage = cover.length && cover[0].mediaId ? urlFor(cover[0].mediaId) : null;
++ const share = shareUrl ? h`<p class="post-share">${raw(kit.share({ title: rev.fields.title, url: shareUrl, text: rev.fields.summary || '', image: shareImage || null }))}</p>` : '';
const badge = { members: 'Members only', private: 'Private', unlisted: 'Unlisted' }[post.visibility];
const updated = rev.number > 1 && post.first_published_at && Date.parse(rev.createdAt) > post.first_published_at + 60000;
const body = sectionsOf(renderBody(rev.content, attachments, { urlFor, rel }));
@@@ -210,6 +213,7 @@ ${raw(toc ? contentsHtml(toc) : '')}
<h1>${rev.fields.title}</h1>
<p class="byline">${author ? h`By <a href="${author.url}" rel="author">${author.name}</a> · ` : ''}${post.published_at ? h`${time(post.first_published_at || post.published_at)} · ` : ''}${updated ? h`updated ${time(Date.parse(rev.createdAt))} · ` : ''}<span class="read-time" title="At ${WORDS_PER_MINUTE} words a minute: a guide, not a promise">≈ ${minutes} min read</span>${badge ? h` <span class="badge">${badge}</span>` : ''}${post.state !== 'published' ? h` <span class="badge">${post.state}</span>` : ''}</p>
${series ? raw(seriesLine(series)) : ''}
++${share ? raw(String(share)) : ''}
${disclosure ? h`<p class="disclosure" role="note"><strong>${disclosure.short}.</strong> ${disclosure.long}</p>` : ''}
${raw(toc ? contentsFold(toc) : '')}
${raw(cover.map((a) => figureHtml(a, { urlFor })).join(''))}
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex 0579f06..0ce7b70 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -20,7 +20,7 @@ const BUDGETS = {
jsRawKB: 338.4, // 338.4 with openvibe-shared 4.114.0 (2026-11-06: the one Share sheet in the navbar (ov-share.js and ov-qr.js load on the first press); navbar.js gains its click handler); 337.1 with openvibe-shared 4.112.1 (2026-11-06: theme-loader.js (4.111.1) and the footer/appbar phone CSS (4.112.1)); 335.8 with openvibe-shared 4.109.2 (2026-11-03: OpenVibe's own select, the navbar loads ov-forms); 333.1 with openvibe-shared 4.107.0 (2026-11-03: open.<tld> sites are named for their domain; the brand knows its front door); 332.5 with openvibe-shared 4.106.2 (2026-11-02: the site's name reads whole on a signed-in phone; the balances stack); 331.3 with openvibe-shared 4.103.0 (2026-11-02: the puzzle glyph, two-line sidebar rows, boost lets parsed pages go, eight solids) (2026-11-02); 330.1 with 2026-11-02: openvibe-shared 4.99.0 (thread kit parts, glyph solids) (2026-11-02); 329.7 with Shared 4.95.0 (keyboard-reachable scrollers) (2026-11-01); 326.7 with openvibe-shared 4.93.2: content labels, glyph fallback, footer counts (2026-11-01); 324.8 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 323.2 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 321.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 318.1 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 315.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 307.8 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 300.5 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 263.4 with shared 4.11.0's seasonal navbar; 254.1 with openvibe-shared 4.9.0 (status text twins in every theme, 2026-10-23); 246.2 with openvibe-shared 4.2.x (Ask Actor in the navbar, 2026-10-15); 239.1 (212.2 before openvibe-shared/shell's web-runtime.js)
jsBrotliKB: 81.1, // 81.1 with openvibe-shared 4.114.0 (2026-11-06: the one Share sheet in the navbar (ov-share.js and ov-qr.js load on the first press); navbar.js gains its click handler); 80.1 with openvibe-shared 4.107.2 (2026-11-03: the last small touch targets in the shared bar and footer reach 44px); 79.5 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 78.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 77.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 76.1 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 74.2 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 70.5 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 67.9 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 65.3 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 61.7 with shared 4.11.0's seasonal navbar; 59.1 with openvibe-shared 4.4.0 (every site in the launcher, 2026-10-17); 56.3 (49.9 before web-runtime.js)
cssFiles: 2, // 1
-- cssRawKB: 48, // 47.7 with the post pages' reading layout (2026-10-30: the measure, the contents fold and rail, the series line and neighbours, all in blog.css); 43.1 with the short home (2026-10-30: the pick block's and the fold's own rules); 39.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 38.1 touch targets on phones (2026-10-29); 37.6 the home's phone limits, desk row and fold (2026-10-29); 36 measured with openvibe-shared v4.39.0 (+1.5 KB raw of kit/room/showcase CSS; raised from 34 by the measured growth; sizes are rounded down to 1 decimal, so the limit must clear 36.0); 29.1 openvibe-shared 4.23.0: the showcase hero on a phone (near the bar, buttons side by side, numbers three across); 27.9 on 2026-10-26 (Shared 4.13's home kit: showcase.css 12.2 → 18.3 KB); 22 on 2026-10-22 (the home's post cards, week bars, desks, topics, questions and links); 17.7 (2026-10-15: blog.css carries the AI desks' few rules; the shared showcase.css grew to ~12 KB)
++ cssRawKB: 48.2, // 48.1 with the post Share button (2026-11-06: the one Share sheet's button under the title); 47.7 with the post pages' reading layout (2026-10-30: the measure, the contents fold and rail, the series line and neighbours, all in blog.css); 43.1 with the short home (2026-10-30: the pick block's and the fold's own rules); 39.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 38.1 touch targets on phones (2026-10-29); 37.6 the home's phone limits, desk row and fold (2026-10-29); 36 measured with openvibe-shared v4.39.0 (+1.5 KB raw of kit/room/showcase CSS; raised from 34 by the measured growth; sizes are rounded down to 1 decimal, so the limit must clear 36.0); 29.1 openvibe-shared 4.23.0: the showcase hero on a phone (near the bar, buttons side by side, numbers three across); 27.9 on 2026-10-26 (Shared 4.13's home kit: showcase.css 12.2 → 18.3 KB); 22 on 2026-10-22 (the home's post cards, week bars, desks, topics, questions and links); 17.7 (2026-10-15: blog.css carries the AI desks' few rules; the shared showcase.css grew to ~12 KB)
cssBrotliKB: 10.4, // 10.1 with the post pages' reading layout (2026-10-30); 9.2 with the short home (2026-10-30: the pick block's and the fold's own rules); 7.3 openvibe-shared 4.26.1 (hero visual); 6.2 (2026-10-26, Shared 4.13's home kit); 5.1 (2026-10-22); 1.4
externalFiles: 1, // 0
};
ddiff --git a/test/share-sheet.test.js b/test/share-sheet.test.js
nnew file mode 100644
iindex 0000000..13bfa63
---- /dev/null
++++ b/test/share-sheet.test.js
@@@ -0,0 +1,30 @@
++'use strict';
++/**
++ * Every blog post carries the network's one Share button (openvibe-shared 4.114.0): one [data-ov-share-sheet] under
++ * the title, with the post's canonical address, title, summary and cover picture. The navbar loads the sheet on the
++ * first press; the page loads nothing more.
++ */
++const assert = require('assert');
++const { boot } = require('./helpers/boot');
++
++(async () => {
++ const t = await boot();
++ try {
++ const created = await t.get('/api/v1/blogs/openvibe/posts', { as: t.official, json: { title: 'A shareable post', summary: 'One line about the post.', body: 'The post body is long enough to publish.\n\n## One\n\nText.\n\n## Two\n\nMore.\n\n## Three\n\nEnd.' } });
++ assert.strictEqual(created.status, 201, created.text);
++ const post = created.json().post;
++ const published = await t.get(`/api/v1/posts/${post.id}/publish`, { as: t.official, json: {} });
++ assert.strictEqual(published.status, 200, published.text);
++ const r = await t.get(`/@openvibe/${post.slug}`);
++ assert.strictEqual(r.status, 200, r.text.slice(0, 200));
++ assert.strictEqual((r.text.match(/data-ov-share-sheet/g) || []).length, 1, 'one Share button');
++ assert.ok(r.text.includes(`data-url="https://openvibe.blog/@openvibe/${post.slug}"`), 'the post address');
++ assert.ok(r.text.includes('data-title="A shareable post"'), 'its title');
++ assert.ok(r.text.includes('data-text="One line about the post."'), 'its summary');
++ assert.ok(r.text.includes('<p class="post-share">'), 'under the title');
++ assert.ok(r.text.includes('class="ovk-btn ovk-btn--quiet"') && r.text.includes('<span>Share</span>'), 'the shared kit button');
++ console.log('blog share sheet ok');
++ } finally {
++ await t.close();
++ }
++})().catch((e) => { console.error(e); process.exit(1); });