飞导航有两百多个链接。如果浏览器直接向 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
所以旧调用地址继续可用,但飞导航是否上线已经与它无关。