a73x

internal/server/web/spa.go

Ref:   Size: 1.2 KiB   History

// Package web embeds the built SvelteKit single-page app and serves it with
// SPA-style fallback (unknown paths resolve to index.html for client routing).
package web

import (
	"io/fs"
	"net/http"
	"path"
	"strings"
)

// spaHandler serves static files from fsys, falling back to index.html for any
// path that doesn't resolve to a real file (so client-side routes like
// /vms/abc load the app). If index.html is absent (UI not built yet) it returns
// a clear 503 instead of a confusing 404.
func spaHandler(fsys fs.FS) http.Handler {
	fileServer := http.FileServerFS(fsys)
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		p := strings.TrimPrefix(path.Clean("/"+r.URL.Path), "/")
		if p != "" && fileExists(fsys, p) {
			fileServer.ServeHTTP(w, r)
			return
		}
		if !fileExists(fsys, "index.html") {
			http.Error(w, "UI not built — run `make web`", http.StatusServiceUnavailable)
			return
		}
		// SPA fallback: serve index.html for unknown (client-routed) paths.
		r2 := r.Clone(r.Context())
		r2.URL.Path = "/"
		fileServer.ServeHTTP(w, r2)
	})
}

func fileExists(fsys fs.FS, name string) bool {
	f, err := fsys.Open(name)
	if err != nil {
		return false
	}
	defer f.Close()
	st, err := f.Stat()
	return err == nil && !st.IsDir()
}