Files
tamabox/docs/theme-assets-cdn.md
T
tamakyi 6c21184733 主题生成器支持素材前缀(主题素材可放 R2/OSS)
主题素材只有一处引用:主题 css 的 url()(分享卡是 Go 用 imaging 画的,不读素材),
所以只要让 CSS 里的地址指向对象存储即可。

- 导出面板新增「素材前缀」:空 = /static/images/themes/<slug>/(图片随二进制发布,与以前一致);
  填 https://cdn.example.com/themes = CSS 写绝对 URL,zip 里的图片改为「上传用」
- 地址统一由 urlOf() 决定。修了一个重复实现:layerCSS 里复制过一份路径拼接,
  只改 urlOf 会出现「背景走 CDN、装饰还指本地」
- 前缀随意切换不影响预览(预览恒用内联 dataURL);存进工程 json;?assetbase= 可自检
- INSTALL 文案按是否托管分叉,写清图片传到哪、换域名要重新导出
- 新增 docs/theme-assets-cdn.md:三种做法(生成器前缀 / 应用内 301 / 边缘改写)、
  缓存与 ?v=、CORS 不需要、深色无素材、迁移用的 sed 命令
- README 更新;UI 修正:240px 面板里 4 字标签会被 flex 压窄,需 flex:0 0 auto + 84px
2026-10-21 19:32:55 +08:00

113 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 把主题素材放到对象存储(R2 / OSS)
现状:主题素材在 `static/images/themes/<slug>/`,通过 `static/fs.go` 的 `//go:embed images/themes`
随二进制发布。好处是部署即生效、不依赖外部服务;代价是**换一张图就要重新编译部署**,
而且一张 1920×1080 的场景图会让二进制大几百 KB。
主题素材只有一处引用:**主题 CSS 里的 `url(...)`**(`--tb-page-image` / `--tb-page-pattern` /
`--tb-deco`)。分享卡是 Go 里用 `imaging` 画出来的,不读主题素材。所以只要解决「CSS 里的地址指向哪」,
就把整套素材搬走了。
下面三种做法按「改动量从 0 到有代码」排列,选一个即可,不必叠加。
---
## A. 生成器直接写绝对地址(已支持,推荐)
`dev/theme-builder` 的导出面板新增「**素材前缀**」:
```
素材前缀 https://cdn.example.com/themes
```
留空 = 沿用 `/static/images/themes/<slug>/`(图片随二进制发布,和以前一样)。
填了 = 导出的 CSS 里所有图片地址变成 `https://cdn.example.com/themes/<slug>/<id>.webp`。
**前缀里不要带 slug**,slug 由导出时自动拼在后面。
图片照旧打进 zip(`static/images/themes/<slug>/*.webp`),只是用途从「放进仓库」变成
「传到对象存储的 `<前缀>/<slug>/` 下」。INSTALL 文案会跟着变,直接写清楚往哪传。
- 优点:零服务端改动、零跳转、缓存最优(CDN 直接命中);
- 缺点:域名烘焙进 CSS —— 换域名/换路径要**重新导出**(旧主题可以用 `sed` 批量替换);
- 仓库里可以删掉 `static/images/themes/<slug>/` 来缩小编译产物,**确认 CSS 已是绝对地址**再删。
命令行等价做法(已有主题不想重导):
```bash
sed -i 's#/static/images/themes/shiroko/#https://cdn.example.com/themes/shiroko/#g' \
static/css/themes/shiroko.css
```
---
## B. 应用内 301 跳转(配置驱动,CSS 一行都不用改)
`conf/app.ini`:
```ini
[theme]
asset_base = https://cdn.example.com/themes ; 留空 = 本地静态目录
```
中间件必须在 `flamego.Static` **之前**注册(`internal/route/route.go` 里 `f.Use(flamego.Static(...))` 前面):
```go
// 主题素材转发到对象存储:命中前缀就 301 出去,其余请求原样放过。
// 必须挂在 static 中间件之前——Static 命中本地文件就直接返回了,轮不到这里。
func themeAssetRedirect() flamego.Handler {
const prefix = "/static/images/themes/"
return func(ctx flamego.Context) {
base := theme.AssetBase() // 从 conf 读,空串 = 不接管
p := ctx.Request().URL.Path
if base == "" || !strings.HasPrefix(p, prefix) {
return
}
w := ctx.ResponseWriter()
w.Header().Set("Cache-Control", "public, max-age=86400")
http.Redirect(w, ctx.Request(), base+strings.TrimPrefix(p, prefix), http.StatusMovedPermanently)
}
}
```
- 优点:已有主题、生成器导出的相对地址**都能用**,切 CDN 只改配置;
- 缺点:冷缓存时每张图多一跳;**301 会被浏览器长期缓存**,以后想切回本地,
老访客可能还在访问 CDN(介意的话用 `302`,或一开始就用方案 A);
- 本地开发记得把 `asset_base` 留空,否则断网时素材全 404。
---
## C. 边缘 / 反代改写(零代码)
素材路径有稳定前缀,最适合在边缘处理:
- **Cloudflare**:Origin Rule / Transform Rule,把 `/static/images/themes/*` 的 Host 改成
R2 自定义域(如 `cdn.example.com`),或 Redirect Rule 直接 301 过去;
- **nginx**:`location /static/images/themes/ { proxy_pass https://cdn.example.com/themes/; }`,
或 `rewrite` 成 CDN 地址后 `return 301`。
- 优点:应用完全不知道素材在哪,不占应用带宽;
- 缺点:绑定部署环境,本地跑没有等效层(要考虑 fallback)。
---
## 三种方案共同要处理的细节
1. **缓存**:对象存储上建议
`Cache-Control: public, max-age=31536000, immutable`(文件名带版本时)。
换图**不要覆盖同名对象**——要么换 `id`(生成器里改文件名),要么给 URL 加 `?v=`;
Cloudflare 默认把 query string 计入缓存键,所以 `?v=` 是有效的。
2. **CORS 不用配**:`background-image` 不触发 CORS,匿名 GET 即可。
3. **主题 CSS 本身不用上 CDN**:一个主题只有几 KB,而且随 `StaticVersion` 走缓存键,
留在 `static/css/themes/` 更省事。
4. **深色模式没有素材**(深色块 `--tb-deco` 恒为 `none`),别只传浅色图就以为漏传了。
5. **分享卡不受影响**:`route/question/share.go` 用 `imaging` 直接画,不加载主题图片。
6. 迁完之后 `go build` 会小一圈,但**主题 css 仍是编译期 embed**:改 CSS 还是要重编译部署。
---
## 建议
- 只是想让部署产物变小、换图不用重编译 → **A**(生成器填前缀,最省事,缓存也最好)。
- 想保留「素材可以随时换、CSS 不用重发」的灵活性 → **B**(配一个 `asset_base`,接受多一跳)。
- 站点已经在 Cloudflare 上、且愿意在边缘配规则 → **C**(最干净,但迁移成本在运维侧)。