你已经把 SvelteKit 路由搭好、调好了 layout,也把 build 部署上线了。结果打开浏览器标签页一看,还是默认的 Svelte 图标——更糟的是干脆一片空白。解决办法通常都藏在同一个文件里:app.html。
app.html 是 SvelteKit 用来在服务端和客户端渲染每一页的 HTML 外壳。根据 SvelteKit 项目结构文档 的说明,这个模板放在 src/ 根目录,是唯一能注入"在每条路由 hydration 后依然保留"标记的地方。这也让它成为放置 favicon <link> 标签的天然位置。
为什么用 app.html 而不用 layout?
你确实可以在 +layout.svelte 的 <svelte:head> 块里加 favicon 标签,大多数路由下都能正常工作。但 app.html 的渲染早于任何 layout 或组件代码,这就带来了两个实打实的好处:
- 浏览器在收到首字节时就会发起 favicon 请求,标签页能更快显示出来。
- 从
+layout.svelte里注入的标签可能被不执行 JavaScript 的爬虫和机器人忽略,而app.html输出的始终是纯静态 HTML。
前置条件
你需要准备:
- 一个跑得起来的 SvelteKit 项目(2.x 或更高)。
- 放在
static/下的 favicon 文件。完整一套建议备齐:32×32 的favicon.ico、180×180 的apple-touch-icon.png、一个 SVG,以及可选的site.webmanifest。
如果还没生成这些文件,可以用 Mzu favicondl 从任意源图一键生成完整尺寸包。
Step-by-step:给 app.html 添加 favicon link 标签
1. 找到 app.html 文件
打开 src/app.html。默认情况下大致长这样:
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8' />
<link rel='icon' href='%sveltekit.assets%/favicon.png' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data='hover'>
<div style='display: contents'>%sveltekit.body%</div>
</body>
</html>
注意 %sveltekit.assets% 和 %sveltekit.head% 这两个占位符——它们会在构建时被替换。可以保留它们,再在上面或下面加新标签。
2. 替换默认的 favicon 行
脚手架默认只带一个 favicon.png 链接。删掉它,换成一套完整的标签。HTML 规范 把 rel='icon' 定义为声明图标的官方方式,照着用这个属性就行。
<link rel='icon' href='%sveltekit.assets%/favicon.svg' type='image/svg+xml' />
<link rel='icon' href='%sveltekit.assets%/favicon.ico' sizes='32x32' />
<link rel='apple-touch-icon' href='%sveltekit.assets%/apple-touch-icon.png' sizes='180x180' />
<link rel='manifest' href='%sveltekit.assets%/site.webmanifest' />
SVG 放在最前面,现代浏览器会优先选它。ICO 作为老浏览器的回退方案。Apple Touch 和 manifest 放最后,它们针对的是特定平台。
3. 加入深色模式支持
如果你的 SVG 图标在深色标签页下需要换种样式(这年头谁还没用深色主题呢?),可以声明两个按 media 区分的来源。我们在 favicon 深色模式指南 里用的也是这个套路:
<link rel='icon' href='%sveltekit.assets%/favicon-light.svg' type='image/svg+xml' media='(prefers-color-scheme: light)' />
<link rel='icon' href='%sveltekit.assets%/favicon-dark.svg' type='image/svg+xml' media='(prefers-color-scheme: dark)' />
浏览器会读取 media 属性,匹配当前配色方案。
4. 把文件放进 static/
SvelteKit 会把 static/ 下所有内容从站点根路径对外提供。把每个 favicon 文件按标签里引用的原名放进去。一个清爽的结构是:
static/
├── favicon.svg
├── favicon.ico
├── favicon-light.svg
├── favicon-dark.svg
├── apple-touch-icon.png
└── site.webmanifest
5. 重启 dev server
Vite 对 HTML 外壳的缓存非常激进。改完 app.html 后,按 Ctrl+C 停掉 dev server,再跑一次 npm run dev。不然你看到的可能还是旧的标记。
常见坑
把 favicon 标签放进 body。浏览器只在 <head> 里找图标声明,<body> 里的内容一律忽略,哪怕文件名就叫 app.html。如果不生效,第一件事就是检查这个位置。
标签里的文件名和 static/ 不一致。Linux 部署上大小写敏感,favicon.ICO 和 favicon.ico 是两个不同的文件。文件名必须完全对上。
部署后图标还是旧的。浏览器对 favicon 的缓存相当顽固——有时能缓存几周。换文件后看不到更新,可以加个查询字符串或 content hash 再部署。这个套路对 static/ 下任何静态资源都适用。
SVG 属性被双重编码。如果复制来的片段里 type="image/svg+xml" 用的是双引号,贴进 app.html 外壳后 Vite 可能会把它弄坏。HTML 属性里统一用单引号,构建过程就不会出岔子。
收尾
在 SvelteKit 里,app.html 模板是声明 favicon 最干净的位置——它在任何 JavaScript 运行之前就生效,并且会随每个页面响应一起输出。把完整的 SVG + ICO + Apple Touch 这一套放进 static/,再把对应的 <link> 标签接到 <head> 里,从首次绘制开始每个浏览器标签页就能拿到一张清晰的图标。
想更全面地了解 SvelteKit 的 favicon 方案——包括 +layout.svelte 的做法和动态图标——可以看我们的 SvelteKit favicon 指南。如果需要现成的图标文件,Mzu favicondl 可以从任意源图在几秒内生成完整一套。