This commit is contained in:
+108
-33
@@ -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 ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user