feat: embed React frontend in Go binary with SPA fallback

This commit is contained in:
2026-05-21 21:39:07 +00:00
parent 74d6130dd5
commit 6b6b66ab89
3 changed files with 87 additions and 0 deletions
+41
View File
@@ -3,7 +3,9 @@ package main
import (
"context"
"embed"
"fmt"
"io/fs"
"log"
"net/http"
"os"
@@ -22,6 +24,9 @@ import (
"gopkg.in/yaml.v3"
)
//go:embed all:src/dist
var frontendFS embed.FS
// Config holds the application configuration.
type Config struct {
DBPath string `yaml:"db_path"`
@@ -84,6 +89,9 @@ func main() {
// API routes (auth required if API key is configured)
r.Mount("/api/v1", auth.Middleware(cfg.APIKey)(apiRouter(sseHub, sqlDB)))
// Serve embedded React frontend with SPA fallback
r.Mount("/", frontendHandler())
// Create server
httpServer := &http.Server{
Addr: ":" + cfg.Port,
@@ -158,4 +166,37 @@ func loadConfig(path string) (*Config, error) {
}
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)
})
}