From 94419f434c782c62509a50b0ae485ccd2abab8ab Mon Sep 17 00:00:00 2001 From: wucm667 Date: Wed, 18 Mar 2026 14:02:00 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E7=9B=B4=E6=8E=A5=E8=AE=BF=E9=97=AE?= =?UTF-8?q?=E6=88=96=E5=88=B7=E6=96=B0=E9=A1=B5=E9=9D=A2=E6=97=B6=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E6=A0=87=E7=AD=BE=E9=A1=B5=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E7=AB=99=E7=82=B9=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端 HTML 注入时同步替换 标签为自定义站点名称, 前端 fetchPublicSettings 完成后重新设置 document.title, 解决路由守卫先于设置加载导致标题回退为默认值的时序问题。 --- backend/internal/web/embed_on.go | 32 +++++++++++++++++++++++++++++++- frontend/src/App.vue | 4 ++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/backend/internal/web/embed_on.go b/backend/internal/web/embed_on.go index 41ce4d48..ffca98a5 100644 --- a/backend/internal/web/embed_on.go +++ b/backend/internal/web/embed_on.go @@ -180,7 +180,37 @@ func (s *FrontendServer) injectSettings(settingsJSON []byte) []byte { // Inject before </head> headClose := []byte("</head>") - return bytes.Replace(s.baseHTML, headClose, append(script, headClose...), 1) + result := bytes.Replace(s.baseHTML, headClose, append(script, headClose...), 1) + + // Replace <title> with custom site name so the browser tab shows it immediately + result = injectSiteTitle(result, settingsJSON) + + return result +} + +// injectSiteTitle replaces the static <title> in HTML with the configured site name. +// This ensures the browser tab shows the correct title before JS executes. +func injectSiteTitle(html, settingsJSON []byte) []byte { + var cfg struct { + SiteName string `json:"site_name"` + } + if err := json.Unmarshal(settingsJSON, &cfg); err != nil || cfg.SiteName == "" { + return html + } + + // Find and replace the existing <title>... + titleStart := bytes.Index(html, []byte("")) + titleEnd := bytes.Index(html, []byte("")) + if titleStart == -1 || titleEnd == -1 || titleEnd <= titleStart { + return html + } + + newTitle := []byte("" + cfg.SiteName + " - AI API Gateway") + var buf bytes.Buffer + buf.Write(html[:titleStart]) + buf.Write(newTitle) + buf.Write(html[titleEnd+len(""):]) + return buf.Bytes() } // replaceNoncePlaceholder replaces the nonce placeholder with actual nonce value diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 4fc6a7c8..7485aa1a 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -3,6 +3,7 @@ import { RouterView, useRouter, useRoute } from 'vue-router' import { onMounted, onBeforeUnmount, watch } from 'vue' import Toast from '@/components/common/Toast.vue' import NavigationProgress from '@/components/common/NavigationProgress.vue' +import { resolveDocumentTitle } from '@/router/title' import AnnouncementPopup from '@/components/common/AnnouncementPopup.vue' import { useAppStore, useAuthStore, useSubscriptionStore, useAnnouncementStore } from '@/stores' import { getSetupStatus } from '@/api/setup' @@ -104,6 +105,9 @@ onMounted(async () => { // Load public settings into appStore (will be cached for other components) await appStore.fetchPublicSettings() + + // Re-resolve document title now that siteName is available + document.title = resolveDocumentTitle(route.meta.title, appStore.siteName, route.meta.titleKey as string) }) From 6028efd26cf2e4855bdaee11359ddff379a06146 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Wed, 18 Mar 2026 14:13:52 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=20injectSiteTitl?= =?UTF-8?q?e=20=E5=87=BD=E6=95=B0=E7=9A=84=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/web/embed_test.go | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/backend/internal/web/embed_test.go b/backend/internal/web/embed_test.go index f270b624..fd47c4da 100644 --- a/backend/internal/web/embed_test.go +++ b/backend/internal/web/embed_test.go @@ -20,6 +20,78 @@ func init() { gin.SetMode(gin.TestMode) } +func TestInjectSiteTitle(t *testing.T) { + t.Run("replaces_title_with_site_name", func(t *testing.T) { + html := []byte(`Sub2API - AI API Gateway`) + settingsJSON := []byte(`{"site_name":"MyCustomSite"}`) + + result := injectSiteTitle(html, settingsJSON) + + assert.Contains(t, string(result), "MyCustomSite - AI API Gateway") + assert.NotContains(t, string(result), "Sub2API") + }) + + t.Run("returns_unchanged_when_site_name_empty", func(t *testing.T) { + html := []byte(`Sub2API - AI API Gateway`) + settingsJSON := []byte(`{"site_name":""}`) + + result := injectSiteTitle(html, settingsJSON) + + assert.Equal(t, string(html), string(result)) + }) + + t.Run("returns_unchanged_when_site_name_missing", func(t *testing.T) { + html := []byte(`Sub2API - AI API Gateway`) + settingsJSON := []byte(`{"other_field":"value"}`) + + result := injectSiteTitle(html, settingsJSON) + + assert.Equal(t, string(html), string(result)) + }) + + t.Run("returns_unchanged_when_invalid_json", func(t *testing.T) { + html := []byte(`Sub2API - AI API Gateway`) + settingsJSON := []byte(`{invalid json}`) + + result := injectSiteTitle(html, settingsJSON) + + assert.Equal(t, string(html), string(result)) + }) + + t.Run("returns_unchanged_when_no_title_tag", func(t *testing.T) { + html := []byte(``) + settingsJSON := []byte(`{"site_name":"MyCustomSite"}`) + + result := injectSiteTitle(html, settingsJSON) + + assert.Equal(t, string(html), string(result)) + }) + + t.Run("returns_unchanged_when_title_has_attributes", func(t *testing.T) { + // The function looks for "" literally, so attributes are not supported + // This is acceptable since index.html uses plain <title> without attributes + html := []byte(`<html><head><title lang="en">Sub2API`) + settingsJSON := []byte(`{"site_name":"NewSite"}`) + + result := injectSiteTitle(html, settingsJSON) + + // Should return unchanged since with attributes is not matched + assert.Equal(t, string(html), string(result)) + }) + + t.Run("preserves_rest_of_html", func(t *testing.T) { + html := []byte(`<html><head><meta charset="UTF-8"><title>Sub2API
`) + settingsJSON := []byte(`{"site_name":"TestSite"}`) + + result := injectSiteTitle(html, settingsJSON) + + assert.Contains(t, string(result), ``) + assert.Contains(t, string(result), ``) + assert.Contains(t, string(result), `
`) + assert.Contains(t, string(result), "TestSite - AI API Gateway") + }) +} + func TestReplaceNoncePlaceholder(t *testing.T) { t.Run("replaces_single_placeholder", func(t *testing.T) { html := []byte(``)