博客

我们如何在构建时生成 favicon

飞导航在开发和部署前从 Google 同步已收录网站的 favicon,再把图标与 Astro 页面一起交给 Cloudflare Static Assets。

飞导航有两百多个链接。如果浏览器直接向 Google 请求图标,一次打开首页就会连接第三方服务几十次。

早期方案是在 icons.fei.im 上实时代理 Google favicon。后来虽然改成了 Static Assets,主站和图标仍然需要分别部署。现在又少了一层:图标在 Astro 构建前生成,直接放进主站。

页面只请求同源地址:

/favicons/chat.openai.com.png

浏览器不再连接 icons.fei.im。图标与 HTML、CSS、JavaScript 一起进入 dist,由主站的 Cloudflare Static Assets 发布。

构建时怎么同步

链接的源数据位于 data/webstack.yml。项目先生成 src/data/links.json,同步脚本再提取并去重域名:

const hostnames = Array.from(
  new Set(
    links.map(({ url }) =>
      new URL(url).hostname.toLowerCase().replace(/^www\./, ""),
    ),
  ),
).sort();

随后请求 Google favicon:

https://www.google.com/s2/favicons?domain_url=https://chat.openai.com&sz=64

Google 有时会返回 JPEG。脚本使用 Sharp 统一转换成 PNG,再按域名保存:

public/favicons/chat.openai.com.png

同步限制为 6 个并发,每个地址最多重试 3 次。上游没有有效图标时,脚本写入飞导航默认图标,因此每个已收录域名都有对应文件。

开发和构建如何调用

启动开发服务器和生产构建前,npm 生命周期会自动执行图标同步:

{
  "scripts": {
    "predev": "npm run migrate:links && npm run sync:favicons:site",
    "prebuild": "npm run migrate:links && npm run sync:favicons:site",
    "sync:favicons:site": "node scripts/sync-favicons.mjs --target=site"
  }
}

生成的 public/favicons/ 不提交到 Git。Cloudflare 从 GitHub 构建时会重新下载,Astro 随后把这些文件复制到 dist/favicons/

组件中的地址很简单:

const hostname = new URL(url).hostname.replace(/^www\./, "");
const faviconUrl = `/favicons/${encodeURIComponent(hostname)}.png`;

图片加载失败时,前端仍会切换到主站 /favicon.png

为什么随主站一起发布

  • 浏览器只连接 fei.im,少一次 DNS 和 TLS 建连;
  • favicon 属于 Static Assets,不产生动态 Worker 调用;
  • 新增链接时,页面和对应图标在同一次部署中上线;
  • 不需要为主站维护第二个构建项目和环境变量;
  • Google 临时故障不会影响已经部署的图片。

代价是每次构建需要额外下载图标,目前两百多个域名大约增加几十秒。对更新频率不高的个人导航站,这比运行时代理简单。

fei-favicon 为什么还保留

仓库里的 workers/favicon/icons.fei.im 没有删除,它们现在是可选的独立静态图标服务,留给其他项目复用。飞导航主站不再请求这个域名。

需要更新独立服务时运行:

npm run deploy:favicon

这个命令使用同步脚本的 worker 目标,生成兼容原地址的无扩展名文件:

https://icons.fei.im/v1/chat.openai.com?size=64

所以旧调用地址继续可用,但飞导航是否上线已经与它无关。