主题生成器支持素材前缀(主题素材可放 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
This commit is contained in:
tamakyi
2026-10-21 19:32:55 +08:00
parent f37ff28a46
commit 6c21184733
6 changed files with 203 additions and 5 deletions
+112
View File
@@ -0,0 +1,112 @@
# 把主题素材放到对象存储(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**(最干净,但迁移成本在运维侧)。