- 新增 gen-favicons.mjs:以各站 public/icon.svg 为设计源, 栅格化生成 src/app/favicon.ico(16/32/48 三档 PNG-in-ICO) - 修复五站 icon-192/512.png 同图的通用图标问题,改为各站品牌设计 - Next 自动注入 <link rel="icon">,/favicon.ico 由 404 变 200
71 lines
2.7 KiB
JavaScript
71 lines
2.7 KiB
JavaScript
/**
|
||
* # 脚本 gen-favicons.mjs
|
||
* 说明:以各站点 public/icon.svg(已有差异化品牌设计)为唯一设计源,生成:
|
||
* 1) sites/<site>/src/app/favicon.ico —— 16/32/48 三档 PNG-in-ICO(Next App Router 约定,自动注入 <link rel="icon">)
|
||
* 2) sites/<site>/public/icon-192.png / icon-512.png —— 覆盖原五站同图的通用 PWA 图标(manifest.webmanifest 引用)
|
||
* 用法:bun packages/core/scripts/gen-favicons.mjs
|
||
*/
|
||
import { readFileSync, writeFileSync } from "node:fs";
|
||
import { dirname, join } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import sharp from "sharp";
|
||
|
||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "sites");
|
||
const SITES = ["ai-stack", "launchai", "the-ai-ledger", "devai-tools", "findai"];
|
||
const ICO_SIZES = [16, 32, 48];
|
||
const PNG_SIZES = [192, 512];
|
||
|
||
/** # 函数 renderSvg
|
||
* 说明:SVG 按目标尺寸矢量栅格化为 PNG(按 viewBox 64 推导 density 保证清晰度)。
|
||
* 参数:svgBuffer 源 SVG;size 目标像素
|
||
* 返回:PNG Buffer
|
||
*/
|
||
async function renderSvg(svgBuffer, size) {
|
||
const density = Math.round((72 * size) / 64);
|
||
return sharp(svgBuffer, { density }).resize(size, size).png().toBuffer();
|
||
}
|
||
|
||
/** # 函数 packIco
|
||
* 说明:把多档 PNG 打包为 ICO 容器(Vista+ 支持 PNG 压缩条目)。
|
||
* 参数:pngs [{ size, buf }]
|
||
* 返回:ICO Buffer
|
||
*/
|
||
function packIco(pngs) {
|
||
const header = Buffer.alloc(6);
|
||
header.writeUInt16LE(0, 0); // 保留位
|
||
header.writeUInt16LE(1, 2); // 类型:图标
|
||
header.writeUInt16LE(pngs.length, 4);
|
||
const entries = Buffer.alloc(16 * pngs.length);
|
||
let offset = 6 + entries.length;
|
||
const blobs = [];
|
||
pngs.forEach((p, i) => {
|
||
const e = entries.subarray(i * 16, (i + 1) * 16);
|
||
e[0] = p.size >= 256 ? 0 : p.size; // 宽(0 表示 256)
|
||
e[1] = p.size >= 256 ? 0 : p.size; // 高
|
||
e[2] = 0; // 调色板色数
|
||
e[3] = 0; // 保留位
|
||
e.writeUInt16LE(1, 4); // 色彩平面
|
||
e.writeUInt16LE(32, 6); // 位深
|
||
e.writeUInt32LE(p.buf.length, 8);
|
||
e.writeUInt32LE(offset, 12);
|
||
offset += p.buf.length;
|
||
blobs.push(p.buf);
|
||
});
|
||
return Buffer.concat([header, entries, ...blobs]);
|
||
}
|
||
|
||
for (const site of SITES) {
|
||
const svg = readFileSync(join(ROOT, site, "public", "icon.svg"));
|
||
const ico = packIco(
|
||
await Promise.all(
|
||
ICO_SIZES.map(async (size) => ({ size, buf: await renderSvg(svg, size) }))
|
||
)
|
||
);
|
||
writeFileSync(join(ROOT, site, "src", "app", "favicon.ico"), ico);
|
||
for (const size of PNG_SIZES) {
|
||
const buf = await renderSvg(svg, size);
|
||
writeFileSync(join(ROOT, site, "public", `icon-${size}.png`), buf);
|
||
}
|
||
console.log(`✓ ${site}: favicon.ico(16/32/48) + icon-192.png + icon-512.png`);
|
||
}
|