openvibers/openvibe.shared Commits

4.87.0: share cards for every named domain; room kit pictures at their drawn size; footer contrast (#241)

OP OpenVibers committed 12 days ago · 6fadccf588 · 17 files, +157 −11

* 4.87.0: share cards for a service's other domains; the footer's quiet line reaches 4.5:1

* Room kit marks and card pictures ask Media for the width they are drawn at

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex a2fec6f..03a0280 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,25 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
++## 4.87.0 — 2026-11-01
++
++- **Share cards for a service's other domains.**
++ - openvibe.pics and openvibe.download (MediaHub), openvibe.homes (Rent) and openvibe.website (Space) previewed as
++ their service: a link to openvibe.pics showed the OpenVibe.Video card.
++ - `social.DOMAINS` names them (id, service, host, name, tagline, what); `social.domainOf(id)` and
++ `social.domainForHost(host)` look them up.
++ - The builders draw `social/<id>.jpg` like the service's card (the same mark, Pics in its pink) and write the
++ domain's `icons/sites.json` entry.
++ - `shell.page()` shares the domain's card on any page whose canonical URL is on that host, so the sites need no
++ change.
++- **The footer's "Nobody live right now" line reaches 4.5:1.** `.ovf-pulse-quiet` was the theme's muted text on the
++ pulse's tint: 4.34:1 on Daylight, on every page with a footer (axe flagged 42 inner pages). It is now the
++ secondary text.
++- **Room kit pictures load at the size they are drawn.** `kit.mark` and `kit.card` put a Media object's original
++ upload in the page: Report and Community drew a 3072 px, 3 MB avatar at 34 px, and Community a 2460 px picture in a
++ 280 px card. They now ask Media's resize ladder for the width (`?w=96` or `160` for marks, `640` for card
++ pictures); `kit.mediaW(url, width)` does the same for a site's own images.
++
## 4.86.0 — 2026-11-01
- **Status colour names every site can use.**
ddiff --git a/README.md b/README.md
iindex 672b52e..b8c99a5 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v4.86.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.87.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex 1bade6b..4738ecb 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.86.0",
++ "version": "4.87.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
ddiff --git a/footer.js b/footer.js
iindex 6805197..2829803 100644
---- a/footer.js
++++ b/footer.js
@@@ -435,7 +435,7 @@
.ovf-pulse-live{display:inline-flex;align-items:center;gap:7px;color:var(--text-primary,#f1f4fb)!important}
.ovf-dot{width:8px;height:8px;border-radius:50%;background:#ef4444;box-shadow:0 0 0 0 rgba(239,68,68,.6);animation:ovfLive 1.8s ease-out infinite}
@keyframes ovfLive{0%{box-shadow:0 0 0 0 rgba(239,68,68,.55)}70%{box-shadow:0 0 0 7px rgba(239,68,68,0)}100%{box-shadow:0 0 0 0 rgba(239,68,68,0)}}
--.ovf-pulse-quiet{font-weight:500!important;color:var(--text-muted,#8b93ad)!important}
++.ovf-pulse-quiet{font-weight:500!important;color:var(--text-secondary,#b6bdd2)!important}
@media (max-width:900px){.ovf-cols{grid-template-columns:repeat(2,minmax(0,1fr))}.ovf-col--brand{grid-column:1/-1}}
@media (max-width:560px){
.ovf{padding-bottom:calc(20px + var(--ov-footer-clearance,0px) + env(safe-area-inset-bottom,0px))}
ddiff --git a/icons/sites.json b/icons/sites.json
iindex 7bb2a15..248c9da 100644
---- a/icons/sites.json
++++ b/icons/sites.json
@@@ -69,6 +69,11 @@
"description": "Name it, price it, run it",
"icon": "network"
},
++ "download": {
++ "name": "OpenVibe.Download",
++ "description": "Your files, shared on your terms",
++ "icon": "media"
++ },
"events": {
"name": "OpenVibe.Events",
"description": "Events your services can count on.",
@@@ -99,6 +104,11 @@
"description": "Answers for every OpenVibe site",
"icon": "network"
},
++ "homes": {
++ "name": "OpenVibe.Homes",
++ "description": "Every home for sale, one search",
++ "icon": "network"
++ },
"host": {
"name": "OpenVibe.Host",
"description": "Hosting, compared honestly",
@@@ -144,6 +154,11 @@
"description": "Go live once, stream everywhere",
"icon": "network"
},
++ "pics": {
++ "name": "OpenVibe.Pics",
++ "description": "Share pictures. Fast, open, yours.",
++ "icon": "media"
++ },
"play": {
"name": "OpenVibe.Play",
"description": "Achievements, leaderboards and cloud saves for every game.",
@@@ -239,6 +254,11 @@
"description": "Know the moment it changes",
"icon": "network"
},
++ "website": {
++ "name": "OpenVibe.Website",
++ "description": "Websites that look finished",
++ "icon": "space"
++ },
"wiki": {
"name": "OpenVibe.Wiki",
"description": "Knowledge, with sources",
ddiff --git a/kit.js b/kit.js
iindex 083d59c..4401d52 100644
---- a/kit.js
++++ b/kit.js
@@@ -28,6 +28,7 @@
* kit.empty({ title, text, action: { href, label } }) · kit.notice({ kind, text }) · kit.more({ label, items })
* kit.button({ href | type, label, icon, kind }) · kit.iconButton(…) · kit.chips(…) · kit.seg(…) · kit.icon(name)
* kit.mark({ name, src, hue, square }) · kit.when(ts) → { iso, text } · kit.esc(text) · kit.ICONS (names)
++ * kit.mediaW(url, width) a Media object at a resize width (64/96/160/320/480/640)
* kit.sidebar({ id, label, action, find, nav, sections, foot }) · kit.sidebarButton({ id }) · kit.withSidebar({ sidebar,
* body }) the left sidebar: vertical navigation, a drawer on phones
*
@@@ -49,6 +50,10 @@
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
/** A URL kept only when it is http(s), a same-site path, a query or a fragment; anything else becomes '#'. */
const url = (u) => { const s = String(u == null ? '' : u).trim(); return /^(https?:\/\/|\/(?!\/)|\?|#)/i.test(s) ? s : '#'; };
++ // A Media object (openvibe.media/o/med_…) at a width from Media's resize ladder (64, 96, 160, 320, 480, 640); any other
++ // URL as url() gives it. Avatars and pictures are stored as uploaded: Report and Community drew a 3072 px, 3 MB
++ // avatar at 34 px (4.87.0).
++ const mediaW = (u, w) => { const s = url(u); return /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]+$/.test(s) ? `${s}?w=${Number(w) || 160}` : s; };
const attrs = (o) => Object.keys(o || {}).filter((k) => o[k] != null && o[k] !== false && /^[a-z][a-z0-9-]*$/i.test(k)).map((k) => (o[k] === true ? ` ${k}` : ` ${k}="${esc(o[k])}"`)).join('');
// ── Icons: 24-unit line drawings in the text's colour (stroke 2, round), one set for every site ──
@@@ -142,7 +147,7 @@
function mark({ name = '?', src = '', hue = null, square = false, size = null } = {}) {
const cls = `ovk-mark${square ? ' ovk-mark--square' : ''}`;
const st = size ? `--ovk-mark:${Number(size) || 40}px;` : '';
-- if (src && url(src) !== '#') return `<img class="${cls}" src="${esc(url(src))}" alt="" loading="lazy" decoding="async"${st ? ` style="${st}"` : ''}>`;
++ if (src && url(src) !== '#') return `<img class="${cls}" src="${esc(mediaW(src, (Number(size) || 40) > 48 ? 160 : 96))}" alt="" loading="lazy" decoding="async"${st ? ` style="${st}"` : ''}>`;
const words = String(name || '?').replace(/[^\p{L}\p{N} ]/gu, ' ').trim().split(/\s+/).filter(Boolean);
const letters = (words.length > 1 ? [...words[0]][0] + [...words[1]][0] : [...(words[0] || '?')].slice(0, 2).join('')).toUpperCase();
return `<span class="${cls}" style="${st}--ovk-hue:${hue == null ? hueOf(name) : Number(hue) || 0}" aria-hidden="true">${esc(letters)}</span>`;
@@@ -395,7 +400,7 @@
const link = url(href);
const first = String(preview || '').split('\n').slice(0, 9).join('\n').slice(0, 480).trimEnd();
const pic = media(image)
-- ? `<img src="${esc(image)}" alt="${esc(alt)}" loading="lazy" decoding="async">`
++ ? `<img src="${esc(mediaW(image, 640))}" alt="${esc(alt)}" loading="lazy" decoding="async">`
: first ? `<pre class="ovk-card-pre" aria-hidden="true">${esc(first)}</pre>`
: `<span class="ovk-card-noimg">${icon(CARD_ICON[type] || 'image')}</span>`;
const loop = media(video) ? `<video class="ovk-card-video" data-src="${esc(video)}" muted loop playsinline preload="none" aria-hidden="true" tabindex="-1"></video>` : '';
@@@ -837,7 +842,7 @@
return {
skeleton,
-- esc, url, icon, ICONS, PATHS, when, hueOf, mark, button, iconButton, action, badge, dot, card, rail, CARD_RATIO,
++ esc, url, mediaW, icon, ICONS, PATHS, when, hueOf, mark, button, iconButton, action, badge, dot, card, rail, CARD_RATIO,
head, tabs, tabOf, list, row, section, fold, more, sidebar, sidebarButton, withSidebar,
switchRow, rangeRow, selectRow, seg, chips, saveBar, find, dock, stats, empty, notice, snippet, sheet, sheetButton,
enhance, VERSION: 2,
ddiff --git a/package.json b/package.json
iindex 688645b..1b2fc1c 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.86.0",
++ "version": "4.87.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/scripts/build-icons.js b/scripts/build-icons.js
iindex 983754e..21e8959 100644
---- a/scripts/build-icons.js
++++ b/scripts/build-icons.js
@@@ -28,6 +28,9 @@ function serviceIds() {
}
function siteOf(id) {
++ // A domain card (openvibe.pics, …) names itself; its icon is its service's (social.iconSite).
++ const d = social.domainOf(id);
++ if (d) return { id, name: d.name, tagline: d.tagline };
const m = JSON.parse(fs.readFileSync(path.join(SERVICES, `${id}.json`), 'utf8'));
if (!m.site) throw new Error(`${id}: no site block`);
return { id, name: m.site.name, tagline: m.site.tagline || '' };
ddiff --git a/scripts/build-social-cards.js b/scripts/build-social-cards.js
iindex 321ac77..46efcf3 100644
---- a/scripts/build-social-cards.js
++++ b/scripts/build-social-cards.js
@@@ -75,6 +75,13 @@ function sites() {
if (only && !only.has(pg.id) && !only.has(pg.service)) continue;
out.push({ ...site, id: pg.id, heading: pg.heading, tagline: pg.tagline, what: pg.what, domain: `${site.domain}${pg.path}`, iconId: site.id });
}
++ for (const d of social.DOMAINS) {
++ const site = byId.get(d.service);
++ if (!site) throw new Error(`domain card ${d.id}: no site ${d.service} in the manifests`);
++ if (only && !only.has(d.id) && !only.has(d.service)) continue;
++ out.push({ ...site, id: d.id, name: d.name, tagline: d.tagline, what: d.what, domain: d.host,
++ accent: d.accent ? social.accentOf(d.id, d.accent) : site.accent, iconId: site.id });
++ }
return out;
}
ddiff --git a/shell.js b/shell.js
iindex 6a80459..07392af 100644
---- a/shell.js
++++ b/shell.js
@@@ -96,9 +96,14 @@ function serviceOf(o) {
const siteId = (o) => { const s = String(serviceOf(o) || '').toLowerCase(); return /^[a-z][a-z0-9-]{1,31}$/.test(s) ? s : ''; };
// A page that names no image shares its site's card (openvibe-shared/social), when the site has one and the page
--// has a canonical URL to make it absolute against. `image: null` opts out.
++// has a canonical URL to make it absolute against; a page on one of a service's named domains (openvibe.pics) shares
++// that domain's card. `image: null` opts out.
function defaultImage(o) {
if (!o.canonical) return null;
++ let host = '';
++ try { host = new URL(o.canonical).host; } catch (e) { /* a relative canonical: the service's card */ }
++ const domain = social.domainForHost(host);
++ if (domain && social.has(domain.id)) return social.url(domain.id);
const service = serviceOf(o);
return service ? social.url(service) : null;
}
ddiff --git a/social.js b/social.js
iindex f06316e..c25b0d9 100644
---- a/social.js
++++ b/social.js
@@@ -14,8 +14,10 @@
// social.accentOf(service, accent?) the two accent stops the card uses (build script)
// social.iconSite(service) the app-icon variant for the service's mark (build script)
// social.pageOf(id) / social.PAGES the page cards: which page and site a card id names (null for a site's card)
++// social.domainOf(id) / social.domainForHost(host) / social.DOMAINS a service's other named domains (openvibe.pics)
//
--// shell.page() uses social.url(o.service) for og:image when the page names no image of its own.
++// shell.page() uses social.url(o.service) for og:image when the page names no image of its own, or the domain's card
++// when the page's canonical URL is on one of DOMAINS' hosts.
// ═══════════════════════════════════════════════════════════════
const fs = require('fs');
const path = require('path');
@@@ -53,6 +55,28 @@ const PAGES = [
/** The page a card id names ({ id, service, heading, … }), or null for a site's own card. */
const pageOf = (id) => PAGES.find((pg) => pg.id === id) || null;
++// Domain cards (social/<id>.jpg): a service's other domains that wear their own name on the navbar (openvibe.pics is
++// MediaHub's, openvibe.homes is Rent's). Each gets a site card of its own, drawn like its service's (the same mark;
++// its own accent when it has one), and shell.page() shares it on any page whose canonical URL is on that host. Before
++// 4.86.1 a link to openvibe.pics previewed as OpenVibe.Video.
++const DOMAINS = [
++ { id: 'pics', service: 'media-hub', host: 'openvibe.pics', name: 'Pics', accent: '#f472b6', tagline: 'Share pictures. Fast, open, yours.',
++ what: 'A link to share or embed any picture, in any size or format, with location and camera details removed.' },
++ { id: 'download', service: 'media-hub', host: 'openvibe.download', name: 'Download', tagline: 'Your files, shared on your terms',
++ what: 'A private drive with 25 GB free: share files with links you can take back, and back up a folder.' },
++ { id: 'homes', service: 'rent', host: 'openvibe.homes', name: 'Homes', tagline: 'Every home for sale, one search',
++ what: 'Zillow, Realtor.com, Redfin, Homes.com, Trulia and Craigslist searched at once, plus HUD and bank homes.' },
++ { id: 'website', service: 'space', host: 'openvibe.website', name: 'Website', tagline: 'Websites that look finished',
++ what: 'Start from a real shop, portfolio, event, link page or launch page, or describe yours, and change anything from the first preview.' },
++];
++/** The domain card an id names ({ id, service, host, name, … }), or null. */
++const domainOf = (id) => DOMAINS.find((d) => d.id === id) || null;
++/** The domain card for a host (a www. prefix and a port ignored), or null. */
++function domainForHost(host) {
++ const h = String(host || '').toLowerCase().replace(/:\d+$/, '').replace(/^www\./, '');
++ return DOMAINS.find((d) => d.host === h) || null;
++}
++
const has = (service) => ID_RE.test(String(service || '')) && isCard(`social/${service}.jpg`);
function url(service, base = '/shared') {
@@@ -63,7 +87,12 @@ function url(service, base = '/shared') {
}
// The mark's tint: app-icon has variants for a few sites; the rest wear the network's.
--const iconSite = (service) => (Object.prototype.hasOwnProperty.call(icon.SITES, service) ? service : service === 'media-hub' ? 'media' : 'network');
++// A domain card wears its service's mark.
++function iconSite(service) {
++ const d = domainOf(service);
++ if (d) return iconSite(d.service);
++ return Object.prototype.hasOwnProperty.call(icon.SITES, service) ? service : service === 'media-hub' ? 'media' : 'network';
++}
/** A deeper stop of the same hue (lightness down 16 points, saturation up 6), so a gradient never turns muddy. */
function deepen(hex) {
@@@ -82,4 +111,4 @@ function accentOf(service, accent) {
return /^#[0-9a-f]{6}$/i.test(accent || '') ? [accent.toLowerCase(), deepen(accent)] : icon.SITES[iconSite(service)].accent;
}
--module.exports = { has, url, isCard, path: cardPath, accentOf, iconSite, pageOf, PAGES, DIR };
++module.exports = { has, url, isCard, path: cardPath, accentOf, iconSite, pageOf, PAGES, domainOf, domainForHost, DOMAINS, DIR };
ddiff --git a/social/download.jpg b/social/download.jpg
nnew file mode 100644
iindex 0000000..b3d8152
BBinary files /dev/null and b/social/download.jpg differ
ddiff --git a/social/homes.jpg b/social/homes.jpg
nnew file mode 100644
iindex 0000000..c36297f
BBinary files /dev/null and b/social/homes.jpg differ
ddiff --git a/social/pics.jpg b/social/pics.jpg
nnew file mode 100644
iindex 0000000..03da4c4
BBinary files /dev/null and b/social/pics.jpg differ
ddiff --git a/social/website.jpg b/social/website.jpg
nnew file mode 100644
iindex 0000000..992f00d
BBinary files /dev/null and b/social/website.jpg differ
ddiff --git a/test/kit-media-width.test.js b/test/kit-media-width.test.js
nnew file mode 100644
iindex 0000000..3441f18
---- /dev/null
++++ b/test/kit-media-width.test.js
@@@ -0,0 +1,17 @@
++'use strict';
++// kit.mark and kit.card ask Media for a resize width instead of the original upload (4.87.0): Report and Community drew a
++// 3072 px, 3 MB avatar at 34 px, and Community a 2460 px picture in a 280 px card.
++const assert = require('assert');
++const kit = require('../kit');
++
++const obj = 'https://openvibe.media/o/med_01M0E4X90R0VPTR7JBXFXC2Y38';
++assert.strictEqual(kit.mediaW(obj, 96), `${obj}?w=96`);
++assert.strictEqual(kit.mediaW(`${obj}?w=64`, 96), `${obj}?w=64`, 'a URL that already has a query is left alone');
++assert.strictEqual(kit.mediaW('https://openvibe.media/t/vod-1.jpg', 96), 'https://openvibe.media/t/vod-1.jpg', 'thumbnails are already small');
++assert.strictEqual(kit.mediaW('https://example.com/a.png', 96), 'https://example.com/a.png');
++assert.strictEqual(kit.mediaW('javascript:alert(1)', 96), '#', 'still sanitised like kit.url');
++
++assert.match(kit.mark({ name: 'a', src: obj }), new RegExp(`src="${obj}\\?w=96"`), 'a 40 px mark asks for 96');
++assert.match(kit.mark({ name: 'a', src: obj, size: 64 }), new RegExp(`src="${obj}\\?w=160"`), 'a bigger mark asks for 160');
++assert.match(kit.card({ title: 't', href: '/x', image: obj }), new RegExp(`<img src="${obj}\\?w=640"`), 'a card picture asks for 640');
++console.log('kit media width: all checks passed');
ddiff --git a/test/social-domains.test.js b/test/social-domains.test.js
nnew file mode 100644
iindex 0000000..282d327
---- /dev/null
++++ b/test/social-domains.test.js
@@@ -0,0 +1,41 @@
++'use strict';
++// Domain cards (4.86.1): a service's other named domains have their own share card, and shell.page() shares it on any
++// page whose canonical URL is on that host. Before, a link to openvibe.pics previewed as OpenVibe.Video, openvibe.homes
++// as Rent and openvibe.website as Space.
++const assert = require('assert');
++const social = require('../social');
++const shell = require('../shell');
++const appIcons = require('../app-icons');
++
++const want = { pics: ['media-hub', 'openvibe.pics'], download: ['media-hub', 'openvibe.download'], homes: ['rent', 'openvibe.homes'],
++ website: ['space', 'openvibe.website'] };
++assert.deepStrictEqual(social.DOMAINS.map((d) => d.id).sort(), Object.keys(want).sort(), 'the four named domains');
++Object.entries(want).forEach(([id, [service, host]]) => {
++ const d = social.domainOf(id);
++ assert.ok(d && d.service === service && d.host === host, `${id}: ${service} on ${host}`);
++ assert.ok(d.name && d.tagline && d.what, `${id}: name, tagline and what`);
++ assert.ok(d.what.length <= 130, `${id}: the description fits the card's two lines (${d.what.length})`);
++ assert.ok(social.has(id), `${id}: social/${id}.jpg is drawn`);
++ assert.strictEqual(social.iconSite(id), social.iconSite(service), `${id} wears ${service}'s mark`);
++ assert.ok(appIcons.has(id) && appIcons.site(id).name === `OpenVibe.${d.name}`, `${id}: an icon entry named OpenVibe.${d.name}`);
++ assert.strictEqual(social.domainForHost(host), d);
++ assert.strictEqual(social.domainForHost(`www.${host}:443`), d, 'www. and a port are ignored');
++});
++assert.strictEqual(social.domainForHost('openvibe.video'), null, 'a service\'s own first domain keeps its card');
++assert.strictEqual(social.domainForHost(''), null);
++
++// shell.page: the canonical's host picks the domain's card; the service's own domain keeps the service's card.
++const og = (html) => (/og:image" content="([^"]+)"/.exec(html) || [])[1] || '';
++assert.match(og(shell.page({ service: 'media-hub', canonical: 'https://openvibe.pics/a/abc', description: 'd' })), /^https:\/\/openvibe\.pics\/shared\/social\/pics\.jpg\?v=/);
++assert.match(og(shell.page({ service: 'rent', canonical: 'https://openvibe.homes/', description: 'd' })), /\/shared\/social\/homes\.jpg\?v=/);
++assert.match(og(shell.page({ service: 'media-hub', canonical: 'https://openvibe.video/v/1', description: 'd' })), /\/shared\/social\/media-hub\.jpg\?v=/);
++assert.match(og(shell.page({ service: 'media-hub', canonical: 'https://openvibe.pics/', image: '/cover.png' })), /\/cover\.png$/, 'an explicit image still wins');
++// The builders draw a card and write an icon entry for each domain (scripts run by hand; their output is committed).
++const fs = require('fs');
++const path = require('path');
++const cards = fs.readFileSync(path.join(__dirname, '..', 'scripts', 'build-social-cards.js'), 'utf8');
++assert.match(cards, /for \(const d of social\.DOMAINS\)/);
++assert.match(cards, /domain: d\.host,/);
++const icons = fs.readFileSync(path.join(__dirname, '..', 'scripts', 'build-icons.js'), 'utf8');
++assert.match(icons, /const d = social\.domainOf\(id\);\s*if \(d\) return \{ id, name: d\.name, tagline: d\.tagline \};/);
++console.log('social domains: all checks passed');