new frontend
Build and Push App Image / build-and-push (push) Successful in 3m38s

This commit is contained in:
domrichardson
2026-06-17 12:08:20 +01:00
parent b690b00016
commit ead8219f3b
40 changed files with 10149 additions and 37 deletions
+108 -33
View File
@@ -6,6 +6,7 @@ import (
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -358,39 +359,9 @@ func main() {
admin.HandleFunc("/auth/providers/{providerId}", authHandler.UpdateProvider).Methods("PUT")
admin.HandleFunc("/auth/providers/{providerId}", adminHandler.DeleteProvider).Methods("DELETE")
// Serve static files (frontend) for all other routes
// This must be after all API route handlers to allow API routes to take precedence
router.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// List of static file extensions to serve directly
staticExts := map[string]bool{
".js": true, ".css": true, ".svg": true, ".png": true,
".jpg": true, ".jpeg": true, ".gif": true, ".ico": true,
".woff": true, ".woff2": true, ".ttf": true, ".eot": true,
}
filePath := "./public" + r.URL.Path
if r.URL.Path == "/" {
filePath = "./public/index.html"
}
// Check if it's a static file (has an extension in staticExts)
isStatic := false
for ext := range staticExts {
if len(r.URL.Path) > len(ext) {
if r.URL.Path[len(r.URL.Path)-len(ext):] == ext {
isStatic = true
break
}
}
}
// If it doesn't look like a static file, serve index.html (SPA routing)
if !isStatic {
filePath = "./public/index.html"
}
http.ServeFile(w, r, filePath)
})
// Serve static files (NextJS frontend) for all other routes.
// Must come after all API route handlers.
router.PathPrefix("/").HandlerFunc(serveNextJS)
// Start server
server := &http.Server{
@@ -529,3 +500,107 @@ func ensureDefaultAdminUser(
log.Printf("default admin user synchronized from environment: %s", adminEmail)
return nil
}
// serveNextJS serves the NextJS static export from ./public.
// It handles /_next/ assets directly, tries {path}/index.html for page routes,
// and falls back to dynamic route pattern matching (NextJS [param] folders).
func serveNextJS(w http.ResponseWriter, r *http.Request) {
const publicDir = "./public"
urlPath := r.URL.Path
// ── /_next/ and other static assets ─────────────────────────────────────
if strings.HasPrefix(urlPath, "/_next/") {
http.ServeFile(w, r, filepath.Join(publicDir, filepath.FromSlash(urlPath)))
return
}
staticExts := []string{
".js", ".css", ".svg", ".png", ".jpg", ".jpeg", ".gif", ".ico",
".woff", ".woff2", ".ttf", ".eot", ".json", ".map", ".txt", ".xml",
}
for _, ext := range staticExts {
if strings.HasSuffix(urlPath, ext) {
http.ServeFile(w, r, filepath.Join(publicDir, filepath.FromSlash(urlPath)))
return
}
}
// ── HTML page resolution ─────────────────────────────────────────────────
// NextJS static export with trailingSlash:true produces {path}/index.html
cleanPath := strings.TrimRight(urlPath, "/")
if cleanPath == "" {
cleanPath = "/"
}
// 1. Try exact {path}/index.html
candidate := filepath.Join(publicDir, filepath.FromSlash(cleanPath), "index.html")
if _, err := os.Stat(candidate); err == nil {
http.ServeFile(w, r, candidate)
return
}
// 2. Try {path}.html
candidate = filepath.Join(publicDir, filepath.FromSlash(cleanPath)+".html")
if _, err := os.Stat(candidate); err == nil {
http.ServeFile(w, r, candidate)
return
}
// 3. Walk path segments and try NextJS dynamic [param] folders
segments := strings.Split(strings.Trim(cleanPath, "/"), "/")
if result := findNextJSPage(publicDir, segments); result != "" {
http.ServeFile(w, r, result)
return
}
// 4. Fallback: root index.html
http.ServeFile(w, r, filepath.Join(publicDir, "index.html"))
}
// findNextJSPage recursively searches the public directory for an HTML file
// that matches the given URL segments, replacing unknown segments with any
// NextJS dynamic directory ([param] named folders).
func findNextJSPage(publicDir string, segments []string) string {
return searchNextJSSegments(publicDir, segments, 0, publicDir)
}
func searchNextJSSegments(publicDir string, segments []string, idx int, currentDir string) string {
if idx == len(segments) {
candidate := filepath.Join(currentDir, "index.html")
if _, err := os.Stat(candidate); err == nil {
return candidate
}
return ""
}
seg := segments[idx]
// Try the exact segment directory
exactDir := filepath.Join(currentDir, seg)
if result := searchNextJSSegments(publicDir, segments, idx+1, exactDir); result != "" {
return result
}
// Try NextJS dynamic segment directories.
// Matches both [param] (source convention) and __param__ (static export convention).
entries, err := os.ReadDir(currentDir)
if err != nil {
return ""
}
for _, entry := range entries {
name := entry.Name()
if !entry.IsDir() {
continue
}
isBracket := len(name) > 2 && name[0] == '[' && name[len(name)-1] == ']'
isPlaceholder := len(name) >= 4 && strings.HasPrefix(name, "__") && strings.HasSuffix(name, "__")
if isBracket || isPlaceholder {
dynamicDir := filepath.Join(currentDir, name)
if result := searchNextJSSegments(publicDir, segments, idx+1, dynamicDir); result != "" {
return result
}
}
}
return ""
}