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

@@ -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

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.
type embeddedSPAHandler struct {
fs fs.FS
indexPath string 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
pkg/serverembed/dist/index.html vendored Normal file
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
pkg/serverembed/dist/vite.svg vendored Normal file
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

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

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,
} },
} },
} },
}) });