mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
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:
@@ -43,6 +43,7 @@ extension/shared/
|
||||
|
||||
# Auto-generated website build output
|
||||
website/www/
|
||||
website/.avif-cache.json
|
||||
|
||||
# Temporary scratch files
|
||||
scratch/
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "koalasync",
|
||||
"version": "2.5.3",
|
||||
"version": "2.5.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "koalasync",
|
||||
"version": "2.5.3",
|
||||
"version": "2.5.4",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@vitest/coverage-v8": "^4.1.9",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"indexnow": "node website/submit-indexnow.cjs",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"subset-flags": "node website/tools/subset-flag-font.mjs",
|
||||
"test": "npm run verify",
|
||||
"test:unit": "vitest run",
|
||||
"verify": "node scripts/verify-release.mjs"
|
||||
|
||||
+2
-2
@@ -92,7 +92,7 @@
|
||||
<nav>
|
||||
<div class="container nav-content">
|
||||
<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>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
@@ -111,7 +111,7 @@
|
||||
<main class="legal-content">
|
||||
<div class="legal-card" data-reveal style="padding: 2.5rem 2rem; text-align: 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>
|
||||
<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;">
|
||||
|
||||
@@ -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
@@ -24,7 +24,72 @@ function minifyCSS(code) {
|
||||
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');
|
||||
}
|
||||
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) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
@@ -57,7 +122,12 @@ function injectAvifPictures(html) {
|
||||
const srcsetMatch = attrs.match(/\bsrcset="([^"]*)"/i);
|
||||
if (srcsetMatch) {
|
||||
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>`;
|
||||
});
|
||||
@@ -136,8 +206,15 @@ async function compile() {
|
||||
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) ──
|
||||
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 = [
|
||||
'styles/foundation.css',
|
||||
'styles/hero.css',
|
||||
@@ -170,17 +247,13 @@ async function compile() {
|
||||
]
|
||||
}
|
||||
];
|
||||
const styleRaw = styleModules
|
||||
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
|
||||
.join('');
|
||||
const styleRaw = styleModules.map(readStyle).join('');
|
||||
const styleMin = minifyCSS(styleRaw);
|
||||
const styleHash = sha8(styleMin);
|
||||
const styleName = `style.${styleHash}.min.css`;
|
||||
const styleSRI = sha384(styleMin);
|
||||
const compiledLandingBundles = landingBundles.map(bundle => {
|
||||
const raw = bundle.modules
|
||||
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
|
||||
.join('');
|
||||
const raw = bundle.modules.map(readStyle).join('');
|
||||
const min = minifyCSS(raw);
|
||||
return {
|
||||
...bundle,
|
||||
@@ -476,6 +549,11 @@ async function compile() {
|
||||
const destAssets = path.join(wwwDir, 'assets');
|
||||
if (fs.existsSync(srcAssets)) {
|
||||
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.');
|
||||
}
|
||||
|
||||
@@ -487,20 +565,49 @@ async function compile() {
|
||||
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) ──
|
||||
// Content-hash cache: mtime comparison never hit because copyDirSync
|
||||
// refreshes mtimes, so every verify re-encoded all AVIFs.
|
||||
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'));
|
||||
for (const f of webpFiles) {
|
||||
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'));
|
||||
if (fs.existsSync(dest) && fs.statSync(dest).mtimeMs >= stat.mtimeMs) continue;
|
||||
await sharp(src).avif({ quality: 65, speed: 4 }).toFile(dest);
|
||||
const key = `${sha8(fs.readFileSync(src))}:q${AVIF_OPTS.quality}s${AVIF_OPTS.speed}`;
|
||||
avifCacheNext[f] = key;
|
||||
if (avifCache[f] === key && fs.existsSync(dest)) { avifCachedCount++; continue; }
|
||||
await sharp(src).avif(AVIF_OPTS).toFile(dest);
|
||||
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 ──
|
||||
console.log('Post-processing HTML...');
|
||||
@@ -540,6 +647,22 @@ async function compile() {
|
||||
// 8c. Minify inline SVGs
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
<nav>
|
||||
<div class="container nav-content">
|
||||
<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>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
@@ -161,7 +161,7 @@
|
||||
<main class="legal-content">
|
||||
<div class="legal-card" data-reveal style="padding: 2rem;">
|
||||
<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>
|
||||
<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;">
|
||||
|
||||
@@ -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 };
|
||||
@@ -121,7 +121,7 @@
|
||||
<nav>
|
||||
<div class="container nav-content">
|
||||
<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>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
@@ -161,7 +161,7 @@
|
||||
<main class="legal-content">
|
||||
<div class="legal-card" data-reveal style="padding: 2rem;">
|
||||
<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>
|
||||
<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;">
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
<nav>
|
||||
<div class="container nav-content">
|
||||
<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>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
@@ -161,7 +161,7 @@
|
||||
<main class="legal-content">
|
||||
<div class="legal-card" data-reveal style="padding: 2rem;">
|
||||
<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>
|
||||
<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;">
|
||||
|
||||
+3
-3
@@ -106,7 +106,7 @@
|
||||
<nav>
|
||||
<div class="container nav-content">
|
||||
<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>
|
||||
</a>
|
||||
<div class="nav-links" id="primary-nav" role="navigation" aria-label="Primary">
|
||||
@@ -155,8 +155,8 @@
|
||||
<div class="join-status-visual">
|
||||
<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;">
|
||||
<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.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="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="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>
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
<nav>
|
||||
<div class="container nav-content">
|
||||
<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>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
@@ -161,7 +161,7 @@
|
||||
<main class="legal-content">
|
||||
<div class="legal-card" data-reveal style="padding: 2rem;">
|
||||
<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>
|
||||
<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;">
|
||||
|
||||
Vendored
+4
-1
@@ -2,7 +2,10 @@
|
||||
|
||||
@font-face {
|
||||
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');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@@ -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-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">
|
||||
<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>
|
||||
</div>
|
||||
<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-user demo-user-b">🐶 HappyDog</span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="demo-video">
|
||||
@@ -591,7 +591,7 @@
|
||||
|
||||
<div class="extension-mockup">
|
||||
<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">
|
||||
<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>
|
||||
@@ -611,7 +611,7 @@
|
||||
<div id="mock-room" class="mock-screen active">
|
||||
<!-- Pre-room state, shown only during the scripted walkthrough -->
|
||||
<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>
|
||||
<button id="demo-create-room" class="demo-create-room-btn" type="button">{{DEMO_CREATE_ROOM}}</button>
|
||||
</div>
|
||||
@@ -1139,7 +1139,7 @@
|
||||
<div class="illus-popup-card">
|
||||
<div class="illus-popup-header">
|
||||
<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>
|
||||
</div>
|
||||
<div class="illus-popup-version mockup-version">{{VERSION}}</div>
|
||||
@@ -1185,7 +1185,7 @@
|
||||
<div class="illus-popup-card">
|
||||
<div class="illus-popup-header">
|
||||
<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>
|
||||
</div>
|
||||
<div class="illus-popup-version mockup-version">{{VERSION}}</div>
|
||||
|
||||
@@ -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.');
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user