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
This commit is contained in:
bigcode
2026-09-16 09:31:24 +08:00
parent 715152f159
commit 2a269c25ae
16 changed files with 70 additions and 0 deletions
+70
View File
@@ -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`);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B