feat(serverembed): add initial HTML and SVG assets
Some checks failed
CI / Test (1.23) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Test (1.22) (push) Has been cancelled

* Create index.html for the web application entry point
* Add vite.svg as a favicon
* Update frontend.go to embed all files in the dist directory
* Modify vite.config.ts to set output directory for builds
This commit is contained in:
Hein
2026-02-20 16:45:30 +02:00
parent 7d6f99b3b3
commit 5ca375fd58
8 changed files with 142 additions and 41 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"io/fs"
"net/http"
"time"
@@ -12,6 +13,7 @@ import (
"git.warky.dev/wdevs/whatshooked/pkg/config"
"git.warky.dev/wdevs/whatshooked/pkg/handlers"
"git.warky.dev/wdevs/whatshooked/pkg/models"
"git.warky.dev/wdevs/whatshooked/pkg/serverembed"
"git.warky.dev/wdevs/whatshooked/pkg/storage"
"github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
@@ -80,9 +82,12 @@ func NewServer(cfg *config.Config, db *bun.DB, wh WhatsHookedInterface) (*Server
// Add custom routes (login, logout, etc.) on main router
SetupCustomRoutes(router, secProvider, db)
// Add static file serving for React app at /ui/ route
// Serve React app from configurable filesystem path
spa := spaHandler{staticPath: cfg.Server.UIPath, indexPath: "index.html"}
// Serve React SPA from the embedded filesystem at /ui/
distFS, err := fs.Sub(serverembed.RootEmbedFS, "dist")
if err != nil {
return nil, fmt.Errorf("failed to sub embedded dist FS: %w", err)
}
spa := embeddedSPAHandler{fs: distFS, indexPath: "index.html"}
router.PathPrefix("/ui/").Handler(http.StripPrefix("/ui", spa))
router.PathPrefix("/ui").Handler(http.StripPrefix("/ui", spa))
@@ -603,42 +608,46 @@ func extractToken(r *http.Request) string {
return ""
}
// spaHandler implements the http.Handler interface for serving a SPA
type spaHandler struct {
staticPath string
indexPath string
// embeddedSPAHandler serves a React SPA from an embedded fs.FS.
// Static assets are served directly; all other paths fall back to index.html
// to support client-side routing.
type embeddedSPAHandler struct {
fs fs.FS
indexPath string
}
// ServeHTTP inspects the URL path to locate a file within the static dir
// If a file is found, it is served. If not, the index.html file is served for client-side routing
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Get the path
func (h embeddedSPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// Strip leading slash so fs.FS Open calls work correctly.
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
// Check whether a file exists at the given path
info, err := http.Dir(h.staticPath).Open(path)
if path == "" {
path = h.indexPath
}
// Try to open the requested path in the embedded FS.
f, err := h.fs.Open(path)
if err != nil {
// File does not exist, serve index.html for client-side routing
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
// Not found — serve index.html for client-side routing.
r2 := r.Clone(r.Context())
r2.URL.Path = "/" + h.indexPath
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r2)
return
}
defer info.Close()
defer f.Close()
// Check if path is a directory
stat, err := info.Stat()
if err != nil {
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
stat, err := f.Stat()
if err != nil || stat.IsDir() {
r2 := r.Clone(r.Context())
r2.URL.Path = "/" + h.indexPath
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r2)
return
}
if stat.IsDir() {
// Serve index.html for directories
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
return
}
// Otherwise, serve the file
http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
// Serve the real file.
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r)
}
// corsMiddleware adds CORS headers to allow frontend requests