openvibers/openvibe.network Commits
First paint never waits on the Network: inline dark canvas + cached-theme boot in the shared head tags, theme script deferred
Changes
@@@ -64,11 +64,18 @@ function manifest({ site = 'network', name, shortName, description, startUrl = '};}++// First paint must never depend on another server. These two inline tags give every page the network's++// dark canvas and the visitor's cached theme immediately; theme-loader.js (deferred, from the Network)++// reconciles with the account afterwards. If the Network is slow or restarting, the page is still++// styled and readable instead of blank white.++const CRITICAL = `<style id="ov-critical">html{background:${BG};color:#e6edf7;color-scheme:dark}</style>`++ + `<script>(function(){try{var r=localStorage.getItem('ov_theme');if(!r)return;var t=JSON.parse(r),v=t&&t.variables;if(!v)return;var e=document.documentElement;for(var k in v)if(k.charAt(0)==='-')e.style.setProperty(k,v[k]);if(v['--bg-primary'])e.style.background=v['--bg-primary'];if(v['--text-primary'])e.style.color=v['--text-primary'];if(v['--color-scheme']==='light'||v['--color-scheme']==='dark')e.style.colorScheme=v['--color-scheme'];if(t.id)e.setAttribute('data-theme',t.id)}catch(_){}})();</script>`;++/** <head> tags every site shares: SVG favicon (inline, no request), theme colour, and the PNG touch icon when the site ships one. */function headTags({ site = 'network', iconBase = null } = {}) {const uri = 'data:image/svg+xml,' + encodeURIComponent(favicon({ site })).replace(/'/g, '%27');-- return [`<link rel="icon" type="image/svg+xml" data-ov-icon="${String(site).replace(/[^a-z]/g, '')}" href="${uri}">`, '<meta name="theme-color" content="#0d131d">', `<meta name="color-scheme" content="dark light">`,++ return [`<link rel="icon" type="image/svg+xml" data-ov-icon="${String(site).replace(/[^a-z]/g, '')}" href="${uri}">`, '<meta name="theme-color" content="#0d131d">', '<meta name="color-scheme" content="dark">', CRITICAL,iconBase ? `<link rel="apple-touch-icon" href="${iconBase}/logo-192.png">` : '', iconBase ? `<link rel="manifest" href="/manifest.webmanifest">` : ''].filter(Boolean).join('\n');}--module.exports = { svg, favicon, manifest, headTags, SITES, BG };++module.exports = { svg, favicon, manifest, headTags, CRITICAL, SITES, BG };@@@ -179,7 +179,7 @@ function page(kind, site) {return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${head}${require('./app-icon').headTags({ site: site.id })}--<script src="https://openvibe.network/shared/theme-loader.js"></script><style>${CSS}</style></head><body>++<script src="https://openvibe.network/shared/theme-loader.js" defer></script><style>${CSS}</style></head><body><noscript><p style="padding:12px 20px;margin:0"><a href="/">${esc(site.name)}</a></p></noscript><main>${article(kind, site)}</main><div id="ov-footer"></div><script src="https://openvibe.network/shared/navbar.js" defer></script><script src="https://openvibe.network/shared/footer.js" defer></script>@@@ -108,6 +108,9 @@// Native controls, scrollbars and form fields follow the theme's light/dark mode.const scheme = vars['--color-scheme'];if (scheme === 'light' || scheme === 'dark') { el.style.colorScheme = scheme; el.setAttribute('data-theme-mode', scheme); }++ // The inline critical style paints a fixed dark canvas before any stylesheet; keep it in step with the theme.++ if (vars['--bg-primary']) el.style.background = vars['--bg-primary'];++ if (vars['--text-primary']) el.style.color = vars['--text-primary'];syncChrome();}
@@ -64,11 +64,18 @@ function manifest({ site = 'network', name, shortName, description, startUrl = '
@@ -64,11 +64,18 @@ function manifest({ site = 'network', name, shortName, description, startUrl = '
};
};
}
}
+// First paint must never depend on another server. These two inline tags give every page the network's
+// dark canvas and the visitor's cached theme immediately; theme-loader.js (deferred, from the Network)
+// reconciles with the account afterwards. If the Network is slow or restarting, the page is still
+// styled and readable instead of blank white.
+const CRITICAL = `<style id="ov-critical">html{background:${BG};color:#e6edf7;color-scheme:dark}</style>`
+ + `<script>(function(){try{var r=localStorage.getItem('ov_theme');if(!r)return;var t=JSON.parse(r),v=t&&t.variables;if(!v)return;var e=document.documentElement;for(var k in v)if(k.charAt(0)==='-')e.style.setProperty(k,v[k]);if(v['--bg-primary'])e.style.background=v['--bg-primary'];if(v['--text-primary'])e.style.color=v['--text-primary'];if(v['--color-scheme']==='light'||v['--color-scheme']==='dark')e.style.colorScheme=v['--color-scheme'];if(t.id)e.setAttribute('data-theme',t.id)}catch(_){}})();</script>`;
+
/** <head> tags every site shares: SVG favicon (inline, no request), theme colour, and the PNG touch icon when the site ships one. */
/** <head> tags every site shares: SVG favicon (inline, no request), theme colour, and the PNG touch icon when the site ships one. */
function headTags({ site = 'network', iconBase = null } = {}) {
function headTags({ site = 'network', iconBase = null } = {}) {
const uri = 'data:image/svg+xml,' + encodeURIComponent(favicon({ site })).replace(/'/g, '%27');
const uri = 'data:image/svg+xml,' + encodeURIComponent(favicon({ site })).replace(/'/g, '%27');
- return [`<link rel="icon" type="image/svg+xml" data-ov-icon="${String(site).replace(/[^a-z]/g, '')}" href="${uri}">`, '<meta name="theme-color" content="#0d131d">', `<meta name="color-scheme" content="dark light">`,
+ return [`<link rel="icon" type="image/svg+xml" data-ov-icon="${String(site).replace(/[^a-z]/g, '')}" href="${uri}">`, '<meta name="theme-color" content="#0d131d">', '<meta name="color-scheme" content="dark">', CRITICAL,
iconBase ? `<link rel="apple-touch-icon" href="${iconBase}/logo-192.png">` : '', iconBase ? `<link rel="manifest" href="/manifest.webmanifest">` : ''].filter(Boolean).join('\n');
iconBase ? `<link rel="apple-touch-icon" href="${iconBase}/logo-192.png">` : '', iconBase ? `<link rel="manifest" href="/manifest.webmanifest">` : ''].filter(Boolean).join('\n');
}
}
-module.exports = { svg, favicon, manifest, headTags, SITES, BG };
+module.exports = { svg, favicon, manifest, headTags, CRITICAL, SITES, BG };
@@ -179,7 +179,7 @@ function page(kind, site) {
@@ -179,7 +179,7 @@ function page(kind, site) {
return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
${head}
${head}
${require('./app-icon').headTags({ site: site.id })}
${require('./app-icon').headTags({ site: site.id })}
-<script src="https://openvibe.network/shared/theme-loader.js"></script><style>${CSS}</style></head><body>
+<script src="https://openvibe.network/shared/theme-loader.js" defer></script><style>${CSS}</style></head><body>
<noscript><p style="padding:12px 20px;margin:0"><a href="/">${esc(site.name)}</a></p></noscript>
<noscript><p style="padding:12px 20px;margin:0"><a href="/">${esc(site.name)}</a></p></noscript>
<main>${article(kind, site)}</main><div id="ov-footer"></div>
<main>${article(kind, site)}</main><div id="ov-footer"></div>
<script src="https://openvibe.network/shared/navbar.js" defer></script><script src="https://openvibe.network/shared/footer.js" defer></script>
<script src="https://openvibe.network/shared/navbar.js" defer></script><script src="https://openvibe.network/shared/footer.js" defer></script>
@@ -108,6 +108,9 @@
@@ -108,6 +108,9 @@
// Native controls, scrollbars and form fields follow the theme's light/dark mode.
// Native controls, scrollbars and form fields follow the theme's light/dark mode.
const scheme = vars['--color-scheme'];
const scheme = vars['--color-scheme'];
if (scheme === 'light' || scheme === 'dark') { el.style.colorScheme = scheme; el.setAttribute('data-theme-mode', scheme); }
if (scheme === 'light' || scheme === 'dark') { el.style.colorScheme = scheme; el.setAttribute('data-theme-mode', scheme); }
+ // The inline critical style paints a fixed dark canvas before any stylesheet; keep it in step with the theme.
+ if (vars['--bg-primary']) el.style.background = vars['--bg-primary'];
+ if (vars['--text-primary']) el.style.color = vars['--text-primary'];
syncChrome();
syncChrome();
}
}