feat: init sproutclaw-web — Go+Gin backend + React frontend
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
68
backend/internal/static/static.go
Normal file
68
backend/internal/static/static.go
Normal file
@@ -0,0 +1,68 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user