//go:build !exclude_frontend package frontend import ( "embed" "fmt" "io/fs" "net/http" "os" "strings" "time" "github.com/gin-gonic/gin" ) //go:embed all:dist/* var frontendFS embed.FS func RegisterFrontend(router *gin.Engine) error { distFS, err := fs.Sub(frontendFS, "dist") if err != nil { return fmt.Errorf("failed to create sub FS: %w", err) } cacheMaxAge := time.Hour * 24 fileServer := NewFileServerWithCaching(http.FS(distFS), int(cacheMaxAge.Seconds())) router.NoRoute(func(c *gin.Context) { // Try to serve the requested file path := strings.TrimPrefix(c.Request.URL.Path, "/") if _, err := fs.Stat(distFS, path); os.IsNotExist(err) { // File doesn't exist, serve index.html instead c.Request.URL.Path = "/" } fileServer.ServeHTTP(c.Writer, c.Request) }) return nil } // FileServerWithCaching wraps http.FileServer to add caching headers type FileServerWithCaching struct { root http.FileSystem lastModified time.Time cacheMaxAge int lastModifiedHeaderValue string cacheControlHeaderValue string } func NewFileServerWithCaching(root http.FileSystem, maxAge int) *FileServerWithCaching { return &FileServerWithCaching{ root: root, lastModified: time.Now(), cacheMaxAge: maxAge, lastModifiedHeaderValue: time.Now().UTC().Format(http.TimeFormat), cacheControlHeaderValue: fmt.Sprintf("public, max-age=%d", maxAge), } } func (f *FileServerWithCaching) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Check if the client has a cached version if ifModifiedSince := r.Header.Get("If-Modified-Since"); ifModifiedSince != "" { ifModifiedSinceTime, err := time.Parse(http.TimeFormat, ifModifiedSince) if err == nil && f.lastModified.Before(ifModifiedSinceTime.Add(1*time.Second)) { // Client's cached version is up to date w.WriteHeader(http.StatusNotModified) return } } w.Header().Set("Last-Modified", f.lastModifiedHeaderValue) w.Header().Set("Cache-Control", f.cacheControlHeaderValue) http.FileServer(f.root).ServeHTTP(w, r) }