fix: prevent custom branding flash on initial load

This commit is contained in:
weiness
2026-07-18 00:24:06 +08:00
parent bc2244c83f
commit 500f241692
8 changed files with 199 additions and 34 deletions
+54 -1
View File
@@ -11,6 +11,7 @@ import (
"io"
"io/fs"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
@@ -209,12 +210,64 @@ func (s *FrontendServer) injectSettings(settingsJSON []byte) []byte {
headClose := []byte("</head>")
result := bytes.Replace(s.baseHTML, headClose, append(script, headClose...), 1)
// Replace <title> with custom site name so the browser tab shows it immediately
// Apply custom branding before the browser paints the static defaults.
result = injectSiteTitle(result, settingsJSON)
result = injectSiteFavicon(result, settingsJSON)
return result
}
// injectSiteFavicon replaces the static favicon with a configured, browser-safe image URL.
func injectSiteFavicon(html, settingsJSON []byte) []byte {
var cfg struct {
SiteLogo string `json:"site_logo"`
}
if err := json.Unmarshal(settingsJSON, &cfg); err != nil {
return html
}
logoURL := safeImageURL(cfg.SiteLogo)
if logoURL == "" {
return html
}
linkStart := bytes.Index(html, []byte(`<link rel="icon"`))
if linkStart == -1 {
return html
}
linkEndOffset := bytes.IndexByte(html[linkStart:], '>')
if linkEndOffset == -1 {
return html
}
linkEnd := linkStart + linkEndOffset + 1
replacement := []byte(`<link rel="icon" href="` + htmlpkg.EscapeString(logoURL) + `" />`)
var buf bytes.Buffer
buf.Write(html[:linkStart])
buf.Write(replacement)
buf.Write(html[linkEnd:])
return buf.Bytes()
}
func safeImageURL(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return ""
}
if strings.HasPrefix(trimmed, "/") && !strings.HasPrefix(trimmed, "//") {
return trimmed
}
if strings.HasPrefix(strings.ToLower(trimmed), "data:image/") {
return trimmed
}
parsed, err := url.Parse(trimmed)
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
return ""
}
return trimmed
}
// 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 {
+35
View File
@@ -114,6 +114,41 @@ func TestInjectSiteTitle(t *testing.T) {
})
}
func TestInjectSiteFavicon(t *testing.T) {
t.Run("replaces_favicon_with_site_logo", func(t *testing.T) {
html := []byte(`<html><head><link rel="icon" type="image/png" href="/logo.png" /></head></html>`)
settingsJSON := []byte(`{"site_logo":"https://example.com/custom-logo.png"}`)
result := injectSiteFavicon(html, settingsJSON)
assert.Contains(t, string(result), `<link rel="icon" href="https://example.com/custom-logo.png" />`)
assert.NotContains(t, string(result), `/logo.png`)
})
t.Run("supports_relative_and_data_image_urls", func(t *testing.T) {
html := []byte(`<link rel="icon" href="/logo.png" />`)
assert.Contains(t, string(injectSiteFavicon(html, []byte(`{"site_logo":"/uploads/logo.svg"}`))), `/uploads/logo.svg`)
assert.Contains(t, string(injectSiteFavicon(html, []byte(`{"site_logo":"data:image/png;base64,abc"}`))), `data:image/png;base64,abc`)
})
t.Run("rejects_unsafe_logo_urls", func(t *testing.T) {
html := []byte(`<link rel="icon" href="/logo.png" />`)
result := injectSiteFavicon(html, []byte(`{"site_logo":"javascript:alert(1)"}`))
assert.Equal(t, string(html), string(result))
})
t.Run("escapes_logo_url_for_html", func(t *testing.T) {
html := []byte(`<link rel="icon" href="/logo.png" />`)
result := injectSiteFavicon(html, []byte(`{"site_logo":"https://example.com/logo.png?a=1&b=2"}`))
assert.Contains(t, string(result), `a=1&amp;b=2`)
})
}
func TestReplaceNoncePlaceholder(t *testing.T) {
t.Run("replaces_single_placeholder", func(t *testing.T) {
html := []byte(`<script nonce="__CSP_NONCE_VALUE__">console.log('test');</script>`)