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:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user