generated from CubeCraft-Creations/Tracehound
feat: embed React frontend in Go binary with SPA fallback
This commit is contained in:
@@ -13,6 +13,10 @@ dist
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Frontend build output (embedded at Go build time)
|
||||||
|
# Allow the fallback placeholder so embed always has at least index.html
|
||||||
|
!src/dist/index.html
|
||||||
|
|
||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -22,6 +24,9 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed all:src/dist
|
||||||
|
var frontendFS embed.FS
|
||||||
|
|
||||||
// Config holds the application configuration.
|
// Config holds the application configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DBPath string `yaml:"db_path"`
|
DBPath string `yaml:"db_path"`
|
||||||
@@ -84,6 +89,9 @@ func main() {
|
|||||||
// API routes (auth required if API key is configured)
|
// API routes (auth required if API key is configured)
|
||||||
r.Mount("/api/v1", auth.Middleware(cfg.APIKey)(apiRouter(sseHub, sqlDB)))
|
r.Mount("/api/v1", auth.Middleware(cfg.APIKey)(apiRouter(sseHub, sqlDB)))
|
||||||
|
|
||||||
|
// Serve embedded React frontend with SPA fallback
|
||||||
|
r.Mount("/", frontendHandler())
|
||||||
|
|
||||||
// Create server
|
// Create server
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Addr: ":" + cfg.Port,
|
Addr: ":" + cfg.Port,
|
||||||
@@ -158,4 +166,37 @@ func loadConfig(path string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// frontendHandler returns an http.Handler that serves the embedded React
|
||||||
|
// frontend from src/dist/ with SPA-style fallback: any path that doesn't
|
||||||
|
// match a static file serves index.html for client-side routing.
|
||||||
|
//
|
||||||
|
// The frontend is embedded via //go:embed all:src/dist at build time.
|
||||||
|
// If src/dist/ is empty or missing at build time, the embedded fallback
|
||||||
|
// index.html (committed to the repo) is served instead, showing a
|
||||||
|
// "run npm run build" message.
|
||||||
|
func frontendHandler() http.Handler {
|
||||||
|
distFS, err := fs.Sub(frontendFS, "src/dist")
|
||||||
|
if err != nil {
|
||||||
|
// Shouldn't happen if embed worked, but be defensive.
|
||||||
|
panic("embedded frontend filesystem not found: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fileServer := http.FileServer(http.FS(distFS))
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Try to serve the requested file.
|
||||||
|
f, err := distFS.Open(r.URL.Path[1:]) // strip leading "/"
|
||||||
|
if err != nil {
|
||||||
|
// File not found — serve index.html for SPA routing.
|
||||||
|
r.URL.Path = "/"
|
||||||
|
fileServer.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
// File exists, serve it.
|
||||||
|
fileServer.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Vendored
+42
@@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>RemoteRig - Frontend Not Built</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 { color: #e74c3c; margin-bottom: 0.5rem; }
|
||||||
|
code {
|
||||||
|
background: #eee;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="message">
|
||||||
|
<h1>Frontend Not Built</h1>
|
||||||
|
<p>The React frontend has not been built yet.</p>
|
||||||
|
<p>Run <code>npm run build</code> from the project root, then rebuild the Go binary.</p>
|
||||||
|
<p><small>API is still available at <code>/api/v1/</code> and health at <code>/health</code></small></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user