Python 33.5%
JavaScript 30.1%
TypeScript 25%
CSS 10%
Shell 1.4%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// ka-ui/gen-assets.mjs — génère pour chaque site : og.png (1200×630, partage3// social « Nom — par Groupe KA »), favicon.svg et apple-touch-icon.png (180²),4// aux couleurs de la marque (ecosystem.json). Sortie : ka-ui/assets/<id>/.5// Usage : node gen-assets.mjs (node_modules playwright requis — symlink ora-ka)6import { chromium } from "playwright";7import { mkdirSync, readFileSync, writeFileSync } from "fs";8import { dirname, join } from "path";9import { fileURLToPath } from "url";1011const here = dirname(fileURLToPath(import.meta.url));12const eco = JSON.parse(readFileSync(join(here, "ecosystem.json"), "utf8"));1314const GRAIN =15 "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.07'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E\")";1617// wordmark scindé : « Lou·Ka » → [« Lou », « Ka »] ; « Vrai-Prix » → [« Vrai-Prix »]18function splitMark(w) {19 if (w.includes("·")) {20 const [l, r] = w.split("·");21 return { left: l, boxed: r };22 }23 if (w === "Groupe KA") return { left: "Groupe", boxed: "KA" };24 return { left: w, boxed: null };25}2627function ogHtml(s) {28 const m = splitMark(s.wordmark);29 return `<!doctype html><html><head><meta charset="utf-8">30<style>31@font-face { font-family:'Space Grotesk'; src: local('Space Grotesk'); }32* { margin:0; box-sizing:border-box; }33body { width:1200px; height:630px; background:#f5f3ee; font-family:'Space Grotesk','Inter',system-ui,sans-serif; color:#141814; position:relative; overflow:hidden; }34body::before { content:""; position:absolute; inset:0; background-image:${GRAIN}; opacity:.5; z-index:10; }35.frame { position:absolute; inset:26px; border:3px solid #141814; border-radius:18px; }36.kicker { position:absolute; top:64px; left:72px; font-family:'JetBrains Mono',monospace; font-size:20px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:#1c5c41; display:flex; align-items:center; gap:14px; }37.kicker::before { content:""; width:34px; height:3px; background:#1c5c41; }38.mark { position:absolute; top:200px; left:72px; font-size:120px; font-weight:700; letter-spacing:-0.045em; display:flex; align-items:baseline; gap:10px; }39.boxed { display:inline-block; background:#141814; color:${s.accent}; border-radius:18px; padding:0 26px 10px; transform:rotate(-2deg); }40.tag { position:absolute; top:370px; left:72px; max-width:900px; font-family:'Inter',system-ui,sans-serif; font-size:31px; line-height:1.3; color:#4d5551; }41.bottom { position:absolute; left:0; right:0; bottom:0; height:92px; background:#141814; display:flex; align-items:center; justify-content:space-between; padding:0 72px; }42.par { color:#f5f3ee; font-size:27px; font-weight:700; letter-spacing:-0.02em; }43.par .ka { color:${s.accent}; }44.dom { font-family:'JetBrains Mono',monospace; font-size:21px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:${s.accent}; }45.swatch { position:absolute; top:64px; right:72px; display:flex; gap:10px; }46.swatch span { width:34px; height:34px; border:3px solid #141814; border-radius:9px; }47</style></head><body>48<div class="frame"></div>49<p class="kicker">Écosystème ·Ka · Québec</p>50<div class="swatch"><span style="background:${s.accent}"></span><span style="background:#141814"></span><span style="background:#ffffff"></span></div>51<h1 class="mark">${m.left}${m.boxed ? `<span class="boxed">${m.boxed}</span>` : ""}</h1>52<p class="tag">${s.tagline}.</p>53<div class="bottom"><span class="par">par Groupe <span class="ka">KA</span></span><span class="dom">${s.domain}</span></div>54</body></html>`;55}5657function faviconSvg(s) {58 const m = splitMark(s.wordmark);59 const letters = m.boxed === "KA" ? "KA" : "Ka";60 return `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">61<rect width="64" height="64" rx="14" fill="#141814"/>62<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="${s.accent}" stroke-opacity="0.35" stroke-width="2"/>63<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="${s.accent}" transform="rotate(-4 32 32)">${letters}</text>64</svg>`;65}6667const browser = await chromium.launch();68const page = await browser.newPage({ viewport: { width: 1200, height: 630 } });69for (const s of eco.sites) {70 const dir = join(here, "assets", s.id);71 mkdirSync(dir, { recursive: true });72 await page.setContent(ogHtml(s), { waitUntil: "networkidle" });73 await page.screenshot({ path: join(dir, "og.png") });74 writeFileSync(join(dir, "favicon.svg"), faviconSvg(s));75 console.log("✓", s.id);76}77// apple-touch-icon 180×180 depuis le favicon78const icon = await browser.newPage({ viewport: { width: 180, height: 180 } });79for (const s of eco.sites) {80 const dir = join(here, "assets", s.id);81 await icon.setContent(82 `<body style="margin:0">${readFileSync(join(dir, "favicon.svg"), "utf8").replace('width="64" height="64"', 'width="180" height="180"')}</body>`,83 );84 await icon.screenshot({ path: join(dir, "apple-touch-icon.png"), omitBackground: true });85}86await browser.close();87console.log("Assets générés dans ka-ui/assets/");88