fix(frontend): serve zone detail static export fallback

This commit is contained in:
ryan
2026-07-12 18:50:13 +08:00
parent 50717d1baf
commit c624512da6
11 changed files with 167 additions and 29 deletions
+10 -1
View File
@@ -101,7 +101,16 @@ func init() {
}
}
// 4. 单页应用(SPA)前端路由兜底:返回 index.html
// 4. Next.js static export dynamic route fallback. Runtime IDs cannot be
// enumerated at build time, so serve the generated route shell instead of
// falling back to the dashboard index.html.
if fallbackPath, ok := resolveNextExportDynamicFallback(subFS, cleanPath); ok {
if serveFileDirect(c, subFS, fallbackPath) {
return
}
}
// 5. 单页应用(SPA)前端路由兜底:返回 index.html
if serveFileDirect(c, subFS, "index.html") {
return
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package root
import (
"io/fs"
"strings"
)
//nolint:unused // Used by frontend.go in embed_frontend builds; default lint runs without that tag.
func resolveNextExportDynamicFallback(subFS fs.FS, cleanPath string) (string, bool) {
parts := strings.Split(cleanPath, "/")
if len(parts) < 2 || parts[0] != "websites" || parts[1] == "" {
return "", false
}
var templatePath string
switch {
case len(parts) == 2 && !strings.Contains(parts[1], "."):
templatePath = "websites/1.html"
case len(parts) == 2 && strings.HasSuffix(parts[1], ".txt"):
templatePath = "websites/1.txt"
case len(parts) == 3 && parts[2] != "" && strings.HasPrefix(parts[2], "__next.") && strings.HasSuffix(parts[2], ".txt"):
templatePath = "websites/1/" + parts[2]
default:
return "", false
}
if _, err := fs.Stat(subFS, templatePath); err != nil {
return "", false
}
return templatePath, true
}
@@ -0,0 +1,74 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package root
import (
"testing"
"testing/fstest"
)
func TestResolveNextExportDynamicFallbackUsesZoneTemplate(t *testing.T) {
subFS := fstest.MapFS{
"index.html": &fstest.MapFile{Data: []byte("dashboard")},
"websites/1.html": &fstest.MapFile{Data: []byte("zone detail")},
"websites/1.txt": &fstest.MapFile{Data: []byte("zone flight")},
"websites/1/__next.!KG1haW4p.websites.$d$zoneId.__PAGE__.txt": &fstest.MapFile{Data: []byte("zone segment")},
}
tests := []struct {
name string
input string
want string
}{
{name: "html", input: "websites/3", want: "websites/1.html"},
{name: "route payload", input: "websites/3.txt", want: "websites/1.txt"},
{
name: "segment payload",
input: "websites/3/__next.!KG1haW4p.websites.$d$zoneId.__PAGE__.txt",
want: "websites/1/__next.!KG1haW4p.websites.$d$zoneId.__PAGE__.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := resolveNextExportDynamicFallback(subFS, tt.input)
if !ok {
t.Fatal("expected dynamic websites route fallback")
}
if got != tt.want {
t.Fatalf("expected %q fallback, got %q", tt.want, got)
}
})
}
}
func TestResolveNextExportDynamicFallbackRejectsNestedOrAssetPath(t *testing.T) {
subFS := fstest.MapFS{
"websites/1.html": &fstest.MapFile{Data: []byte("zone detail")},
}
tests := []string{
"websites",
"websites/3/settings",
"websites/3.js",
"websites/3/",
"websites/3/missing.txt",
}
for _, tt := range tests {
t.Run(tt, func(t *testing.T) {
if got, ok := resolveNextExportDynamicFallback(subFS, tt); ok {
t.Fatalf("expected no fallback, got %q", got)
}
})
}
}
func TestResolveNextExportDynamicFallbackRequiresGeneratedTemplate(t *testing.T) {
subFS := fstest.MapFS{
"index.html": &fstest.MapFile{Data: []byte("dashboard")},
}
if got, ok := resolveNextExportDynamicFallback(subFS, "websites/3"); ok {
t.Fatalf("expected no fallback without generated template, got %q", got)
}
}