Landing performance: flag font subset, responsive mascots, AVIF cache, HTML minify

- Flag font: ship a committed fontTools subset (43 KB, -45%) of
  TwemojiCountryFlags.woff2 with only the 15 flags the site uses, under a
  content-hashed name with a preload on every flag-bearing page. The build
  validates the subset against a manifest and fails when a new flag
  appears without regenerating (npm run subset-flags). JS subsetters
  (subset-font/harfbuzzjs) were rejected: their wasm builds silently drop
  the COLR/CPAL color tables, rendering all flags invisible.
- Responsive mascots: legal/join/404 pages served 434-500px sources for
  175-180px slots; the build now emits 180w/360w variants (AVIF 1x: ~7 KB
  vs ~22 KB) and the pages use srcset. Small logo spots (14-42px) now use
  the existing 64/128px variants instead of the 256px file.
- injectAvifPictures copies the img sizes attribute onto the AVIF
  <source>; without it Chrome mis-selects w-descriptor candidates and can
  drop the image entirely.
- AVIF conversion is cached by content hash (.avif-cache.json); the old
  mtime check never hit because copyDirSync refreshes mtimes, so every
  verify re-encoded all 26 files.
- HTML output is minified (comments + indentation, <pre> preserved,
  newlines kept for inline-script safety): index.html 121 KB -> 93 KB.
- unicode-range trimmed to U+1F1E6-1F1FF; tag-sequence flags fall through
  to the system emoji font.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
KoalaDev
2026-07-12 04:34:53 +02:00
parent 9fd4e870f3
commit af4ac943a9
17 changed files with 397 additions and 36 deletions
+1
View File
@@ -43,6 +43,7 @@ extension/shared/
# Auto-generated website build output # Auto-generated website build output
website/www/ website/www/
website/.avif-cache.json
# Temporary scratch files # Temporary scratch files
scratch/ scratch/
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "koalasync", "name": "koalasync",
"version": "2.5.3", "version": "2.5.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "koalasync", "name": "koalasync",
"version": "2.5.3", "version": "2.5.4",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.61.1", "@playwright/test": "^1.61.1",
"@vitest/coverage-v8": "^4.1.9", "@vitest/coverage-v8": "^4.1.9",
+1
View File
@@ -9,6 +9,7 @@
"indexnow": "node website/submit-indexnow.cjs", "indexnow": "node website/submit-indexnow.cjs",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"subset-flags": "node website/tools/subset-flag-font.mjs",
"test": "npm run verify", "test": "npm run verify",
"test:unit": "vitest run", "test:unit": "vitest run",
"verify": "node scripts/verify-release.mjs" "verify": "node scripts/verify-release.mjs"
+2 -2
View File
@@ -92,7 +92,7 @@
<nav> <nav>
<div class="container nav-content"> <div class="container nav-content">
<a href="/" class="logo-area" style="text-decoration: none;"> <a href="/" class="logo-area" style="text-decoration: none;">
<img src="/assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40"> <img src="/assets/NewLogoIcon_128.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span> <span>KoalaSync</span>
</a> </a>
<div class="nav-links"> <div class="nav-links">
@@ -111,7 +111,7 @@
<main class="legal-content"> <main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2.5rem 2rem; text-align: center;"> <div class="legal-card" data-reveal style="padding: 2.5rem 2rem; text-align: center;">
<div style="display: flex; justify-content: center;"> <div style="display: flex; justify-content: center;">
<img src="/assets/KoalaSearching.webp" alt="A cute koala looking through a telescope, searching for the missing page" class="legal-mascot" width="180" height="180"> <img src="/assets/KoalaSearching-180.webp" srcset="/assets/KoalaSearching-180.webp 180w, /assets/KoalaSearching-360.webp 360w" sizes="180px" alt="A cute koala looking through a telescope, searching for the missing page" class="legal-mascot" width="180" height="180">
</div> </div>
<h1 style="margin-bottom: 0.5rem;">404</h1> <h1 style="margin-bottom: 0.5rem;">404</h1>
<p style="text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 1.5rem;"> <p style="text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 1.5rem;">
@@ -0,0 +1,22 @@
{
"comment": "Generated by tools/subset-flag-font.mjs — do not edit. build.cjs fails when site flags and this manifest diverge.",
"flags": [
"BR",
"CN",
"DE",
"ES",
"FR",
"GB",
"IT",
"JP",
"KR",
"NL",
"PL",
"PT",
"RU",
"TR",
"UA"
],
"sourceFontSha256": "9f04f14429bb6a9f415c7a4dd902a918d7e81a4f7526c415496fdb063954e3b8",
"subsetSha256": "d6683cd924e343443effaa96008656e9914c168afad6cd8ac168af79e2b65811"
}
Binary file not shown.
+137 -14
View File
@@ -24,7 +24,72 @@ function minifyCSS(code) {
if (minifyCSS('.parent :is(.a, .b) { color: red; }') !== '.parent :is(.a,.b){color:red}') { if (minifyCSS('.parent :is(.a, .b) { color: red; }') !== '.parent :is(.a,.b){color:red}') {
throw new Error('CSS minifier must preserve descendant combinators before functional pseudo-classes'); throw new Error('CSS minifier must preserve descendant combinators before functional pseudo-classes');
} }
const MIN_AVIF_KB = 0; // HTML minifier: comments + indentation only. Newlines are preserved so
// inline scripts/JSON-LD stay valid; <pre> blocks keep their formatting.
function minifyHTML(html) {
return html.split(/(<pre\b[\s\S]*?<\/pre>)/).map((part, i) => {
if (i % 2 === 1) return part;
return part
.replace(/<!--(?!\[)[\s\S]*?-->/g, '')
.replace(/\n[ \t]+/g, '\n')
.replace(/\n{2,}/g, '\n');
}).join('');
}
// ── Country-flag font subset (committed artifact, validated here) ────────
// The subset itself is generated by tools/subset-flag-font.mjs (Python
// fontTools — the harfbuzz-based JS subsetters drop the COLR/CPAL color
// tables and produce invisible flags, so the build never subsets itself).
// This step verifies the committed subset still matches the site and ships
// it under a content-hashed name.
function stageFlagFontSubset(websiteDir, wwwDir) {
const { collectFlagSourceFiles, extractUsedFlags, flagToCountryCode } = require('./flag-font-utils.cjs');
const assetsDir = path.join(websiteDir, 'assets');
const subsetPath = path.join(assetsDir, 'TwemojiCountryFlags.subset.woff2');
const manifestPath = path.join(assetsDir, 'TwemojiCountryFlags.subset.json');
const regenerate = 'run "npm run subset-flags" and commit the result';
if (!fs.existsSync(subsetPath) || !fs.existsSync(manifestPath)) {
throw new Error(`Flag font subset or manifest missing ${regenerate}`);
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const subset = fs.readFileSync(subsetPath);
const sha256 = buf => crypto.createHash('sha256').update(buf).digest('hex');
if (sha256(subset) !== manifest.subsetSha256) {
throw new Error(`TwemojiCountryFlags.subset.woff2 does not match its manifest ${regenerate}`);
}
const sourceFont = fs.readFileSync(path.join(assetsDir, 'TwemojiCountryFlags.woff2'));
if (sha256(sourceFont) !== manifest.sourceFontSha256) {
throw new Error(`TwemojiCountryFlags.woff2 changed since the subset was generated ${regenerate}`);
}
const usedFlags = extractUsedFlags(collectFlagSourceFiles(websiteDir));
if (usedFlags.length === 0) {
throw new Error('Flag extraction found no flag emoji extraction is broken');
}
const usedCodes = usedFlags.map(flagToCountryCode);
const missing = usedCodes.filter(c => !manifest.flags.includes(c));
if (missing.length > 0) {
throw new Error(`Flags used on the site but missing from the font subset: ${missing.join(', ')} ${regenerate}`);
}
const extra = manifest.flags.filter(c => !usedCodes.includes(c));
if (extra.length > 0) {
console.warn(` ⚠️ Subset contains unused flags (${extra.join(', ')}) "npm run subset-flags" would shrink it.`);
}
const name = `TwemojiCountryFlags.${sha8(subset)}.woff2`;
const destAssets = path.join(wwwDir, 'assets');
fs.mkdirSync(destAssets, { recursive: true });
for (const f of fs.readdirSync(destAssets)) {
if (/^TwemojiCountryFlags\.[0-9a-f]{8}\.woff2$/.test(f) && f !== name) {
fs.unlinkSync(path.join(destAssets, f));
}
}
fs.writeFileSync(path.join(destAssets, name), subset);
const pct = ((1 - subset.length / sourceFont.length) * 100).toFixed(0);
console.log(` Flag font: ${name} (${(subset.length / 1024).toFixed(1)} KB, ${usedCodes.length} flags, -${pct}% vs unsubset)`);
return name;
}
function copyDirSync(src, dest) { function copyDirSync(src, dest) {
fs.mkdirSync(dest, { recursive: true }); fs.mkdirSync(dest, { recursive: true });
@@ -57,7 +122,12 @@ function injectAvifPictures(html) {
const srcsetMatch = attrs.match(/\bsrcset="([^"]*)"/i); const srcsetMatch = attrs.match(/\bsrcset="([^"]*)"/i);
if (srcsetMatch) { if (srcsetMatch) {
const avifSrcset = srcsetMatch[1].replace(/\.webp/gi, '.avif'); const avifSrcset = srcsetMatch[1].replace(/\.webp/gi, '.avif');
return `<picture><source srcset="${avifSrcset}" type="image/avif"><img${attrs}></picture>`; // A <source> with width descriptors requires its own sizes
// attribute; without it browsers may pick and even reject
// candidates (observed as broken images in Chrome).
const sizesMatch = attrs.match(/\bsizes="([^"]*)"/i);
const sizesAttr = sizesMatch ? ` sizes="${sizesMatch[1]}"` : '';
return `<picture><source srcset="${avifSrcset}"${sizesAttr} type="image/avif"><img${attrs}></picture>`;
} }
return `<picture><source srcset="${avifSrc}" type="image/avif"><img${attrs}></picture>`; return `<picture><source srcset="${avifSrc}" type="image/avif"><img${attrs}></picture>`;
}); });
@@ -136,8 +206,15 @@ async function compile() {
console.warn(` ⚠️ Warning: ${ogSrc} not found. Skipping og-image generation.`); console.warn(` ⚠️ Warning: ${ogSrc} not found. Skipping og-image generation.`);
} }
// ── 0.7 Stage the committed flag-font subset (~45% smaller) ──
console.log('Staging flag font subset...');
const flagFontName = stageFlagFontSubset(websiteDir, wwwDir);
// ── 1. Minify CSS/JS (must happen first so hashes go into HTML) ── // ── 1. Minify CSS/JS (must happen first so hashes go into HTML) ──
console.log('Minifying CSS/JS...'); console.log('Minifying CSS/JS...');
// CSS references the hashed subset instead of the raw font file
const readStyle = relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8')
.replaceAll('TwemojiCountryFlags.woff2', flagFontName);
const styleModules = [ const styleModules = [
'styles/foundation.css', 'styles/foundation.css',
'styles/hero.css', 'styles/hero.css',
@@ -170,17 +247,13 @@ async function compile() {
] ]
} }
]; ];
const styleRaw = styleModules const styleRaw = styleModules.map(readStyle).join('');
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
.join('');
const styleMin = minifyCSS(styleRaw); const styleMin = minifyCSS(styleRaw);
const styleHash = sha8(styleMin); const styleHash = sha8(styleMin);
const styleName = `style.${styleHash}.min.css`; const styleName = `style.${styleHash}.min.css`;
const styleSRI = sha384(styleMin); const styleSRI = sha384(styleMin);
const compiledLandingBundles = landingBundles.map(bundle => { const compiledLandingBundles = landingBundles.map(bundle => {
const raw = bundle.modules const raw = bundle.modules.map(readStyle).join('');
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
.join('');
const min = minifyCSS(raw); const min = minifyCSS(raw);
return { return {
...bundle, ...bundle,
@@ -476,6 +549,11 @@ async function compile() {
const destAssets = path.join(wwwDir, 'assets'); const destAssets = path.join(wwwDir, 'assets');
if (fs.existsSync(srcAssets)) { if (fs.existsSync(srcAssets)) {
copyDirSync(srcAssets, destAssets); copyDirSync(srcAssets, destAssets);
// Pages use the hashed subset staged in step 0.7; ship neither the raw
// font nor the unhashed subset artifacts
fs.rmSync(path.join(destAssets, 'TwemojiCountryFlags.woff2'), { force: true });
fs.rmSync(path.join(destAssets, 'TwemojiCountryFlags.subset.woff2'), { force: true });
fs.rmSync(path.join(destAssets, 'TwemojiCountryFlags.subset.json'), { force: true });
console.log(' Assets copied.'); console.log(' Assets copied.');
} }
@@ -487,20 +565,49 @@ async function compile() {
console.log(' ✓ Apple touch icons copied to www root.'); console.log(' ✓ Apple touch icons copied to www root.');
} }
// ── 6.5 Responsive variants for the static-page mascots ──
// These pages showed 434-500px sources at 180px CSS (2x need: 360px).
// Variants feed the AVIF step below and the srcset attributes in the
// static HTML files.
console.log('Generating responsive mascot variants...');
const RESPONSIVE_VARIANTS = [
{ src: 'KoalaImprintl.webp', widths: [180, 360] },
{ src: 'KoalaPrivacy.webp', widths: [180, 360] },
{ src: 'KoalaSearching.webp', widths: [180, 360] }
];
for (const variant of RESPONSIVE_VARIANTS) {
const variantSrc = path.join(destAssets, variant.src);
if (!fs.existsSync(variantSrc)) {
throw new Error(`Responsive variant source ${variant.src} not found in assets`);
}
for (const width of variant.widths) {
const out = variantSrc.replace(/\.webp$/, `-${width}.webp`);
await sharp(variantSrc).resize({ width }).toFile(out);
}
}
// ── 7. Convert all WebP to AVIF (quality 65) ── // ── 7. Convert all WebP to AVIF (quality 65) ──
// Content-hash cache: mtime comparison never hit because copyDirSync
// refreshes mtimes, so every verify re-encoded all AVIFs.
console.log('Converting WebP → AVIF...'); console.log('Converting WebP → AVIF...');
let avifCount = 0; const AVIF_OPTS = { quality: 65, speed: 4 };
const avifCachePath = path.join(websiteDir, '.avif-cache.json');
let avifCache = {};
try { avifCache = JSON.parse(fs.readFileSync(avifCachePath, 'utf8')); } catch { /* cold cache */ }
const avifCacheNext = {};
let avifCount = 0, avifCachedCount = 0;
const webpFiles = fs.readdirSync(destAssets).filter(f => f.endsWith('.webp')); const webpFiles = fs.readdirSync(destAssets).filter(f => f.endsWith('.webp'));
for (const f of webpFiles) { for (const f of webpFiles) {
const src = path.join(destAssets, f); const src = path.join(destAssets, f);
const stat = fs.statSync(src);
if (stat.size < MIN_AVIF_KB * 1024) continue;
const dest = path.join(destAssets, f.replace(/\.webp$/, '.avif')); const dest = path.join(destAssets, f.replace(/\.webp$/, '.avif'));
if (fs.existsSync(dest) && fs.statSync(dest).mtimeMs >= stat.mtimeMs) continue; const key = `${sha8(fs.readFileSync(src))}:q${AVIF_OPTS.quality}s${AVIF_OPTS.speed}`;
await sharp(src).avif({ quality: 65, speed: 4 }).toFile(dest); avifCacheNext[f] = key;
if (avifCache[f] === key && fs.existsSync(dest)) { avifCachedCount++; continue; }
await sharp(src).avif(AVIF_OPTS).toFile(dest);
avifCount++; avifCount++;
} }
console.log(` ${avifCount} AVIF files generated.`); fs.writeFileSync(avifCachePath, JSON.stringify(avifCacheNext, null, 2) + '\n');
console.log(` ${avifCount} AVIF files generated, ${avifCachedCount} unchanged (cache).`);
// ── 8. Post-process ALL HTML files ── // ── 8. Post-process ALL HTML files ──
console.log('Post-processing HTML...'); console.log('Post-processing HTML...');
@@ -540,6 +647,22 @@ async function compile() {
// 8c. Minify inline SVGs // 8c. Minify inline SVGs
html = minifyInlineSvgs(html); html = minifyInlineSvgs(html);
// 8d. Preload the subset flag font on pages that show flags. This
// cuts the HTML→CSS→font chain; unicode-range alone would delay the
// request until after CSS parse + layout.
if (/[\u{1F1E6}-\u{1F1FF}]{2}/u.test(html)) {
const relDir = path.relative(wwwDir, path.dirname(filePath));
// 404.html is served for arbitrary URLs and must use absolute paths
const prefix = path.basename(filePath) === '404.html'
? '/'
: (relDir ? '../'.repeat(relDir.split(path.sep).length) : '');
const preload = `<link rel="preload" as="font" type="font/woff2" href="${prefix}assets/${flagFontName}" crossorigin>`;
html = html.replace('</head>', `${preload}\n</head>`);
}
// 8e. Minify HTML (comments + indentation)
html = minifyHTML(html);
fs.writeFileSync(filePath, html); fs.writeFileSync(filePath, html);
}); });
+2 -2
View File
@@ -121,7 +121,7 @@
<nav> <nav>
<div class="container nav-content"> <div class="container nav-content">
<a href="./" class="logo-area" style="text-decoration: none;"> <a href="./" class="logo-area" style="text-decoration: none;">
<img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40"> <img src="../assets/NewLogoIcon_128.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span> <span>KoalaSync</span>
</a> </a>
<div class="nav-links"> <div class="nav-links">
@@ -161,7 +161,7 @@
<main class="legal-content"> <main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2rem;"> <div class="legal-card" data-reveal style="padding: 2rem;">
<div style="display: flex; justify-content: center;"> <div style="display: flex; justify-content: center;">
<img src="../assets/KoalaPrivacy.webp" alt="Niedlicher Koala, der den Datenschutz repräsentiert" class="legal-mascot" width="180" height="180"> <img src="../assets/KoalaPrivacy-180.webp" srcset="../assets/KoalaPrivacy-180.webp 180w, ../assets/KoalaPrivacy-360.webp 360w" sizes="180px" alt="Niedlicher Koala, der den Datenschutz repräsentiert" class="legal-mascot" width="180" height="180">
</div> </div>
<h1>Datenschutz</h1> <h1>Datenschutz</h1>
<p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;"> <p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
+48
View File
@@ -0,0 +1,48 @@
/**
* Shared helpers for the country-flag font subset.
* Used by build.cjs (validation) and tools/subset-flag-font.mjs (generation).
*
* Flags are regional-indicator pairs; tag-sequence flags (🏴 England etc.)
* are NOT extracted or subset — if one is ever added it will render via the
* system emoji font.
*/
const fs = require('fs');
const path = require('path');
const FLAG_PAIR_RE = /[\u{1F1E6}-\u{1F1FF}]{2}/gu;
function collectFlagSourceFiles(websiteDir) {
const files = [];
for (const entry of fs.readdirSync(websiteDir)) {
if (entry.endsWith('.html')) files.push(path.join(websiteDir, entry));
}
for (const dir of ['alternatives', 'locales']) {
const p = path.join(websiteDir, dir);
if (!fs.existsSync(p)) continue;
for (const entry of fs.readdirSync(p)) {
if (/\.(html|json)$/.test(entry)) files.push(path.join(p, entry));
}
}
return files;
}
function extractUsedFlags(files) {
const flags = new Set();
for (const file of files) {
for (const m of fs.readFileSync(file, 'utf8').matchAll(FLAG_PAIR_RE)) {
flags.add(m[0]);
}
}
return [...flags].sort();
}
// 🇩🇪 → "DE" (for readable manifests and tool output)
function flagToCountryCode(flag) {
return [...flag].map(c => String.fromCharCode(c.codePointAt(0) - 0x1F1E6 + 65)).join('');
}
function countryCodeToFlag(code) {
return [...code].map(c => String.fromCodePoint(0x1F1E6 + c.charCodeAt(0) - 65)).join('');
}
module.exports = { FLAG_PAIR_RE, collectFlagSourceFiles, extractUsedFlags, flagToCountryCode, countryCodeToFlag };
+2 -2
View File
@@ -121,7 +121,7 @@
<nav> <nav>
<div class="container nav-content"> <div class="container nav-content">
<a href="./" class="logo-area" style="text-decoration: none;"> <a href="./" class="logo-area" style="text-decoration: none;">
<img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40"> <img src="../assets/NewLogoIcon_128.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span> <span>KoalaSync</span>
</a> </a>
<div class="nav-links"> <div class="nav-links">
@@ -161,7 +161,7 @@
<main class="legal-content"> <main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2rem;"> <div class="legal-card" data-reveal style="padding: 2rem;">
<div style="display: flex; justify-content: center;"> <div style="display: flex; justify-content: center;">
<img src="../assets/KoalaImprintl.webp" alt="Niedlicher Koala, der das Impressum repräsentiert" class="legal-mascot" width="180" height="180"> <img src="../assets/KoalaImprintl-180.webp" srcset="../assets/KoalaImprintl-180.webp 180w, ../assets/KoalaImprintl-360.webp 360w" sizes="180px" alt="Niedlicher Koala, der das Impressum repräsentiert" class="legal-mascot" width="180" height="180">
</div> </div>
<h1>Impressum / Anbieterinformationen</h1> <h1>Impressum / Anbieterinformationen</h1>
<p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;"> <p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
+2 -2
View File
@@ -121,7 +121,7 @@
<nav> <nav>
<div class="container nav-content"> <div class="container nav-content">
<a href="./" class="logo-area" style="text-decoration: none;"> <a href="./" class="logo-area" style="text-decoration: none;">
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40"> <img src="assets/NewLogoIcon_128.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span> <span>KoalaSync</span>
</a> </a>
<div class="nav-links"> <div class="nav-links">
@@ -161,7 +161,7 @@
<main class="legal-content"> <main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2rem;"> <div class="legal-card" data-reveal style="padding: 2rem;">
<div style="display: flex; justify-content: center;"> <div style="display: flex; justify-content: center;">
<img src="assets/KoalaImprintl.webp" alt="Cute Koala representing the legal notice page" class="legal-mascot" width="180" height="180"> <img src="assets/KoalaImprintl-180.webp" srcset="assets/KoalaImprintl-180.webp 180w, assets/KoalaImprintl-360.webp 360w" sizes="180px" alt="Cute Koala representing the legal notice page" class="legal-mascot" width="180" height="180">
</div> </div>
<h1>Legal Notice</h1> <h1>Legal Notice</h1>
<p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;"> <p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
+3 -3
View File
@@ -106,7 +106,7 @@
<nav> <nav>
<div class="container nav-content"> <div class="container nav-content">
<a href="./" class="logo-area" style="text-decoration: none;"> <a href="./" class="logo-area" style="text-decoration: none;">
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="64" height="64"> <img src="assets/NewLogoIcon_128.webp" alt="KoalaSync Logo" width="64" height="64">
<span>KoalaSync</span> <span>KoalaSync</span>
</a> </a>
<div class="nav-links" id="primary-nav" role="navigation" aria-label="Primary"> <div class="nav-links" id="primary-nav" role="navigation" aria-label="Primary">
@@ -155,8 +155,8 @@
<div class="join-status-visual"> <div class="join-status-visual">
<div class="status-ring active-pulse" id="status-ring"></div> <div class="status-ring active-pulse" id="status-ring"></div>
<div id="join-status-icon" style="z-index: 2; transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); display: flex; align-items: center; justify-content: center;"> <div id="join-status-icon" style="z-index: 2; transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); display: flex; align-items: center; justify-content: center;">
<img src="assets/KoalaSearching.webp" alt="A cute koala looking through a telescope searching for your extension" class="join-status-mascot searching-mascot join-status-pulse" lang="en"> <img src="assets/KoalaSearching-180.webp" srcset="assets/KoalaSearching-180.webp 180w, assets/KoalaSearching-360.webp 360w" sizes="175px" alt="A cute koala looking through a telescope searching for your extension" class="join-status-mascot searching-mascot join-status-pulse" lang="en">
<img src="assets/KoalaSearching.webp" alt="Ein niedlicher Koala schaut durch ein Fernrohr und sucht nach deiner Erweiterung" class="join-status-mascot searching-mascot join-status-pulse" lang="de"> <img src="assets/KoalaSearching-180.webp" srcset="assets/KoalaSearching-180.webp 180w, assets/KoalaSearching-360.webp 360w" sizes="175px" alt="Ein niedlicher Koala schaut durch ein Fernrohr und sucht nach deiner Erweiterung" class="join-status-mascot searching-mascot join-status-pulse" lang="de">
</div> </div>
</div> </div>
+2 -2
View File
@@ -121,7 +121,7 @@
<nav> <nav>
<div class="container nav-content"> <div class="container nav-content">
<a href="./" class="logo-area" style="text-decoration: none;"> <a href="./" class="logo-area" style="text-decoration: none;">
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40"> <img src="assets/NewLogoIcon_128.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span> <span>KoalaSync</span>
</a> </a>
<div class="nav-links"> <div class="nav-links">
@@ -161,7 +161,7 @@
<main class="legal-content"> <main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2rem;"> <div class="legal-card" data-reveal style="padding: 2rem;">
<div style="display: flex; justify-content: center;"> <div style="display: flex; justify-content: center;">
<img src="assets/KoalaPrivacy.webp" alt="Cute Koala representing privacy and data security" class="legal-mascot" width="180" height="180"> <img src="assets/KoalaPrivacy-180.webp" srcset="assets/KoalaPrivacy-180.webp 180w, assets/KoalaPrivacy-360.webp 360w" sizes="180px" alt="Cute Koala representing privacy and data security" class="legal-mascot" width="180" height="180">
</div> </div>
<h1>Privacy Policy</h1> <h1>Privacy Policy</h1>
<p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;"> <p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
+4 -1
View File
@@ -2,7 +2,10 @@
@font-face { @font-face {
font-family: 'Twemoji Country Flags'; font-family: 'Twemoji Country Flags';
unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F; /* Regional-indicator pairs only: the build subsets this font to the
flags actually used. Tag-sequence flags (U+1F3F4 + tags) are not in
the subset and must fall through to the system emoji font. */
unicode-range: U+1F1E6-1F1FF;
src: url('assets/TwemojiCountryFlags.woff2') format('woff2'); src: url('assets/TwemojiCountryFlags.woff2') format('woff2');
font-display: swap; font-display: swap;
} }
+6 -6
View File
@@ -416,7 +416,7 @@
<span class="demo-tab-title"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="11" height="11" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg> <span class="demo-title-text-ep1">Stranger Things - S4E1</span></span> <span class="demo-tab-title"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="11" height="11" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg> <span class="demo-title-text-ep1">Stranger Things - S4E1</span></span>
<span class="demo-tab-user demo-user-a">🐱 ChillCat</span> <span class="demo-tab-user demo-user-a">🐱 ChillCat</span>
<button class="demo-ext-launcher" id="demo-launcher" type="button" aria-expanded="true" title="Open or close the KoalaSync popup"> <button class="demo-ext-launcher" id="demo-launcher" type="button" aria-expanded="true" title="Open or close the KoalaSync popup">
<img src="{{ASSET_PATH}}assets/NewLogoIcon.webp" alt="" width="16" height="16" loading="eager" decoding="async"> <img src="{{ASSET_PATH}}assets/NewLogoIcon_64.webp" alt="" width="16" height="16" loading="eager" decoding="async">
</button> </button>
</div> </div>
<div class="demo-video"> <div class="demo-video">
@@ -504,7 +504,7 @@
<span class="demo-tab-title"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="11" height="11" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg> <span class="demo-title-text-ep1">Stranger Things - S4E1</span></span> <span class="demo-tab-title"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="11" height="11" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg> <span class="demo-title-text-ep1">Stranger Things - S4E1</span></span>
<span class="demo-tab-user demo-user-b">🐶 HappyDog</span> <span class="demo-tab-user demo-user-b">🐶 HappyDog</span>
<span class="demo-ext-launcher demo-ext-static" aria-hidden="true" title="KoalaSync is installed here too"> <span class="demo-ext-launcher demo-ext-static" aria-hidden="true" title="KoalaSync is installed here too">
<img src="{{ASSET_PATH}}assets/NewLogoIcon.webp" alt="" width="16" height="16" loading="eager" decoding="async"> <img src="{{ASSET_PATH}}assets/NewLogoIcon_64.webp" alt="" width="16" height="16" loading="eager" decoding="async">
</span> </span>
</div> </div>
<div class="demo-video"> <div class="demo-video">
@@ -591,7 +591,7 @@
<div class="extension-mockup"> <div class="extension-mockup">
<div class="mock-header-row"> <div class="mock-header-row">
<div class="mock-header-title"><img src="{{ASSET_PATH}}assets/NewLogoIcon.webp" alt="KoalaSync Logo">KoalaSync</div> <div class="mock-header-title"><img src="{{ASSET_PATH}}assets/NewLogoIcon_64.webp" alt="KoalaSync Logo">KoalaSync</div>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" rel="noopener" class="mock-version-link" title="Visit GitHub Repository"> <a href="https://github.com/Shik3i/KoalaSync" target="_blank" rel="noopener" class="mock-version-link" title="Visit GitHub Repository">
<svg viewBox="0 0 16 16" fill="currentColor" style="width: 12px; height: 12px; display: block;"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg> <svg viewBox="0 0 16 16" fill="currentColor" style="width: 12px; height: 12px; display: block;"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
<span class="mockup-version">{{VERSION}}</span> <span class="mockup-version">{{VERSION}}</span>
@@ -611,7 +611,7 @@
<div id="mock-room" class="mock-screen active"> <div id="mock-room" class="mock-screen active">
<!-- Pre-room state, shown only during the scripted walkthrough --> <!-- Pre-room state, shown only during the scripted walkthrough -->
<div id="demo-room-empty" class="demo-room-empty"> <div id="demo-room-empty" class="demo-room-empty">
<img src="{{ASSET_PATH}}assets/NewLogoIcon.webp" alt="" width="42" height="42" loading="eager" decoding="async"> <img src="{{ASSET_PATH}}assets/NewLogoIcon_128.webp" alt="" width="42" height="42" loading="eager" decoding="async">
<div class="demo-room-empty-text">{{DEMO_NO_ROOM}}</div> <div class="demo-room-empty-text">{{DEMO_NO_ROOM}}</div>
<button id="demo-create-room" class="demo-create-room-btn" type="button">{{DEMO_CREATE_ROOM}}</button> <button id="demo-create-room" class="demo-create-room-btn" type="button">{{DEMO_CREATE_ROOM}}</button>
</div> </div>
@@ -1139,7 +1139,7 @@
<div class="illus-popup-card"> <div class="illus-popup-card">
<div class="illus-popup-header"> <div class="illus-popup-header">
<div class="illus-popup-brand"> <div class="illus-popup-brand">
<img src="{{ASSET_PATH}}assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="14" height="14"> <img src="{{ASSET_PATH}}assets/NewLogoIcon_64.webp" alt="KoalaSync Logo" width="14" height="14">
<span>KoalaSync</span> <span>KoalaSync</span>
</div> </div>
<div class="illus-popup-version mockup-version">{{VERSION}}</div> <div class="illus-popup-version mockup-version">{{VERSION}}</div>
@@ -1185,7 +1185,7 @@
<div class="illus-popup-card"> <div class="illus-popup-card">
<div class="illus-popup-header"> <div class="illus-popup-header">
<div class="illus-popup-brand"> <div class="illus-popup-brand">
<img src="{{ASSET_PATH}}assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="14" height="14"> <img src="{{ASSET_PATH}}assets/NewLogoIcon_64.webp" alt="KoalaSync Logo" width="14" height="14">
<span>KoalaSync</span> <span>KoalaSync</span>
</div> </div>
<div class="illus-popup-version mockup-version">{{VERSION}}</div> <div class="illus-popup-version mockup-version">{{VERSION}}</div>
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env node
/**
* Regenerate the committed country-flag font subset.
*
* npm run subset-flags (or: node website/tools/subset-flag-font.mjs)
*
* Extracts every flag emoji used in website HTML/locales, subsets
* assets/TwemojiCountryFlags.woff2 down to exactly those flags via Python
* fontTools (harfbuzz-based JS subsetters drop the COLR color tables and
* produce invisible flags), and writes:
*
* assets/TwemojiCountryFlags.subset.woff2 (committed)
* assets/TwemojiCountryFlags.subset.json (committed manifest)
*
* The website build (build.cjs) is pure Node: it only validates the
* manifest and fails when a flag was added without re-running this tool.
*
* Requires python3 with fonttools + brotli. Override the interpreter with
* the PYTHON env var, e.g. PYTHON=.venv/bin/python npm run subset-flags
*/
import { execFileSync } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const websiteDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const { collectFlagSourceFiles, extractUsedFlags, flagToCountryCode } = require(path.join(websiteDir, 'flag-font-utils.cjs'));
const sourceFont = path.join(websiteDir, 'assets', 'TwemojiCountryFlags.woff2');
const outputFont = path.join(websiteDir, 'assets', 'TwemojiCountryFlags.subset.woff2');
const manifestPath = path.join(websiteDir, 'assets', 'TwemojiCountryFlags.subset.json');
const python = process.env.PYTHON || 'python3';
try {
execFileSync(python, ['-c', 'import fontTools.subset, brotli'], { stdio: 'pipe' });
} catch {
console.error(`error: ${python} lacks fonttools/brotli.`);
console.error('Install them (e.g. "pip3 install fonttools brotli") or point PYTHON at an interpreter that has them:');
console.error(' PYTHON=/path/to/venv/bin/python npm run subset-flags');
process.exit(1);
}
const flags = extractUsedFlags(collectFlagSourceFiles(websiteDir));
if (flags.length === 0) {
console.error('error: no flag emoji found in website sources — extraction is broken');
process.exit(1);
}
const codes = flags.map(flagToCountryCode);
console.log(`Found ${codes.length} flags: ${codes.join(' ')}`);
execFileSync(python, [
path.join(websiteDir, 'tools', 'subset_flag_font.py'),
sourceFont,
outputFont,
codes.join(',')
], { stdio: 'inherit' });
const sha = buf => crypto.createHash('sha256').update(buf).digest('hex');
const manifest = {
comment: 'Generated by tools/subset-flag-font.mjs — do not edit. build.cjs fails when site flags and this manifest diverge.',
flags: codes,
sourceFontSha256: sha(fs.readFileSync(sourceFont)),
subsetSha256: sha(fs.readFileSync(outputFont))
};
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
const before = fs.statSync(sourceFont).size;
const after = fs.statSync(outputFont).size;
console.log(`Wrote ${path.relative(process.cwd(), outputFont)} (${(after / 1024).toFixed(1)} KB, -${((1 - after / before) * 100).toFixed(0)}% vs source) + manifest.`);
console.log('Commit both files.');
+90
View File
@@ -0,0 +1,90 @@
"""Subset TwemojiCountryFlags.woff2 to the given country flags.
Invoked by subset-flag-font.mjs — do not run directly unless debugging:
python3 subset_flag_font.py <source.woff2> <output.woff2> DE,FR,GB,...
Requires: fonttools, brotli (pip install fonttools brotli).
harfbuzz-based JS subsetters (subset-font/harfbuzzjs) silently drop the
COLR/CPAL color tables of this font, producing invisible flags — fontTools
subsets them correctly. Each requested flag is validated to have a GSUB
ligature AND COLR color layers so a broken subset can never be emitted.
"""
import io
import sys
from fontTools import subset
from fontTools.ttLib import TTFont
def flag_codepoints(code):
return [0x1F1E6 + ord(ch) - ord("A") for ch in code]
def ligature_glyph(font, first, second):
"""Return the GSUB ligature glyph for a regional-indicator pair."""
gsub = font["GSUB"].table
for lookup in gsub.LookupList.Lookup:
subtables = []
if lookup.LookupType == 4:
subtables = lookup.SubTable
elif lookup.LookupType == 7: # extension
subtables = [st.ExtSubTable for st in lookup.SubTable if st.ExtensionLookupType == 4]
for st in subtables:
for lig in st.ligatures.get(first, []):
if lig.Component == [second]:
return lig.LigGlyph
return None
def main():
src_path, out_path, codes_arg = sys.argv[1], sys.argv[2], sys.argv[3]
codes = sorted(codes_arg.split(","))
font = TTFont(src_path)
if "COLR" not in font or "CPAL" not in font:
sys.exit("source font has no COLR/CPAL tables")
text = "".join(chr(cp) for code in codes for cp in flag_codepoints(code))
options = subset.Options()
options.flavor = "woff2"
subsetter = subset.Subsetter(options=options)
subsetter.populate(text=text)
subsetter.subset(font)
buf = io.BytesIO()
font.save(buf)
result = TTFont(io.BytesIO(buf.getvalue()))
for table in ("COLR", "CPAL", "GSUB", "cmap"):
if table not in result:
sys.exit(f"subset lost the {table} table")
cmap = result.getBestCmap()
glyph_name = {}
for code in codes:
for cp in flag_codepoints(code):
if cp not in cmap:
sys.exit(f"flag {code}: regional indicator U+{cp:X} missing from subset cmap")
first, second = (cmap[cp] for cp in flag_codepoints(code))
lig = ligature_glyph(result, first, second)
if lig is None:
sys.exit(f"flag {code}: no GSUB ligature in subset — font has no artwork for it")
glyph_name[code] = lig
colr = result["COLR"]
if colr.version == 0:
for code, lig in glyph_name.items():
if not colr.ColorLayers.get(lig):
sys.exit(f"flag {code}: ligature glyph {lig} has no COLR color layers")
# COLR v1 would need different traversal; Twemoji country flags is v0
elif colr.version != 0:
sys.exit(f"unexpected COLR version {colr.version} — validation only supports v0")
with open(out_path, "wb") as f:
f.write(buf.getvalue())
print(f"subset OK: {len(codes)} flags, {len(buf.getvalue())} bytes")
if __name__ == "__main__":
main()