From f6af57cf58e898876d6c42b472dc8a34d3a7559d Mon Sep 17 00:00:00 2001 From: NanamiAdmin Date: Thu, 10 Sep 2026 20:43:46 +0800 Subject: [PATCH] feat(web): add static file serving for the frontend --- router.go | 5 +-- web/embed.go | 6 ++++ web/handler.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 web/embed.go create mode 100644 web/handler.go diff --git a/router.go b/router.go index 56b46e8..8603550 100644 --- a/router.go +++ b/router.go @@ -6,6 +6,7 @@ import ( "nukumizu-backend/handler" "nukumizu-backend/postLog" + "nukumizu-backend/web" ) // SetupRouter registers all HTTP routes and returns a configured ServeMux. @@ -38,8 +39,8 @@ func SetupRouter() *http.ServeMux { mux.HandleFunc("/api/system/getLogs", logSocketHandler.Handle) } - // Catch-all 404 handler. - mux.HandleFunc("/", NotFoundHandler) + // Static file serving for the web frontend. + mux.HandleFunc("/", web.ServeStatic) postLog.Info("Router setup completed") return mux diff --git a/web/embed.go b/web/embed.go new file mode 100644 index 0000000..65206e0 --- /dev/null +++ b/web/embed.go @@ -0,0 +1,6 @@ +package web + +import "embed" + +//go:embed dist/* +var StaticFiles embed.FS diff --git a/web/handler.go b/web/handler.go new file mode 100644 index 0000000..5635c8a --- /dev/null +++ b/web/handler.go @@ -0,0 +1,93 @@ +package web + +import ( + "io/fs" + "net/http" + "path" + "strings" +) + +var staticFS http.FileSystem + +func init() { + subFS, err := fs.Sub(StaticFiles, "dist") + if err != nil { + panic(err) + } + staticFS = http.FS(subFS) +} + +func ServeStatic(w http.ResponseWriter, r *http.Request) { + urlPath := r.URL.Path + + if urlPath == "/" || urlPath == "/index.html" || strings.HasPrefix(urlPath, "/assets/") { + if urlPath == "/" { + urlPath = "/index.html" + } + + filePath := strings.TrimPrefix(urlPath, "/") + f, err := staticFS.Open(filePath) + if err != nil { + serveIndexHTML(w) + return + } + defer f.Close() + + stat, err := f.Stat() + if err != nil { + serveIndexHTML(w) + return + } + + if stat.IsDir() { + serveIndexHTML(w) + return + } + + setContentType(w, path.Ext(filePath)) + http.ServeContent(w, r, filePath, stat.ModTime(), f) + return + } + + serveIndexHTML(w) +} + +func serveIndexHTML(w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache") + data, _ := StaticFiles.ReadFile("dist/index.html") + w.Write(data) +} + +func setContentType(w http.ResponseWriter, ext string) { + switch ext { + case ".html": + w.Header().Set("Content-Type", "text/html; charset=utf-8") + case ".css": + w.Header().Set("Content-Type", "text/css; charset=utf-8") + case ".js": + w.Header().Set("Content-Type", "application/javascript; charset=utf-8") + case ".json": + w.Header().Set("Content-Type", "application/json; charset=utf-8") + case ".png": + w.Header().Set("Content-Type", "image/png") + case ".jpg", ".jpeg": + w.Header().Set("Content-Type", "image/jpeg") + case ".gif": + w.Header().Set("Content-Type", "image/gif") + case ".svg": + w.Header().Set("Content-Type", "image/svg+xml") + case ".ico": + w.Header().Set("Content-Type", "image/x-icon") + case ".woff": + w.Header().Set("Content-Type", "font/woff") + case ".woff2": + w.Header().Set("Content-Type", "font/woff2") + case ".ttf": + w.Header().Set("Content-Type", "font/ttf") + case ".eot": + w.Header().Set("Content-Type", "application/vnd.ms-fontobject") + default: + w.Header().Set("Content-Type", "application/octet-stream") + } +}