feat(arch): unify frontend and backend into single Go backend server with static export
- Configure Next.js static export (output: 'export') with client-side dynamic fetching - Implement Go static serving with SPA fallback in backend/internal/router/router.go - Unify multi-stage build in backend/Dockerfile (Node -> Go -> minimal Alpine runtime) - Simplify root docker-compose.yml to single anl-app service on port 8080 - Update REQUIREMENTS.md DM-03 and AC-17 normative contracts to v3.0 Unified Go Backend - Restore strict regression verification in Gate 9 (AC-35/36/37) and unittest suite - All Go unit tests, TypeScript type checks, ESLint, and E2E gates passed clean
This commit is contained in:
@@ -3,6 +3,9 @@ package router
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.godopu.com/lab/landing_page/backend/internal/handlers"
|
||||
"git.godopu.com/lab/landing_page/backend/internal/httpx"
|
||||
@@ -127,5 +130,54 @@ func SetupRouter(db *sql.DB, adminToken string, corsOrigins ...string) *gin.Engi
|
||||
admin.DELETE("/standard-documents/:id", standardsHandler.DeleteStandardDocument)
|
||||
}
|
||||
|
||||
// 308 Permanent Redirect for /intro -> / (H-11 / IA-02)
|
||||
r.GET("/intro", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusPermanentRedirect, "/")
|
||||
})
|
||||
|
||||
// Static assets serving
|
||||
r.Static("/_next", "./web/_next")
|
||||
r.StaticFile("/favicon.ico", "./web/favicon.ico")
|
||||
r.StaticFile("/robots.txt", "./web/robots.txt")
|
||||
r.StaticFile("/sitemap.xml", "./web/sitemap.xml")
|
||||
r.StaticFile("/icon.svg", "./web/icon.svg")
|
||||
r.Static("/fonts", "./web/fonts")
|
||||
|
||||
// NoRoute fallback for static exported HTML & 404
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// Never let a mistyped/unknown API path fall through to HTML.
|
||||
if strings.HasPrefix(path, "/api/") {
|
||||
httpx.NotFound(c, "Route not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Try an exact static file (e.g. an asset with an extension)
|
||||
candidate := filepath.Join("./web", filepath.Clean(path))
|
||||
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
|
||||
c.File(candidate)
|
||||
return
|
||||
}
|
||||
|
||||
// Try <path>/index.html — how every known page route is emitted by trailingSlash
|
||||
indexCandidate := filepath.Join("./web", filepath.Clean(path), "index.html")
|
||||
if _, err := os.Stat(indexCandidate); err == nil {
|
||||
c.File(indexCandidate)
|
||||
return
|
||||
}
|
||||
|
||||
// Genuinely unknown route: serve site's 404 page with 404 status
|
||||
notFoundPath := "./web/404/index.html"
|
||||
if _, err := os.Stat(notFoundPath); err != nil {
|
||||
notFoundPath = "./web/404.html"
|
||||
}
|
||||
if notFoundBytes, err := os.ReadFile(notFoundPath); err == nil {
|
||||
c.Data(http.StatusNotFound, "text/html; charset=utf-8", notFoundBytes)
|
||||
} else {
|
||||
c.String(http.StatusNotFound, "404 page not found")
|
||||
}
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user