- Logo em duas cores (teal + cobre), inline no cabeçalho e rodapé para seguir o tema do site; SVGs estáticos com prefers-color-scheme. - scripts/generate-favicons.mjs passa a gerar favicons, ícones de app (maskable), site.webmanifest e a imagem Open Graph 1200x630. - Manifest com fundo/tema obsidiana e lang pt-BR. Claude-Session: https://claude.ai/code/session_016W1xmizFcmXLrmabSvfSuc Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
158 lines
7.1 KiB
JavaScript
158 lines
7.1 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import puppeteer from 'puppeteer';
|
|
|
|
/**
|
|
* Gera todos os ativos de marca a partir da geometria do logo:
|
|
* favicons (transparentes), ícones de app (tile obsidiana), site.webmanifest
|
|
* e a imagem de compartilhamento (Open Graph, 1200x630).
|
|
*
|
|
* Cores = tokens de app/globals.css. Uso: npm run favicons
|
|
* (usa o Chrome do Puppeteer; defina CHROME_PATH para usar outro binário)
|
|
*/
|
|
|
|
const COR = {
|
|
obsidiana: "#07090d",
|
|
tealEscuro: "#2dd4bf",
|
|
cobreEscuro: "#ffb39b",
|
|
tealMedio: "#1a8f83", // legível em aba clara e escura
|
|
cobreMedio: "#c8694e",
|
|
texto: "#f4f6f8",
|
|
suave: "#a6afbd",
|
|
};
|
|
|
|
const marca = (teal, cobre) => `
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" stroke-linecap="round">
|
|
<g stroke="${teal}" stroke-width="3.4"><path d="M2 21h7"/><path d="M2 32h7"/><path d="M2 43h7"/></g>
|
|
<rect x="9.25" y="9.25" width="45.5" height="45.5" rx="13" stroke="${teal}" stroke-width="4.5"/>
|
|
<g stroke="${cobre}" stroke-width="4.5"><path d="M19 21h26"/><path d="M19 32h21"/><path d="M19 43h15"/></g>
|
|
</svg>`;
|
|
|
|
function createIco(pngBuffers) {
|
|
// pngBuffers: array of { width, height, buffer }
|
|
const count = pngBuffers.length;
|
|
const headerSize = 6;
|
|
const dirEntrySize = 16;
|
|
let offset = headerSize + dirEntrySize * count;
|
|
|
|
const header = Buffer.alloc(headerSize);
|
|
header.writeUInt16LE(0, 0); // reserved
|
|
header.writeUInt16LE(1, 2); // ICO type
|
|
header.writeUInt16LE(count, 4); // count
|
|
|
|
const entries = [];
|
|
for (const img of pngBuffers) {
|
|
const entry = Buffer.alloc(dirEntrySize);
|
|
entry.writeUInt8(img.width >= 256 ? 0 : img.width, 0);
|
|
entry.writeUInt8(img.height >= 256 ? 0 : img.height, 1);
|
|
entry.writeUInt8(0, 2); // colors
|
|
entry.writeUInt8(0, 3); // reserved
|
|
entry.writeUInt16LE(1, 4); // planes
|
|
entry.writeUInt16LE(32, 6); // bpp
|
|
entry.writeUInt32LE(img.buffer.length, 8); // size
|
|
entry.writeUInt32LE(offset, 12); // offset
|
|
entries.push(entry);
|
|
offset += img.buffer.length;
|
|
}
|
|
|
|
return Buffer.concat([header, ...entries, ...pngBuffers.map(b => b.buffer)]);
|
|
}
|
|
|
|
|
|
const pagina = (corpo, css = "") => `<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500&family=Space+Grotesk:wght@600&display=block" rel="stylesheet">
|
|
<style>*{margin:0;padding:0;box-sizing:border-box}html,body{width:100%;height:100%;overflow:hidden;background:transparent}${css}</style>
|
|
</head><body>${corpo}</body></html>`;
|
|
|
|
async function run() {
|
|
const browser = await puppeteer.launch({
|
|
executablePath: process.env.CHROME_PATH || undefined,
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
});
|
|
const page = await browser.newPage();
|
|
|
|
// 1. Favicons: marca sozinha, fundo transparente. O logo ocupa x 2..55; centraliza.
|
|
const favicon = pagina(`<div style="width:100%;height:100%;display:grid;place-items:center">${marca(COR.tealMedio, COR.cobreMedio).replace('<svg ', '<svg style="width:100%;height:100%;display:block" ')}</div>`);
|
|
await page.setContent(favicon);
|
|
const rendered = {};
|
|
for (const size of [16, 32, 48]) {
|
|
await page.setViewport({ width: size, height: size, deviceScaleFactor: 1 });
|
|
rendered[size] = await page.screenshot({ type: 'png', omitBackground: true, clip: { x: 0, y: 0, width: size, height: size } });
|
|
}
|
|
|
|
// 2. Ícones de app: tile obsidiana, marca com margem de segurança (maskable).
|
|
const tile = pagina(`<div style="width:100%;height:100%;background:${COR.obsidiana};display:grid;place-items:center">
|
|
<div style="width:58%;height:58%;transform:translateX(5%)">${marca(COR.tealEscuro, COR.cobreEscuro).replace('<svg ', '<svg style="width:100%;height:100%;display:block" ')}</div></div>`);
|
|
await page.setContent(tile);
|
|
for (const size of [180, 192, 512]) {
|
|
await page.setViewport({ width: size, height: size, deviceScaleFactor: 1 });
|
|
rendered[size] = await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: size, height: size } });
|
|
}
|
|
|
|
// 3. Open Graph / Twitter: 1200x630.
|
|
const og = pagina(`
|
|
<main>
|
|
<div class="topo">${marca(COR.tealEscuro, COR.cobreEscuro).replace('<svg ', '<svg class="logo" ')}<span class="nome">ESPDocs</span></div>
|
|
<h1>Documentação do ESP32<br>em português</h1>
|
|
<p>Especificações, pinagem, comparação e seleção das 12 séries do ESP32.</p>
|
|
<div class="chips"><span>Séries</span><span>Seletor</span><span>Comparador</span><span>Diagnóstico</span><span>Livro</span></div>
|
|
</main>
|
|
<div class="barra"></div>`, `
|
|
body{background:${COR.obsidiana};font-family:'Manrope',system-ui,sans-serif;color:${COR.texto};position:relative}
|
|
body::before{content:"";position:absolute;inset:0;background:radial-gradient(52rem 30rem at 50% -14%,rgba(45,212,191,.16),transparent 70%),radial-gradient(40rem 26rem at 100% 30%,rgba(56,140,248,.09),transparent 70%)}
|
|
main{position:relative;height:100%;padding:76px 88px 0}
|
|
.topo{display:flex;align-items:center;gap:20px}
|
|
.logo{width:68px;height:68px}
|
|
.nome{font:600 44px 'Space Grotesk',sans-serif;letter-spacing:-0.045em}
|
|
h1{margin-top:64px;font:600 86px/1.03 'Space Grotesk',sans-serif;letter-spacing:-0.04em}
|
|
p{margin-top:30px;max-width:900px;font-size:30px;line-height:1.45;color:${COR.suave}}
|
|
.chips{position:absolute;left:88px;bottom:62px;display:flex;gap:12px}
|
|
.chips span{border:1.5px solid #2e3846;border-radius:999px;padding:9px 22px;font-size:22px;font-weight:500;color:${COR.texto}}
|
|
.barra{position:absolute;left:0;right:0;bottom:0;height:10px;background:${COR.tealEscuro}}`);
|
|
await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 1 });
|
|
await page.setContent(og, { waitUntil: 'networkidle0' });
|
|
await page.evaluate(() => document.fonts.ready);
|
|
const ogPng = await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: 1200, height: 630 } });
|
|
|
|
await browser.close();
|
|
|
|
const icoBuffer = createIco([
|
|
{ width: 16, height: 16, buffer: rendered[16] },
|
|
{ width: 32, height: 32, buffer: rendered[32] },
|
|
{ width: 48, height: 48, buffer: rendered[48] }
|
|
]);
|
|
fs.writeFileSync('public/favicon.ico', icoBuffer);
|
|
fs.writeFileSync('public/favicon-48x48.png', rendered[48]);
|
|
fs.writeFileSync('public/favicon-32x32.png', rendered[32]);
|
|
fs.writeFileSync('public/favicon-16x16.png', rendered[16]);
|
|
fs.writeFileSync('public/apple-touch-icon.png', rendered[180]);
|
|
fs.writeFileSync('public/icon-192.png', rendered[192]);
|
|
fs.writeFileSync('public/icon-512.png', rendered[512]);
|
|
fs.writeFileSync('app/opengraph-image.png', ogPng);
|
|
console.log('Favicons, ícones de app e imagem Open Graph gerados.');
|
|
|
|
const manifest = {
|
|
name: "ESPDocs",
|
|
short_name: "ESPDocs",
|
|
description: "Documentação do ecossistema ESP32 em português brasileiro",
|
|
lang: "pt-BR",
|
|
start_url: "/",
|
|
display: "standalone",
|
|
background_color: COR.obsidiana,
|
|
theme_color: COR.obsidiana,
|
|
icons: [
|
|
{ src: "/favicon-48x48.png", sizes: "48x48", type: "image/png" },
|
|
{ src: "/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any maskable" },
|
|
{ src: "/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any maskable" }
|
|
]
|
|
};
|
|
fs.writeFileSync('public/site.webmanifest', JSON.stringify(manifest, null, 2) + '\n');
|
|
console.log('site.webmanifest gerado.');
|
|
}
|
|
|
|
run().catch(err => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|