Merge pull request #1116 from wucm667/fix/inject-site-title-in-html
fix: 直接访问或刷新页面时浏览器标签页显示自定义站点名称
This commit is contained in:
@@ -180,7 +180,37 @@ func (s *FrontendServer) injectSettings(settingsJSON []byte) []byte {
|
|||||||
|
|
||||||
// Inject before </head>
|
// Inject before </head>
|
||||||
headClose := []byte("</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>...</title>
|
||||||
|
titleStart := bytes.Index(html, []byte("<title>"))
|
||||||
|
titleEnd := bytes.Index(html, []byte("</title>"))
|
||||||
|
if titleStart == -1 || titleEnd == -1 || titleEnd <= titleStart {
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
newTitle := []byte("<title>" + cfg.SiteName + " - AI API Gateway</title>")
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.Write(html[:titleStart])
|
||||||
|
buf.Write(newTitle)
|
||||||
|
buf.Write(html[titleEnd+len("</title>"):])
|
||||||
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceNoncePlaceholder replaces the nonce placeholder with actual nonce value
|
// replaceNoncePlaceholder replaces the nonce placeholder with actual nonce value
|
||||||
|
|||||||
@@ -20,6 +20,78 @@ func init() {
|
|||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInjectSiteTitle(t *testing.T) {
|
||||||
|
t.Run("replaces_title_with_site_name", func(t *testing.T) {
|
||||||
|
html := []byte(`<html><head><title>Sub2API - AI API Gateway</title></head><body></body></html>`)
|
||||||
|
settingsJSON := []byte(`{"site_name":"MyCustomSite"}`)
|
||||||
|
|
||||||
|
result := injectSiteTitle(html, settingsJSON)
|
||||||
|
|
||||||
|
assert.Contains(t, string(result), "<title>MyCustomSite - AI API Gateway</title>")
|
||||||
|
assert.NotContains(t, string(result), "Sub2API")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns_unchanged_when_site_name_empty", func(t *testing.T) {
|
||||||
|
html := []byte(`<html><head><title>Sub2API - AI API Gateway</title></head><body></body></html>`)
|
||||||
|
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(`<html><head><title>Sub2API - AI API Gateway</title></head><body></body></html>`)
|
||||||
|
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(`<html><head><title>Sub2API - AI API Gateway</title></head><body></body></html>`)
|
||||||
|
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(`<html><head></head><body></body></html>`)
|
||||||
|
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 "<title>" 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</title></head><body></body></html>`)
|
||||||
|
settingsJSON := []byte(`{"site_name":"NewSite"}`)
|
||||||
|
|
||||||
|
result := injectSiteTitle(html, settingsJSON)
|
||||||
|
|
||||||
|
// Should return unchanged since <title> 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</title><script src="app.js"></script></head><body><div id="app"></div></body></html>`)
|
||||||
|
settingsJSON := []byte(`{"site_name":"TestSite"}`)
|
||||||
|
|
||||||
|
result := injectSiteTitle(html, settingsJSON)
|
||||||
|
|
||||||
|
assert.Contains(t, string(result), `<meta charset="UTF-8">`)
|
||||||
|
assert.Contains(t, string(result), `<script src="app.js"></script>`)
|
||||||
|
assert.Contains(t, string(result), `<div id="app"></div>`)
|
||||||
|
assert.Contains(t, string(result), "<title>TestSite - AI API Gateway</title>")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestReplaceNoncePlaceholder(t *testing.T) {
|
func TestReplaceNoncePlaceholder(t *testing.T) {
|
||||||
t.Run("replaces_single_placeholder", func(t *testing.T) {
|
t.Run("replaces_single_placeholder", func(t *testing.T) {
|
||||||
html := []byte(`<script nonce="__CSP_NONCE_VALUE__">console.log('test');</script>`)
|
html := []byte(`<script nonce="__CSP_NONCE_VALUE__">console.log('test');</script>`)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { RouterView, useRouter, useRoute } from 'vue-router'
|
|||||||
import { onMounted, onBeforeUnmount, watch } from 'vue'
|
import { onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
import Toast from '@/components/common/Toast.vue'
|
import Toast from '@/components/common/Toast.vue'
|
||||||
import NavigationProgress from '@/components/common/NavigationProgress.vue'
|
import NavigationProgress from '@/components/common/NavigationProgress.vue'
|
||||||
|
import { resolveDocumentTitle } from '@/router/title'
|
||||||
import AnnouncementPopup from '@/components/common/AnnouncementPopup.vue'
|
import AnnouncementPopup from '@/components/common/AnnouncementPopup.vue'
|
||||||
import { useAppStore, useAuthStore, useSubscriptionStore, useAnnouncementStore } from '@/stores'
|
import { useAppStore, useAuthStore, useSubscriptionStore, useAnnouncementStore } from '@/stores'
|
||||||
import { getSetupStatus } from '@/api/setup'
|
import { getSetupStatus } from '@/api/setup'
|
||||||
@@ -104,6 +105,9 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// Load public settings into appStore (will be cached for other components)
|
// Load public settings into appStore (will be cached for other components)
|
||||||
await appStore.fetchPublicSettings()
|
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)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user