2
0

feat(pages): add logo and favicon upload to pages brand settings

Add ability to upload logo and favicon images directly instead of only using URLs. Images stored in repo-avatars storage with hash-based filenames (max 5 MB, JPG/PNG/WebP/GIF). Uploaded images take priority over URL fields. Includes upload/delete endpoints for both logo and favicon, UI with previews, and ResolvedLogoURL/ResolvedFaviconURL helper methods. Updates base_head template to use resolved favicon URL.
This commit is contained in:
2026-03-15 22:45:29 -04:00
parent 79a0c2683e
commit fc86952bf4
8 changed files with 280 additions and 10 deletions

View File

@@ -297,7 +297,7 @@ func resolveLogoURL(ctx *context.Context, repo *repo_model.Repository, config *p
}
return repo.Owner.AvatarLink(ctx)
default: // "url" or empty
return config.Brand.LogoURL
return config.Brand.ResolvedLogoURL()
}
}

View File

@@ -246,9 +246,13 @@ func PagesBrand(ctx *context.Context) {
func PagesBrandPost(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
config.Brand.Name = ctx.FormString("brand_name")
config.Brand.LogoURL = ctx.FormString("brand_logo_url")
config.Brand.Tagline = ctx.FormString("brand_tagline")
config.Brand.FaviconURL = ctx.FormString("brand_favicon_url")
if config.Brand.UploadedLogo == "" {
config.Brand.LogoURL = ctx.FormString("brand_logo_url")
}
if config.Brand.UploadedFavicon == "" {
config.Brand.FaviconURL = ctx.FormString("brand_favicon_url")
}
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
@@ -257,6 +261,160 @@ func PagesBrandPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/brand")
}
const maxBrandImageSize = 5 * 1024 * 1024 // 5 MB
// PagesBrandUploadLogo handles logo image file upload.
func PagesBrandUploadLogo(ctx *context.Context) {
repo := ctx.Repo.Repository
file, header, err := ctx.Req.FormFile("brand_logo")
if err != nil {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_upload_error"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
return
}
defer file.Close()
if header.Size > maxBrandImageSize {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_image_too_large"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
return
}
data, err := io.ReadAll(file)
if err != nil {
ctx.ServerError("ReadAll", err)
return
}
st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_not_an_image"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
return
}
hash := sha256.Sum256(data)
filename := fmt.Sprintf("pages-logo-%d-%x", repo.ID, hash[:8])
config := getPagesLandingConfig(ctx)
if config.Brand.UploadedLogo != "" {
_ = storage.RepoAvatars.Delete(config.Brand.UploadedLogo)
}
if err := storage.SaveFrom(storage.RepoAvatars, filename, func(w io.Writer) error {
_, err := w.Write(data)
return err
}); err != nil {
ctx.ServerError("SaveFrom", err)
return
}
config.Brand.UploadedLogo = filename
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.brand_logo_uploaded"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
}
// PagesBrandDeleteLogo removes the uploaded logo image.
func PagesBrandDeleteLogo(ctx *context.Context) {
repo := ctx.Repo.Repository
config := getPagesLandingConfig(ctx)
if config.Brand.UploadedLogo != "" {
_ = storage.RepoAvatars.Delete(config.Brand.UploadedLogo)
config.Brand.UploadedLogo = ""
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.brand_logo_deleted"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
}
// PagesBrandUploadFavicon handles favicon file upload.
func PagesBrandUploadFavicon(ctx *context.Context) {
repo := ctx.Repo.Repository
file, header, err := ctx.Req.FormFile("brand_favicon")
if err != nil {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_upload_error"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
return
}
defer file.Close()
if header.Size > maxBrandImageSize {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_image_too_large"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
return
}
data, err := io.ReadAll(file)
if err != nil {
ctx.ServerError("ReadAll", err)
return
}
st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_not_an_image"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
return
}
hash := sha256.Sum256(data)
filename := fmt.Sprintf("pages-favicon-%d-%x", repo.ID, hash[:8])
config := getPagesLandingConfig(ctx)
if config.Brand.UploadedFavicon != "" {
_ = storage.RepoAvatars.Delete(config.Brand.UploadedFavicon)
}
if err := storage.SaveFrom(storage.RepoAvatars, filename, func(w io.Writer) error {
_, err := w.Write(data)
return err
}); err != nil {
ctx.ServerError("SaveFrom", err)
return
}
config.Brand.UploadedFavicon = filename
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.brand_favicon_uploaded"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
}
// PagesBrandDeleteFavicon removes the uploaded favicon.
func PagesBrandDeleteFavicon(ctx *context.Context) {
repo := ctx.Repo.Repository
config := getPagesLandingConfig(ctx)
if config.Brand.UploadedFavicon != "" {
_ = storage.RepoAvatars.Delete(config.Brand.UploadedFavicon)
config.Brand.UploadedFavicon = ""
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.brand_favicon_deleted"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
}
func PagesHero(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.hero")
ctx.Data["PageIsSettingsPages"] = true

View File

@@ -1333,6 +1333,10 @@ func registerWebRoutes(m *web.Router) {
m.Group("/pages", func() {
m.Combo("").Get(repo_setting.Pages).Post(repo_setting.PagesPost)
m.Combo("/brand").Get(repo_setting.PagesBrand).Post(repo_setting.PagesBrandPost)
m.Post("/brand/upload_logo", repo_setting.PagesBrandUploadLogo)
m.Post("/brand/delete_logo", repo_setting.PagesBrandDeleteLogo)
m.Post("/brand/upload_favicon", repo_setting.PagesBrandUploadFavicon)
m.Post("/brand/delete_favicon", repo_setting.PagesBrandDeleteFavicon)
m.Combo("/hero").Get(repo_setting.PagesHero).Post(repo_setting.PagesHeroPost)
m.Post("/hero/upload_image", repo_setting.PagesHeroUploadImage)
m.Post("/hero/delete_image", repo_setting.PagesHeroDeleteImage)