Restore landing CSS cascade stability

This commit is contained in:
KoalaDev
2026-07-11 16:11:31 +02:00
parent 72d4f04526
commit 7614534818
6 changed files with 56 additions and 90 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ The website is 100% static HTML, CSS, and JavaScript.
- **Static i18n compiler**: `build.cjs` combines `template.html` with dictionaries in `locales/`.
- **Build-time minification**: Source CSS/JS stays readable; generated output uses `.min.css` and `.min.js`.
- **Strangler CSS architecture**: `styles/*.css` supplies page/runtime bundles; `style.legacy.css` is the byte-identical, unloaded reference monolith. The landing ships a critical shell, a desktop/on-demand demo bundle, and idle-loaded below-fold styles. Legal, join, and alternatives rules stay out of landing bundles.
- **Strangler CSS architecture**: `styles/*.css` supplies page-specific production bundles; `style.legacy.css` is the byte-identical, unloaded reference monolith. The landing bundle preserves the legacy cascade while excluding Join- and Alternatives-only rules. Structural CSS is not activated after paint because that causes layout shifts.
- **Zero backend**: The compiled site can be hosted by any static file server.
- **Zero external assets**: Fonts, icons, scripts, and images must remain self-hosted.
- **Generated SEO/runtime files**: `version.json`, sitemap, robots, clean URLs, localized pages, and minified assets are copied or generated into `www/`.
-23
View File
@@ -1,27 +1,6 @@
// KoalaSync Landing Page Logic
document.addEventListener('DOMContentLoaded', () => {
const deferredCss = document.body.dataset.deferredCss;
if (deferredCss) {
const loadDeferredCss = () => {
if (document.querySelector('link[data-landing-deferred]')) return;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = deferredCss;
link.dataset.landingDeferred = '';
const integrity = document.body.dataset.deferredCssIntegrity;
if (integrity) {
link.integrity = integrity;
link.crossOrigin = 'anonymous';
}
document.head.appendChild(link);
};
if ('requestIdleCallback' in window) {
window.requestIdleCallback(loadDeferredCss, { timeout: 1500 });
} else {
window.setTimeout(loadDeferredCss, 250);
}
}
// Mockup Video Title Randomization on Load
const SERIES_NAMES = [
'Stranger Things',
@@ -1332,8 +1311,6 @@ document.addEventListener('DOMContentLoaded', () => {
let previousFocus = null;
const openModal = () => {
const demoStyles = document.querySelector('link[data-landing-demo]');
if (demoStyles) demoStyles.media = 'all';
previousFocus = document.activeElement && typeof document.activeElement.focus === 'function'
? document.activeElement
: null;
+42 -50
View File
@@ -145,23 +145,24 @@ async function compile() {
'styles/alternatives.css',
'styles/demo.css'
];
// Landing CSS is split by actual render dependency, not by the historical
// section comments in style.legacy.css. The initial bundle contains every
// selector used by the background, navigation, hero, interactive mockup,
// responsive shell, and accessibility states. Below-fold sections load
// after first paint from app.js. Utility-page rules never reach the landing.
const landingCriticalModules = [
'styles/foundation.css',
'styles/hero.css',
'styles/legal.css',
'styles/landing-controls.css',
'styles/landing-demo-gate.css'
];
const landingDemoModules = ['styles/demo.css'];
const landingDeferredModules = [
'styles/landing-primary.css',
'styles/landing-sections.css',
'styles/landing-faq.css'
// Keep the landing cascade equivalent to styleModules while excluding
// utility-page-only rules. A single blocking file avoids both request
// fan-out and layout shifts from activating structural CSS after paint.
const landingBundles = [
{
key: 'landing',
modules: [
'styles/foundation.css',
'styles/hero.css',
'styles/landing-primary.css',
'styles/legal.css',
'styles/landing-controls.css',
'styles/landing-sections.css',
'styles/landing-selfhost.css',
'styles/landing-faq.css',
'styles/demo.css'
]
}
];
const styleRaw = styleModules
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
@@ -170,24 +171,18 @@ async function compile() {
const styleHash = sha8(styleMin);
const styleName = `style.${styleHash}.min.css`;
const styleSRI = sha384(styleMin);
const landingStyleRaw = landingCriticalModules
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
.join('');
const landingStyleMin = minifyCSS(landingStyleRaw);
const landingStyleName = `landing.${sha8(landingStyleMin)}.min.css`;
const landingStyleSRI = sha384(landingStyleMin);
const landingDemoRaw = landingDemoModules
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
.join('');
const landingDemoMin = minifyCSS(landingDemoRaw);
const landingDemoName = `landing-demo.${sha8(landingDemoMin)}.min.css`;
const landingDemoSRI = sha384(landingDemoMin);
const landingDeferredRaw = landingDeferredModules
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
.join('');
const landingDeferredMin = minifyCSS(landingDeferredRaw);
const landingDeferredName = `landing-deferred.${sha8(landingDeferredMin)}.min.css`;
const landingDeferredSRI = sha384(landingDeferredMin);
const compiledLandingBundles = landingBundles.map(bundle => {
const raw = bundle.modules
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
.join('');
const min = minifyCSS(raw);
return {
...bundle,
min,
name: `${bundle.key === 'landing' ? 'landing' : `landing-${bundle.key}`}.${sha8(min)}.min.css`,
sri: sha384(min)
};
});
const appRaw = fs.readFileSync(path.join(websiteDir, 'app.js'), 'utf8');
const appMin = await minifyJS(appRaw);
@@ -205,9 +200,9 @@ async function compile() {
const appPct = ((1 - appMin.length / appRaw.length) * 100).toFixed(0);
const langPct = ((1 - langMin.length / langRaw.length) * 100).toFixed(0);
console.log(` CSS: ${styleName} (${(styleMin.length/1024).toFixed(1)} KB, -${stylePct}%)`);
console.log(` Landing CSS: ${landingStyleName} (${(landingStyleMin.length/1024).toFixed(1)} KB)`);
console.log(` Desktop/modal demo CSS: ${landingDemoName} (${(landingDemoMin.length/1024).toFixed(1)} KB)`);
console.log(` Deferred landing CSS: ${landingDeferredName} (${(landingDeferredMin.length/1024).toFixed(1)} KB)`);
for (const bundle of compiledLandingBundles) {
console.log(` Landing ${bundle.key}: ${bundle.name} (${(bundle.min.length/1024).toFixed(1)} KB)`);
}
console.log(` App: ${appName} (${(appMin.length/1024).toFixed(1)} KB, -${appPct}%)`);
console.log(` Lang: ${langName} (${(langMin.length/1024).toFixed(1)} KB, -${langPct}%)`);
@@ -221,9 +216,9 @@ async function compile() {
// Write minified files
fs.writeFileSync(path.join(wwwDir, styleName), styleMin);
fs.writeFileSync(path.join(wwwDir, landingStyleName), landingStyleMin);
fs.writeFileSync(path.join(wwwDir, landingDemoName), landingDemoMin);
fs.writeFileSync(path.join(wwwDir, landingDeferredName), landingDeferredMin);
for (const bundle of compiledLandingBundles) {
fs.writeFileSync(path.join(wwwDir, bundle.name), bundle.min);
}
fs.writeFileSync(path.join(wwwDir, appName), appMin);
fs.writeFileSync(path.join(wwwDir, langName), langMin);
@@ -520,15 +515,12 @@ async function compile() {
html = html.replace(/(<link\b[^>]*?\bhref=")((?:\.\.\/)*\/?)style\.min\.css"/g, (m, before, prefix) => {
return `${before}${prefix}${styleName}" integrity="${styleSRI}" crossorigin="anonymous"`;
});
html = html.replace(/(<link\b[^>]*?\bhref=")((?:\.\.\/)*\/?)landing\.min\.css"/g, (m, before, prefix) => {
return `${before}${prefix}${landingStyleName}" integrity="${landingStyleSRI}" crossorigin="anonymous"`;
});
html = html.replace(/(<link\b[^>]*?\bhref=")((?:\.\.\/)*\/?)landing-demo\.min\.css"/g, (m, before, prefix) => {
return `${before}${prefix}${landingDemoName}" integrity="${landingDemoSRI}" crossorigin="anonymous"`;
});
html = html.replace(/data-deferred-css="((?:\.\.\/)*\/?)landing-deferred\.min\.css"/g, (m, prefix) => {
return `data-deferred-css="${prefix}${landingDeferredName}" data-deferred-css-integrity="${landingDeferredSRI}"`;
});
for (const bundle of compiledLandingBundles) {
const placeholder = bundle.key === 'landing' ? 'landing.min.css' : `landing-${bundle.key}.min.css`;
html = html.replace(new RegExp(`(<link\\b[^>]*?\\bhref=")((?:\\.\\.\\/)*\\/?)${placeholder.replaceAll('.', '\\.') }"`, 'g'), (m, before, prefix) => {
return `${before}${prefix}${bundle.name}" integrity="${bundle.sri}" crossorigin="anonymous"`;
});
}
html = html.replace(/(<script\b[^>]*?\bsrc=")((?:\.\.\/)*\/?)app\.min\.js"/g, (m, before, prefix) => {
return `${before}${prefix}${appName}" integrity="${appSRI}" crossorigin="anonymous"`;
});
-14
View File
@@ -1,14 +0,0 @@
/* Initial mobile shell for the deferred interactive hero demo. */
@media (max-width: 900px) {
.hero-grid > .hero-mockup-wrapper {
display: none !important;
}
.hero-github-cta {
display: none;
}
.btn-demo-mobile {
display: inline-flex;
}
}
+12
View File
@@ -0,0 +1,12 @@
.selfhost-mascot {
display: block;
width: 300px;
height: auto;
margin: 0 auto;
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.15));
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.selfhost-mascot:hover {
transform: scale(1.05) translateY(-4px) rotate(-1deg);
}
+1 -2
View File
@@ -11,7 +11,6 @@
<link rel="dns-prefetch" href="https://github.com">
<link rel="stylesheet" href="{{ASSET_PATH}}landing.min.css">
<link rel="stylesheet" href="{{ASSET_PATH}}landing-demo.min.css" media="(min-width: 769px)" data-landing-demo>
<link rel="preload" as="image" href="{{ASSET_PATH}}assets/LookDownKoala.avif" imagesrcset="{{ASSET_PATH}}assets/LookDownKoala-1x.avif 90w, {{ASSET_PATH}}assets/LookDownKoala.avif 205w" imagesizes="90px" type="image/avif" fetchpriority="high">
<link rel="icon" type="image/png" sizes="16x16" href="{{ASSET_PATH}}assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ASSET_PATH}}assets/favicon-32x32.png">
@@ -209,7 +208,7 @@
</style>
</noscript>
</head>
<body data-deferred-css="{{ASSET_PATH}}landing-deferred.min.css">
<body>
<div class="scroll-progress-bar"></div>
<a class="skip-link" href="#main-content">Skip to main content</a>