fix(branding): regenerate raster favicons — white mark was shipped without its gradient tile (#7390)

* fix(branding): regenerate raster favicons — white mark was shipped without its gradient tile

favicon.ico, icon-512.png and apple-touch-icon.png contained only the
white network mark on a transparent background: every opaque pixel was
pure #FFFFFF, with no trace of the red gradient tile that favicon.svg
and apple-touch-icon.svg define. On light browser tab strips and
bookmark bars the icon therefore rendered as a blank white square.

Regenerate all three assets from the canonical SVG sources:

- favicon.ico: same 7 frames as before (16–256), each rasterized from
  the vector at native size, stored as PNG frames — 141 KB → 16 KB
- icon-512.png: rendered from favicon.svg with the gradient tile
- apple-touch-icon.png: rendered from apple-touch-icon.svg at the
  180×180 size layout.tsx declares (the shipped file was 512×512)

Add scripts/ad-hoc/generate-brand-icons.mjs so the raster assets can be
reproduced from the SVGs (byte-identical) instead of drifting again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(branding): keep standalone PNGs truecolor; palette-quantize only ICO frames

Review feedback (gemini-code-assist): 8-bit palette quantization
measurably clips the antialiased gradient on large assets — 1242 → 253
distinct colors at 512px. Make palette opt-in per render: ICO frames
keep it (16 KB container), icon-512.png and apple-touch-icon.png are
now truecolor (+9 KB total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gogo
2026-07-19 09:18:45 +09:00
committed by GitHub
parent ea650253af
commit 10e5dd81e0
4 changed files with 77 additions and 0 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,77 @@
// Regenerates the raster brand icons in public/ from their SVG sources, so the
// PNG/ICO assets can never drift from the canonical vector artwork again.
//
// public/favicon.svg → public/favicon.ico (16/32/48/64/96/128/256, PNG frames)
// public/favicon.svg → public/icon-512.png
// public/apple-touch-icon.svg → public/apple-touch-icon.png (180×180, as declared in layout.tsx)
//
// Usage:
// npm i --no-save sharp
// node scripts/ad-hoc/generate-brand-icons.mjs
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
let sharp;
try {
sharp = (await import("sharp")).default;
} catch {
console.error("sharp is required: npm i --no-save sharp");
process.exit(1);
}
const publicDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "public");
const faviconSvg = readFileSync(join(publicDir, "favicon.svg"));
const appleSvg = readFileSync(join(publicDir, "apple-touch-icon.svg"));
// favicon.svg is authored on a 32×32 viewBox; scale density so each target
// size is rendered from the vector instead of upscaling a small raster.
// Palette quantization (8-bit, ≤256 colors) is only applied where asked:
// it keeps the ICO frames tiny, but measurably clips the antialiased
// gradient on large standalone PNGs (1242 → 253 distinct colors at 512px),
// so those stay truecolor.
const renderPng = (svg, viewBox, size, usePalette = false) =>
sharp(svg, { density: 72 * (size / viewBox) })
.resize(size, size)
.png({ compressionLevel: 9, palette: usePalette, effort: 10 })
.toBuffer();
// ICO container with PNG-compressed frames (identical layout to the icon the
// package previously shipped, but ~16 KB instead of ~141 KB).
function buildIco(frames) {
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0); // reserved
header.writeUInt16LE(1, 2); // type: icon
header.writeUInt16LE(frames.length, 4);
let offset = 6 + 16 * frames.length;
const entries = frames.map(({ size, png }) => {
const e = Buffer.alloc(16);
e.writeUInt8(size === 256 ? 0 : size, 0); // width (0 means 256)
e.writeUInt8(size === 256 ? 0 : size, 1); // height
e.writeUInt16LE(1, 4); // color planes
e.writeUInt16LE(32, 6); // bits per pixel
e.writeUInt32LE(png.length, 8);
e.writeUInt32LE(offset, 12);
offset += png.length;
return e;
});
return Buffer.concat([header, ...entries, ...frames.map((f) => f.png)]);
}
const icoSizes = [16, 32, 48, 64, 96, 128, 256];
const frames = [];
for (const size of icoSizes) {
frames.push({ size, png: await renderPng(faviconSvg, 32, size, true) });
}
writeFileSync(join(publicDir, "favicon.ico"), buildIco(frames));
writeFileSync(join(publicDir, "icon-512.png"), await renderPng(faviconSvg, 32, 512));
// layout.tsx declares apple-touch-icon.png as 180×180; its SVG source is
// authored on a 180×180 viewBox.
writeFileSync(join(publicDir, "apple-touch-icon.png"), await renderPng(appleSvg, 180, 180));
console.log(`favicon.ico (${icoSizes.join("/")}), icon-512.png, apple-touch-icon.png regenerated`);