feat: add favicon settings to site configuration

- Add Favicon field to SiteSetting model with FaviconIsURL() helper
- Implement favicon upload and management in site settings page
- Support both external URL and local file upload for favicon
- Add favicon display in page header with automatic URL detection
- Add i18n translations for favicon settings (EN/ZH)
- Update middleware and helpers to pass favicon data to templates
- Include migration script for existing databases

Users can now customize their site's favicon from /admin/settings/site
by either providing an external URL or uploading a local image file
(recommended formats: .ico, .png, .svg, 16x16 or 32x32 pixels).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 20:09:05 +08:00
co-authored by Claude Fable 5
parent da7a39c1c8
commit b600eac21c
9 changed files with 193 additions and 0 deletions
+41
View File
@@ -74,6 +74,7 @@ func SiteSettingsPage(db *gorm.DB) gin.HandlerFunc {
// Pre-compute derived values so the template never invokes methods on
// an interface{}-wrapped struct (which Go templates cannot resolve).
data["SiteLogoIsURL"] = s.LogoIsURL()
data["SiteFaviconIsURL"] = s.FaviconIsURL()
if msg := c.Query("saved"); msg == "1" {
data["Success"] = tr["settings_saved"]
}
@@ -101,6 +102,46 @@ func SiteSettingsSave(db *gorm.DB, storagePath string) gin.HandlerFunc {
s.FooterTextEn = strings.TrimSpace(c.PostForm("footer_text_en"))
s.UpdatedBy = userIDFromSession(c)
// Favicon upload (optional). A favicon_url form field takes precedence over an
// uploaded file, so admins can set either a local file or an external link.
if faviconURL := strings.TrimSpace(c.PostForm("favicon_url")); faviconURL != "" {
s.Favicon = faviconURL
} else if file, header, err := c.Request.FormFile("favicon"); err == nil {
defer file.Close()
check := ValidateUpload(header)
if !check.OK || check.Type.Category != models.CategoryImage {
c.Redirect(http.StatusFound, "/admin/settings/site")
return
}
logoDir := filepath.Join(storagePath, "logos")
os.MkdirAll(logoDir, 0755)
// Remove the previous local favicon (skip external URLs).
if s.Favicon != "" && !s.FaviconIsURL() {
os.Remove(filepath.Join(logoDir, s.Favicon))
}
ext := strings.ToLower(filepath.Ext(header.Filename))
savedName := fmt.Sprintf("favicon%s", ext)
dst, err := os.Create(filepath.Join(logoDir, savedName))
if err != nil {
c.Redirect(http.StatusFound, "/admin/settings/site")
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
c.Redirect(http.StatusFound, "/admin/settings/site")
return
}
s.Favicon = savedName
}
// Remove favicon entirely if requested.
if c.PostForm("favicon_clear") == "1" {
if s.Favicon != "" && !s.FaviconIsURL() {
os.Remove(filepath.Join(storagePath, "logos", s.Favicon))
}
s.Favicon = ""
}
// Logo upload (optional). A logo_url form field takes precedence over an
// uploaded file, so admins can set either a local file or an external link.
if logoURL := strings.TrimSpace(c.PostForm("logo_url")); logoURL != "" {