openvibers/openvibe.tools Commits

Home headline: only a doing word finishes "Small tools that …" (#283)

OP OpenVibers committed 2 days ago · 16518de269 · 2 files, +13 −3

The owner, 2026-11-10, on the home page: "Small tools that one reputation report for a domain or IP." reads
wrong. A tool without a hand-written phrase spoke in the first clause of its tagline, and some taglines name a
thing instead of saying what the tool does. Now such a clause is used only when it starts with a doing word (a
short list: check, convert, find, make, merge, test, trim …); otherwise that tool sits out of the headline. A
question or an exclamation ends the clause too ("…policy?." could show). The reputation tool gets its own phrase:
"check a domain or IP address". The headline test fails on a phrase that starts with one, a, an, the, your,
every or all, or ends in "?." / "!.".

Changes

ddiff --git a/apps/gateway/server/pages/site.js b/apps/gateway/server/pages/site.js
iindex 5c06b87..efa49b3 100644
---- a/apps/gateway/server/pages/site.js
++++ b/apps/gateway/server/pages/site.js
@@@ -121,6 +121,11 @@ details{border-bottom:1px solid var(--border,rgba(255,255,255,.08));padding:12px
/* Categories as folds (11-09): the icon and the name are the fold's title; its count sits beside them. */
.job .fold-block{margin-top:10px}.job .fold-h .sec-h{display:flex;align-items:center;gap:10px;flex:1 1 auto;min-width:0;margin:0}.job .fold-h h2{margin:0}
.job{border-top:1px solid var(--border,rgba(255,255,255,.08));padding-top:4px}
++// The doing words a tool's tagline may start with to finish "Small tools that …" (see clause() in the home).
++const HEADLINE_VERBS = new Set(('add build calculate change check clean compare compress convert count create crop decode design detect draw edit '
++ + 'encode estimate explore extract find fix flip format generate get look make map measure merge minify mix pick plan play '
++ + 'preview read record remove rename resize roll rotate save scan search see send share shorten sign split strip test time '
++ + 'track translate trim turn validate verify watch write').split(' '));
/* The headline's last words change: each phrase is a link to its tool, all stacked in one cell (as tall as the
tallest, so nothing below moves). The one showing rises in; the one leaving lifts away. Once through, then it rests. */
.hm-rot{display:grid}.hm-ph{grid-area:1/1;color:var(--accent-light,var(--accent,#60a5fa));text-decoration:none;opacity:0;visibility:hidden;transform:translateY(.35em);
@@@ -436,15 +441,18 @@ function pageIndex(top = [], activity = { available: false }) {
const rankedIds = (activity.popular || []).map(t => t.id);
const quickTools = [...new Set([...rankedIds, ...POPULAR, ...available.map(t => t.id)])].map(id => byId.get(id)).filter(t => t && t.status !== 'unavailable').slice(0, 12);
const quick = quickTools.map(t => kit.button({ href: t.url, label: t.name, kind: 'quiet' }).replace('<span>', icon(t.icon, 20) + '<span>'));
-- const phrases = { yt: 'save a video', logo: 'make a logo', ssl: 'check a TLS certificate', trim: 'trim your audio', regex: 'test a regular expression', maps: 'find places nearby', food: 'find a meal nearby', mergepdf: 'merge your PDFs', dns: "check a domain’s DNS", compress: 'shrink an image', json: 'format your JSON', jsonminify: 'minify your JSON', fancy: 'restyle your text', whois: 'look up a domain', convert: 'convert your images', resize: 'resize your images', mp3: 'convert your audio' };
++ const phrases = { reputation: 'check a domain or IP address', yt: 'save a video', logo: 'make a logo', ssl: 'check a TLS certificate', trim: 'trim your audio', regex: 'test a regular expression', maps: 'find places nearby', food: 'find a meal nearby', mergepdf: 'merge your PDFs', dns: "check a domain’s DNS", compress: 'shrink an image', json: 'format your JSON', jsonminify: 'minify your JSON', fancy: 'restyle your text', whois: 'look up a domain', convert: 'convert your images', resize: 'resize your images', mp3: 'convert your audio' };
// A tool with no hand-written phrase speaks in its own tagline: its first clause, as the end of "Small tools
// that …" ("Look up DNS records for any domain: A, AAAA…" → "look up DNS records for any domain"). An acronym
// keeps its capitals. "help with <name>" was the old fallback, and read like a form letter.
++ // Only a clause that starts with a doing word finishes the sentence (the owner, 2026-11-10, on "Small tools that one
++ // reputation report for a domain or IP": "the wording is kind of awkward"); a tagline that names a thing instead is
++ // skipped, never bent into the sentence. A question or an exclamation ends the clause too.
const clause = (t) => {
-- let s = String(t.tagline || '').split(/[:;.(]|, | — | - /)[0].trim();
++ let s = String(t.tagline || '').split(/[:;.(?!]|, | — | - /)[0].trim();
if (s.split(/\s+/).length < 2 || s.length > 46) return null;
if (/^[A-Z][a-z]/.test(s)) s = s[0].toLowerCase() + s.slice(1);
-- return s;
++ return HEADLINE_VERBS.has(s.split(/\s+/)[0]) ? s : null;
};
const spoken = quickTools.map(t => [t, phrases[t.id] || clause(t)]).filter(([, p]) => p).slice(0, 6);
// Every phrase is in the page as a link to its tool, stacked in one grid cell: the cell is as tall as the tallest,
ddiff --git a/apps/gateway/test/home-headline.test.js b/apps/gateway/test/home-headline.test.js
iindex af840dd..9667db3 100644
---- a/apps/gateway/test/home-headline.test.js
++++ b/apps/gateway/test/home-headline.test.js
@@@ -27,6 +27,8 @@ const site = require('../server/pages/site');
assert.match(m[2], /^(https:\/\/[a-z0-9.-]+\/|\/tool\/[a-z0-9-]+)/, `phrase ${i} links to a tool`);
assert.match(m[4], /^[a-z][^<]{3,48}\.$/, `phrase ${i} reads as the end of the sentence: "${m[4]}"`);
assert.ok(!/^help with /.test(m[4]), 'no form-letter fallback');
++ // "Small tools that one reputation report for a domain or IP." (the owner, 2026-11-10): a phrase is a doing, never a thing.
++ assert.ok(!/^(one|a|an|the|your|every|all)\b/.test(m[4]) && !/[?!]\.$/.test(m[4]), `phrase ${i} starts with a doing word: "${m[4]}"`);
if (i) assert.ok(/aria-hidden="true"/.test(m[3]) && /tabindex="-1"/.test(m[3]), 'a phrase that is not showing is out of the tab order and unread');
});
assert.strictEqual(new Set(links.map((m) => m[2])).size, links.length, 'each phrase is a different tool');