Files
ai-directory-platform/packages/core/scripts/gen-favicons.mjs
T
bigcode 2a269c25ae 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
2026-09-16 09:31:24 +08:00

71 lines
2.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* # 脚本 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`);
}