package embed_static
import (
"embed"
"io/fs"
"net/http"
"regexp"
"strings"
"github.com/gin-gonic/gin"
)
//go:embed static/*
var staticFS embed.FS
var (
spaTitleRe = regexp.MustCompile(`(?s)
.*?`)
spaBrandTitleFn func() string
spaBrandJSONFn func() []byte // 站点品牌 JSON,注入 window.__J13_BRANDING__
spaBrandFaviconFn func() string // 站点 Favicon URL,注入
)
// SetSPADocumentTitle 注册站点标题提供者,ServeSPA 会注入到入口 HTML,避免刷新闪烁
func SetSPADocumentTitle(fn func() string) {
spaBrandTitleFn = fn
}
// SetSPABrandingJSON 注册品牌 JSON 提供者(须为合法 JSON 对象),供前端首屏同步读入
func SetSPABrandingJSON(fn func() []byte) {
spaBrandJSONFn = fn
}
// SetSPAFaviconURL 注册 Favicon URL 提供者,注入到入口 HTML 的
func SetSPAFaviconURL(fn func() string) {
spaBrandFaviconFn = fn
}
// SetupEmbed 配置内嵌资源:React SPA 静态资源
func SetupEmbed(r *gin.Engine) error {
if sub, err := fs.Sub(staticFS, "static/spa/assets"); err == nil {
fileServer := http.StripPrefix("/assets", http.FileServer(http.FS(sub)))
r.GET("/assets/*filepath", func(c *gin.Context) {
// 仅 200 写 immutable,避免中间层把旧 chunk 的 404 长期缓存
w := &cacheOnOKWriter{ResponseWriter: c.Writer, cacheControl: "public, max-age=31536000, immutable"}
c.Writer = w
fileServer.ServeHTTP(c.Writer, c.Request)
})
}
if sub, err := fs.Sub(staticFS, "static/spa/stickers"); err == nil {
fileServer := http.StripPrefix("/stickers", http.FileServer(http.FS(sub)))
r.GET("/stickers/*filepath", func(c *gin.Context) {
// stickers 不含哈希指纹,设置适中的缓存
c.Header("Cache-Control", "public, max-age=86400")
fileServer.ServeHTTP(c.Writer, c.Request)
})
}
return nil
}
// cacheOnOKWriter 仅在最终状态码为 200 时写入长期 Cache-Control
type cacheOnOKWriter struct {
gin.ResponseWriter
cacheControl string
wroteHeader bool
}
func (w *cacheOnOKWriter) WriteHeader(code int) {
if !w.wroteHeader {
w.wroteHeader = true
if code == http.StatusOK {
w.Header().Set("Cache-Control", w.cacheControl)
}
}
w.ResponseWriter.WriteHeader(code)
}
func (w *cacheOnOKWriter) Write(b []byte) (int, error) {
if !w.wroteHeader {
w.WriteHeader(http.StatusOK)
}
return w.ResponseWriter.Write(b)
}
// ServeSPA 返回 React SPA 入口(仅注入站点默认标题)
func ServeSPA(c *gin.Context) {
ServeSPAWithMeta(c, nil)
}
// ServeSPANoIndex 返回带 noindex 的 SPA(登录/后台等私密页)
func ServeSPANoIndex(c *gin.Context) {
title := ""
if spaBrandTitleFn != nil {
title = strings.TrimSpace(spaBrandTitleFn())
}
ServeSPAWithMeta(c, &SPAPageMeta{
Title: title,
Robots: "noindex,nofollow",
})
}
// IsSPARoute 判断是否应由 SPA 处理
func IsSPARoute(path string) bool {
if path == "/health" || path == "/robots.txt" || path == "/sitemap.xml" {
return false
}
if strings.HasPrefix(path, "/api") ||
strings.HasPrefix(path, "/admin") ||
strings.HasPrefix(path, "/uploads") ||
strings.HasPrefix(path, "/media") ||
strings.HasPrefix(path, "/assets") ||
strings.HasPrefix(path, "/stickers") ||
strings.HasPrefix(path, "/oauth") ||
strings.HasPrefix(path, "/.well-known") {
return false
}
return true
}