feat(serverembed): add initial HTML and SVG assets
CI / Lint (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Test (1.22) (push) Has been cancelled
CI / Test (1.23) (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
+1 -1
View File
@@ -1,7 +1,7 @@
.PHONY: build clean test lint lintfix run-server run-cli help build-ui migrate generate-models install-relspecgo seed .PHONY: build clean test lint lintfix run-server run-cli help build-ui migrate generate-models install-relspecgo seed
# Variables # Variables
FRONTEND_DIR=frontend FRONTEND_DIR=web
SQL_DIR=sql SQL_DIR=sql
MODELS_DIR=pkg/models MODELS_DIR=pkg/models
+37 -28
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"time" "time"
@@ -12,6 +13,7 @@ import (
"git.warky.dev/wdevs/whatshooked/pkg/config" "git.warky.dev/wdevs/whatshooked/pkg/config"
"git.warky.dev/wdevs/whatshooked/pkg/handlers" "git.warky.dev/wdevs/whatshooked/pkg/handlers"
"git.warky.dev/wdevs/whatshooked/pkg/models" "git.warky.dev/wdevs/whatshooked/pkg/models"
"git.warky.dev/wdevs/whatshooked/pkg/serverembed"
"git.warky.dev/wdevs/whatshooked/pkg/storage" "git.warky.dev/wdevs/whatshooked/pkg/storage"
"github.com/bitechdev/ResolveSpec/pkg/common" "github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database" "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 // Add custom routes (login, logout, etc.) on main router
SetupCustomRoutes(router, secProvider, db) SetupCustomRoutes(router, secProvider, db)
// Add static file serving for React app at /ui/ route // Serve React SPA from the embedded filesystem at /ui/
// Serve React app from configurable filesystem path distFS, err := fs.Sub(serverembed.RootEmbedFS, "dist")
spa := spaHandler{staticPath: cfg.Server.UIPath, indexPath: "index.html"} 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))
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 "" return ""
} }
// spaHandler implements the http.Handler interface for serving a SPA // embeddedSPAHandler serves a React SPA from an embedded fs.FS.
type spaHandler struct { // Static assets are served directly; all other paths fall back to index.html
staticPath string // to support client-side routing.
indexPath string type embeddedSPAHandler struct {
fs fs.FS
indexPath string
} }
// ServeHTTP inspects the URL path to locate a file within the static dir func (h embeddedSPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 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
path := r.URL.Path 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 if path == "" {
info, err := http.Dir(h.staticPath).Open(path) path = h.indexPath
}
// Try to open the requested path in the embedded FS.
f, err := h.fs.Open(path)
if err != nil { if err != nil {
// File does not exist, serve index.html for client-side routing // Not found — serve index.html for client-side routing.
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath) r2 := r.Clone(r.Context())
r2.URL.Path = "/" + h.indexPath
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r2)
return return
} }
defer info.Close() defer f.Close()
// Check if path is a directory stat, err := f.Stat()
stat, err := info.Stat() if err != nil || stat.IsDir() {
if err != nil { r2 := r.Clone(r.Context())
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath) r2.URL.Path = "/" + h.indexPath
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r2)
return return
} }
if stat.IsDir() { // Serve the real file.
// Serve index.html for directories http.FileServer(http.FS(h.fs)).ServeHTTP(w, r)
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
return
}
// Otherwise, serve the file
http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
} }
// corsMiddleware adds CORS headers to allow frontend requests // corsMiddleware adds CORS headers to allow frontend requests
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/ui/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>web</title>
<script type="module" crossorigin src="/ui/assets/index-D_NQzvuP.js"></script>
<link rel="stylesheet" crossorigin href="/ui/assets/index-Bfia8Lvm.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1 -1
View File
@@ -4,5 +4,5 @@ import (
"embed" "embed"
) )
//go:embed dist/** readme //go:embed all:dist readme
var RootEmbedFS embed.FS var RootEmbedFS embed.FS
+15 -11
View File
@@ -1,21 +1,25 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import react from '@vitejs/plugin-react' import react from "@vitejs/plugin-react";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
base: '/ui/', base: "/ui/",
build: {
outDir: "../pkg/serverembed/dist",
emptyOutDir: true,
},
server: { server: {
port: 3000, port: 3000,
proxy: { proxy: {
'/api': { "/api": {
target: 'http://localhost:8080', target: "http://localhost:8080",
changeOrigin: true, changeOrigin: true,
}, },
'/health': { "/health": {
target: 'http://localhost:8080', target: "http://localhost:8080",
changeOrigin: true, changeOrigin: true,
} },
} },
} },
}) });