69 lines
1.9 KiB
Go
69 lines
1.9 KiB
Go
package static
|
||
|
||
import (
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// Register attaches the static file handler to the Gin router.
|
||
// distDir is the path to frontend/dist/.
|
||
func Register(r *gin.Engine, distDir string) {
|
||
r.NoRoute(func(c *gin.Context) {
|
||
path := c.Request.URL.Path
|
||
|
||
// Skip API routes – they're handled elsewhere
|
||
if strings.HasPrefix(path, "/api/") {
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||
return
|
||
}
|
||
|
||
// Try to serve a real file
|
||
fsPath := filepath.Join(distDir, filepath.FromSlash(path))
|
||
if info, err := os.Stat(fsPath); err == nil && !info.IsDir() {
|
||
serveFile(c, fsPath)
|
||
return
|
||
}
|
||
|
||
// SPA fallback: serve index.html
|
||
indexPath := filepath.Join(distDir, "index.html")
|
||
if _, err := os.Stat(indexPath); err != nil {
|
||
c.String(http.StatusNotFound, "frontend not built – run npm run build:web in frontend/")
|
||
return
|
||
}
|
||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||
c.File(indexPath)
|
||
})
|
||
}
|
||
|
||
func serveFile(c *gin.Context, fsPath string) {
|
||
name := filepath.Base(fsPath)
|
||
ext := strings.ToLower(filepath.Ext(name))
|
||
|
||
if name == "index.html" || strings.HasSuffix(name, ".webmanifest") {
|
||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||
} else if isHashedAsset(name, ext) {
|
||
c.Header("Cache-Control", "public, max-age=31536000, immutable")
|
||
} else if ext == ".woff" || ext == ".woff2" || ext == ".ttf" || ext == ".otf" {
|
||
c.Header("Cache-Control", "public, max-age=31536000, immutable")
|
||
} else {
|
||
c.Header("Cache-Control", "public, max-age=3600")
|
||
}
|
||
|
||
// Gin's File() handles Content-Type and ETag automatically
|
||
c.File(fsPath)
|
||
}
|
||
|
||
// isHashedAsset returns true if the filename looks like a Vite hashed asset
|
||
// (e.g. index-BxYt3k2a.js, chunk-abc123.css).
|
||
func isHashedAsset(name, ext string) bool {
|
||
switch ext {
|
||
case ".js", ".css", ".mjs":
|
||
return strings.Contains(name, "-") || strings.Contains(name, ".")
|
||
}
|
||
return false
|
||
}
|