feat: 为全部 5 站生成 favicon.ico 与各站专属 PWA 图标
- 新增 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
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* # 脚本 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`);
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 992 B |