openvibers/openvibe.space Commits

Font Awesome out, the OpenVibe glyphs in (plan L54 K4) (#157)

OP OpenVibers committed 12 days ago · 8c2d7f022a · 5 files, +85 −5

The owner, 2026-11-01: "get rid of Font Awesome completely and just use our own
openvibe.design system". The layout's link to cdnjs' all.min.css is gone; the icons
come from the OpenVibe glyphs (openvibe-shared/glyphs) and the brand logos
(openvibe-shared/brands), which the Shared navbar loads for the page. cdnjs stays in
the CSP: CodeMirror still comes from there.

Every fa- name Space named that no glyph draws is now one that is drawn:
folder-plus -> folder, file-zipper -> box-archive, bookmark -> book,
square-poll-vertical -> chart-column.

test/no-font-awesome.test.js holds it there: no page links Font Awesome, nothing
draws with its font, and every fa- name is drawn.

Changes

ddiff --git a/server/render/hosting-pages.js b/server/render/hosting-pages.js
iindex 5f19511..48111f7 100644
---- a/server/render/hosting-pages.js
++++ b/server/render/hosting-pages.js
@@@ -42,7 +42,7 @@ function home({ projects, csrf, canCreate = true }) {
</div><button type="submit" class="hd-btn hd-btn--primary">${icon('fa-plus')} Create project</button>`, { cls: 'hd-form' });
return `<header class="hd-head"><p class="hd-eyebrow">${icon('fa-globe')} Static hosting</p><h1>Your projects</h1>
<p class="hd-sub">A project holds sites, their deploys and domains, and the people who may change them.</p></header>
--${projects.length ? `<div class="hd-tiles">${cards}</div>` : `<div class="hd-empty">${icon('fa-folder-plus')}<p>You have no projects yet.</p></div>`}
++${projects.length ? `<div class="hd-tiles">${cards}</div>` : `<div class="hd-empty">${icon('fa-folder')}<p>You have no projects yet.</p></div>`}
${card('New project', create, { sub: canCreate ? 'Production sites can take custom domains; a sandbox is for trying things and is never indexed.' : '' })}`;
}
@@@ -128,7 +128,7 @@ function site({ site: s, project: p, role, deploys, activations, domains, siteCo
<p class="hd-chips"><a class="hd-url hd-url--big" href="${esc(url)}" rel="noopener">${esc(url.replace(/^https?:\/\//, ''))} ${icon('fa-arrow-up-right-from-square')}</a> ${status} <code>${esc(s.id)}</code></p></header>
${canDeploy ? card('Upload a deploy', form(`/sites/${s.id}/deploys`, csrf, `<div class="hd-sources">
<fieldset class="hd-source"><legend>${icon('fa-folder-open')} A folder</legend><input type="file" name="files" multiple webkitdirectory aria-label="A folder to upload"><input type="hidden" name="strip" value="folder"></fieldset>
--<fieldset class="hd-source"><legend>${icon('fa-file-zipper')} …or an archive (.tar.gz / .tar)</legend><input type="file" name="archive" accept=".tar,.tgz,.gz,application/gzip,application/x-tar" aria-label="An archive to upload (.tar.gz or .tar)"></fieldset>
++<fieldset class="hd-source"><legend>${icon('fa-box-archive')} …or an archive (.tar.gz / .tar)</legend><input type="file" name="archive" accept=".tar,.tgz,.gz,application/gzip,application/x-tar" aria-label="An archive to upload (.tar.gz or .tar)"></fieldset>
</div>
<label class="hd-field hd-field--narrow">Deploy from subfolder (optional) <input name="root" placeholder="dist"></label>
<fieldset class="hd-choices"><legend>When it is ready</legend>
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex 575af61..f75e54d 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -140,9 +140,10 @@ function renderPage(o) {
...feeds.map((f) => `<link rel="alternate" type="application/rss+xml" title="${escapeHtml(f.title)}" href="${escapeHtml(f.href)}">`),
...(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),
`<link rel="stylesheet" href="${asset('css/space.css')}">`,
++ // The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01).
++ `<script id="ov-glyphs-loader" src="${ovServe.url('ov-glyphs.js')}" defer></script>`,
// Page-only stylesheets of this site (the dashboard's hosting.css), after space.css; the home page loads none.
...(o.localStyles || []).map((rel) => `<link rel="stylesheet" href="${asset(rel)}">`),
-- '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">',
`<meta name="ov-boost" content="space@${escapeHtml(RELEASE)}">`,
`<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,
// The rules that show the picked part of a pick block are written for the page, because there is one per part.
ddiff --git a/server/spaces/render.js b/server/spaces/render.js
iindex eae2f40..d855020 100644
---- a/server/spaces/render.js
++++ b/server/spaces/render.js
@@@ -169,7 +169,7 @@ function editorPage({ space, files, csrf, canPublish, url, aiOn, buildsLeft, tem
};
// The claim (openvibe.website, plan 5.9): a signed-in person who holds the secret keeps the draft.
const claim = guest ? (guest.signedIn
-- ? `<form method="post" action="/spaces/${esc(guest.id)}/claim"><input type="hidden" name="key" value="${esc(guest.key)}"><input type="hidden" name="csrf" value="${esc(guest.claimCsrf || '')}"><button class="sp-btn" type="submit"><i class="fa-solid fa-bookmark" aria-hidden="true"></i> Keep this site</button></form>`
++ ? `<form method="post" action="/spaces/${esc(guest.id)}/claim"><input type="hidden" name="key" value="${esc(guest.key)}"><input type="hidden" name="csrf" value="${esc(guest.claimCsrf || '')}"><button class="sp-btn" type="submit"><i class="fa-solid fa-book" aria-hidden="true"></i> Keep this site</button></form>`
: `<p class="sp-muted">This draft is yours until you sign in. <a class="sp-btn sp-btn-quiet" href="${esc(`/auth/login?next=${encodeURIComponent(`/spaces/${guest.id}/edit?key=${guest.key}`)}`)}">Sign in to keep it</a></p>`) : '';
const body = `<div class="sp-editor" id="sp-editor" data-view="preview">
<header class="sp-ed-bar">
ddiff --git a/server/spaces/templates.js b/server/spaces/templates.js
iindex ca8d6a0..03b5b04 100644
---- a/server/spaces/templates.js
++++ b/server/spaces/templates.js
@@@ -13,7 +13,7 @@ const TEMPLATES = [
{ id: 'guestbook', name: 'Guestbook', icon: 'fa-book-open', accent: '#c2410c', description: 'Visitors sign in and leave a note for everyone to read.', capabilities: ['profile', 'data'] },
{ id: 'ai-buddy', name: 'AI buddy', icon: 'fa-robot', accent: '#4f46e5', description: 'A chat with an AI character you describe; each visitor\'s conversation is kept for them.', capabilities: ['profile', 'kv', 'ai'],
instructions: 'You are a friendly, curious and slightly funny companion. Keep answers short and warm, ask a question back now and then, and never pretend to be a person.' },
-- { id: 'poll', name: 'Quick poll', icon: 'fa-square-poll-vertical', accent: '#059669', description: 'One question, a few answers, one vote each, live results.', capabilities: ['profile', 'data'] },
++ { id: 'poll', name: 'Quick poll', icon: 'fa-chart-column', accent: '#059669', description: 'One question, a few answers, one vote each, live results.', capabilities: ['profile', 'data'] },
{ id: 'reaction', name: 'Reaction game', icon: 'fa-bolt', accent: '#16a34a', description: 'Click when it turns green; best times go on a leaderboard.', capabilities: ['profile', 'data'] },
{ id: 'trivia', name: 'Trivia', icon: 'fa-circle-question', accent: '#d97706', description: 'Questions whose answers stay on the server (a space function), with how many got each right.', capabilities: [], functions: ['check'] },
{ id: 'todo', name: 'Private to-dos', icon: 'fa-list-check', accent: '#ca8a04', description: 'A to-do list saved to each visitor\'s own account.', capabilities: ['profile', 'kv'] },
ddiff --git a/test/no-font-awesome.test.js b/test/no-font-awesome.test.js
nnew file mode 100644
iindex 0000000..94afe0f
---- /dev/null
++++ b/test/no-font-awesome.test.js
@@@ -0,0 +1,79 @@
++'use strict';
++// No Font Awesome (plan L54 K4; the owner, 2026-11-01: "get rid of Font Awesome completely and just use our own
++// openvibe.design system"). OpenVibe.Space's icons are the OpenVibe glyphs (openvibe-shared/glyphs, drawn in the browser by
++// /shared/ov-glyphs.js, which the Shared navbar loads on a page that names an icon) and the brand logos from
++// openvibe-shared/brands. This holds it there:
++// - no page or stylesheet links Font Awesome, and nothing draws with Font Awesome's font;
++// - every page that names an icon loads the navbar or the glyph drop-in;
++// - every fa- name OpenVibe.Space uses is one the glyphs or the brands draw (a name nobody draws would show nothing).
++// node test/no-font-awesome.test.js
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const glyphs = require('openvibe-shared/glyphs');
++const brands = require('openvibe-shared/brands');
++
++const ROOT = path.join(__dirname, '..');
++const walk = (dir, ok, out = []) => {
++ if (!fs.existsSync(dir)) return out;
++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
++ if (e.name === 'node_modules' || e.name === 'vendor' || e.name.startsWith('.')) continue;
++ const p = path.join(dir, e.name);
++ if (e.isDirectory()) walk(p, ok, out); else if (ok.test(e.name)) out.push(p);
++ }
++ return out;
++};
++const rel = (p) => path.relative(ROOT, p);
++
++// The pages (whole HTML files) and the code that draws them.
++const PAGE_ROOTS = [];
++const CODE_ROOTS = ["public","server"];
++const LAYOUTS = ["server/render/layout.js"]; // the server-rendered pages: their layout brings the navbar or the glyphs
++
++// A Font Awesome stylesheet, wherever it is written.
++const FA_CSS = /fontawesome[^"'\s]*\.css|font-awesome[^"'\s]*\.css|cdnjs\.cloudflare\.com\/ajax\/libs\/font-awesome/i;
++
++const pages = [];
++for (const r of PAGE_ROOTS) pages.push(...walk(path.join(ROOT, r), /\.html$/));
++for (const p of pages) {
++ const html = fs.readFileSync(p, 'utf8');
++ assert.ok(!/fontawesome|font-awesome/i.test(html), `${rel(p)} links Font Awesome`);
++ // A whole page (with a <head>; fragments are parts of a page) that names an icon loads what draws it.
++ if (/<head[\s>]/i.test(html) && /\bfa-[a-z]/.test(html) && !/\/shared\/navbar|ov-glyphs\.js/.test(html)) {
++ assert.fail(`${rel(p)} has fa- icons but loads neither the navbar nor /shared/ov-glyphs.js`);
++ }
++}
++
++// Nothing draws with Font Awesome's font.
++for (const r of CODE_ROOTS) {
++ for (const p of walk(path.join(ROOT, r), /\.css$/)) {
++ assert.ok(!/font-family:\s*["']?Font ?Awesome/i.test(fs.readFileSync(p, 'utf8')), `${rel(p)} draws with Font Awesome's font`);
++ }
++ for (const p of walk(path.join(ROOT, r), /\.(html|js|mjs|cjs)$/)) {
++ const t = fs.readFileSync(p, 'utf8');
++ assert.ok(!/['"]\d+px FontAwesome|Font Awesome 6 Free/.test(t), `${rel(p)} draws with Font Awesome's font`);
++ assert.ok(!FA_CSS.test(t), `${rel(p)} links Font Awesome`);
++ }
++}
++
++// The server-rendered pages get the glyphs from the Shared navbar (shell.page) or from the drop-in.
++for (const f of LAYOUTS) {
++ const t = fs.readFileSync(path.join(ROOT, f), 'utf8');
++ assert.ok(/shell\.page|ov-glyphs\.js|\/shared\/navbar/.test(t), `${f} renders pages without the navbar or /shared/ov-glyphs.js`);
++}
++
++// Every fa- name in the pages, the scripts and the server views is drawn by a glyph or a brand.
++const MOD = /^(solid|regular|light|thin|duotone|brands|sharp|classic|fw|lg|xl|xs|sm|2xs|2xl|\d+x|li|ul|border|inverse|pull-left|pull-right|rotate-\d+|rotate-by|flip-horizontal|flip-vertical|flip-both|width-auto|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|swap-opacity|stack-text)$/;
++const files = [];
++for (const r of CODE_ROOTS) files.push(...walk(path.join(ROOT, r), /\.(html|js)$/));
++const missing = new Map();
++for (const f of files) {
++ for (const m of fs.readFileSync(f, 'utf8').matchAll(/\bfa-([a-z0-9]+(?:-[a-z0-9]+)*)\b/g)) {
++ const n = m[1];
++ if (MOD.test(n) || glyphs.fromFa(n) || brands.fromFa(n)) continue;
++ if (!missing.has(n)) missing.set(n, rel(f));
++ }
++}
++assert.deepStrictEqual([...missing].map(([n, f]) => `fa-${n} (${f})`), [], 'every icon this site names is drawn by the OpenVibe glyphs or brands');
++
++console.log(`no font awesome: ${pages.length} pages, ${files.length} files, every fa- name drawn`);