2
0

feat(theme): add homepage customization and pinned organizations

- Add customizable homepage title and tagline via admin theme settings
- Add ability for site admins to pin organizations to homepage
- Add pinned organization display format option (condensed/regular)
- Hide promotional text when pinned organizations are displayed
- Add database migration for is_homepage_pinned column
- Add custom site icon support for favicon and navbar

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
GitCaddy
2026-01-12 16:17:51 +00:00
parent ea53c3a6c1
commit 67505cb7c6
18 changed files with 506 additions and 3 deletions

View File

@@ -405,6 +405,7 @@ func prepareMigrationTasks() []*migration {
newMigration(328, "Add wiki index table for search", v1_26.AddWikiIndexTable),
newMigration(329, "Add release archive columns", v1_26.AddReleaseArchiveColumns),
newMigration(330, "Add runner capabilities column", v1_26.AddRunnerCapabilitiesColumn),
newMigration(331, "Add is_homepage_pinned to user table", v1_26.AddIsHomepagePinnedToUser),
}
return preparedMigrations
}

View File

@@ -0,0 +1,16 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
// AddIsHomepagePinnedToUser adds is_homepage_pinned column to user table for organizations
func AddIsHomepagePinnedToUser(x *xorm.Engine) error {
type User struct {
IsHomepagePinned bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(new(User))
}

View File

@@ -596,3 +596,21 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
"team_user.uid": userID,
})
}
// GetHomepagePinnedOrganizations returns all organizations that are pinned to the homepage
func GetHomepagePinnedOrganizations(ctx context.Context) ([]*Organization, error) {
orgs := make([]*Organization, 0, 10)
return orgs, db.GetEngine(ctx).
Where("type = ?", user_model.UserTypeOrganization).
And("is_homepage_pinned = ?", true).
And("visibility = ?", structs.VisibleTypePublic).
OrderBy("name ASC").
Find(&orgs)
}
// SetHomepagePinned updates the homepage pinned status for an organization
func (org *Organization) SetHomepagePinned(ctx context.Context, pinned bool) error {
org.IsHomepagePinned = pinned
_, err := db.GetEngine(ctx).ID(org.ID).Cols("is_homepage_pinned").Update(org)
return err
}

View File

@@ -121,6 +121,8 @@ type User struct {
// true: the user is only allowed to see organizations/repositories that they has explicit rights to.
// (ex: in private Gitea instances user won't be allowed to see even organizations/repositories that are set as public)
IsRestricted bool `xorm:"NOT NULL DEFAULT false"`
// IsHomepagePinned indicates if this organization should appear on the homepage
IsHomepagePinned bool `xorm:"NOT NULL DEFAULT false"`
AllowGitHook bool
AllowImportLocal bool // Allow migrate repository by local path

View File

@@ -53,9 +53,20 @@ type RepositoryStruct struct {
GitGuideRemoteName *config.Value[string]
}
type ThemeStruct struct {
DisableRegistration *config.Value[bool]
CustomSiteIconURL *config.Value[string]
CustomHomeLogoURL *config.Value[string]
CustomHomeHTML *config.Value[string]
CustomHomeTitle *config.Value[string]
CustomHomeTagline *config.Value[string]
PinnedOrgDisplayFormat *config.Value[string]
}
type ConfigStruct struct {
Picture *PictureStruct
Repository *RepositoryStruct
Theme *ThemeStruct
}
var (
@@ -74,6 +85,15 @@ func initDefaultConfig() {
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"),
},
Theme: &ThemeStruct{
DisableRegistration: config.ValueJSON[bool]("theme.disable_registration").WithFileConfig(config.CfgSecKey{Sec: "service", Key: "DISABLE_REGISTRATION"}),
CustomSiteIconURL: config.ValueJSON[string]("theme.custom_site_icon_url").WithDefault(""),
CustomHomeLogoURL: config.ValueJSON[string]("theme.custom_home_logo_url").WithDefault(""),
CustomHomeHTML: config.ValueJSON[string]("theme.custom_home_html").WithDefault(""),
CustomHomeTitle: config.ValueJSON[string]("theme.custom_home_title").WithDefault(""),
CustomHomeTagline: config.ValueJSON[string]("theme.custom_home_tagline").WithDefault(""),
PinnedOrgDisplayFormat: config.ValueJSON[string]("theme.pinned_org_display_format").WithDefault("condensed"),
},
}
}

View File

@@ -39,6 +39,7 @@ func NewFuncMap() template.FuncMap {
"QueryEscape": queryEscape,
"QueryBuild": QueryBuild,
"SanitizeHTML": SanitizeHTML,
"SafeHTML": SafeHTML,
"URLJoin": util.URLJoin,
"DotEscape": dotEscape,
@@ -172,6 +173,11 @@ func SanitizeHTML(s string) template.HTML {
return markup.Sanitize(s)
}
// SafeHTML marks a string as safe HTML (no sanitization). Use with caution - only for trusted admin content.
func SafeHTML(s string) template.HTML {
return template.HTML(s)
}
func htmlFormat(s any, args ...any) template.HTML {
if len(args) == 0 {
// to prevent developers from calling "HTMLFormat $userInput" by mistake which will lead to XSS

View File

@@ -338,6 +338,7 @@
"home.collaborative_repos": "Collaborative Repositories",
"home.my_orgs": "My Organizations",
"home.my_mirrors": "My Mirrors",
"home.featured_organizations": "Featured Organizations",
"home.view_home": "View %s",
"home.filter": "Other Filters",
"home.filter_by_team_repositories": "Filter by team repositories",
@@ -2798,6 +2799,9 @@
"org.settings.confirm_delete_account": "Confirm Deletion",
"org.settings.delete_failed": "Deleting organization failed due to an internal error",
"org.settings.delete_successful": "Organization <b>%s</b> has been deleted successfully.",
"org.settings.homepage_pinning": "Homepage Visibility",
"org.settings.pin_to_homepage": "Pin this organization to the homepage",
"org.settings.pin_to_homepage_help": "When enabled, this organization will be featured on the public homepage. Only administrators can change this setting.",
"org.settings.hooks_desc": "Add webhooks which will be triggered for <strong>all repositories</strong> under this organization.",
"org.settings.labels_desc": "Add labels which can be used on issues for <strong>all repositories</strong> under this organization.",
"org.members.membership_visibility": "Membership Visibility:",
@@ -3828,5 +3832,43 @@
"org.stats": "Stats",
"org.recent_activity": "Recent Activity",
"org.profile_repo_no_permission": "You do not have permission to create repositories in this organization.",
"org.profile_repo_create_failed": "Failed to create the profile repository."
"org.profile_repo_create_failed": "Failed to create the profile repository.",
"admin.config.theme_config": "Theme Configuration",
"admin.config.disable_registration": "Disable Registration",
"admin.config.disable_registration_desc": "When enabled, new users cannot sign up. Only administrators can create new accounts.",
"admin.config.custom_home_logo": "Homepage Logo",
"admin.config.custom_logo_url_placeholder": "Enter URL or upload a file below",
"admin.config.upload_logo": "Upload Logo File",
"admin.config.reset_logo": "Reset to Default",
"admin.config.custom_home_html": "Custom Homepage Content",
"admin.config.custom_home_html_placeholder": "Enter custom HTML for the homepage (shown to non-logged-in users)",
"admin.config.custom_home_html_help": "This HTML will replace the default homepage content. Leave empty to use the default.",
"admin.config.custom_home_title": "Homepage Title",
"admin.config.custom_home_title_placeholder": "Leave empty to use app name",
"admin.config.custom_home_title_help": "Custom title displayed on the homepage. Leave empty to use the default app name.",
"admin.config.custom_home_tagline": "Homepage Tagline",
"admin.config.custom_home_tagline_placeholder": "Leave empty to use default tagline",
"admin.config.custom_home_tagline_help": "Custom tagline displayed below the title on the homepage. Leave empty to use the default.",
"admin.config.pinned_org_display_format": "Pinned Organization Display Format",
"admin.config.pinned_org_format_condensed": "Condensed (icon on left)",
"admin.config.pinned_org_format_regular": "Regular (icon above)",
"admin.config.pinned_org_display_format_help": "Choose how pinned organizations are displayed on the homepage.",
"admin.config.logo_upload_success": "Homepage logo uploaded successfully",
"admin.config.logo_url_success": "Homepage logo URL updated successfully",
"admin.config.logo_reset_success": "Homepage logo reset to default",
"admin.config.logo_invalid_type": "Invalid file type. Allowed: SVG, PNG, JPG, GIF",
"admin.config.home_logo_help": "This logo will be displayed on the homepage for non-logged-in users. Recommended: SVG, PNG, JPG or GIF.",
"admin.config.custom_site_icon": "Site Icon (Favicon & Navbar)",
"admin.config.custom_icon_url_placeholder": "Enter icon URL or upload a file below",
"admin.config.upload_icon": "Upload Icon File",
"admin.config.reset_icon": "Reset to Default",
"admin.config.current_icon": "Current Icon",
"admin.config.icon_url": "Icon URL",
"admin.config.icon_upload_success": "Site icon uploaded successfully",
"admin.config.icon_url_success": "Site icon URL updated successfully",
"admin.config.icon_reset_success": "Site icon reset to default",
"admin.config.icon_invalid_type": "Invalid file type. Allowed: SVG, PNG, ICO",
"admin.config.site_icon_help": "This icon will be used as the site favicon and in the navbar. Recommended: SVG or PNG, square dimensions.",
"admin.config.current_logo": "Current Homepage Logo",
"admin.config.logo_url": "Logo URL"
}

View File

@@ -5,8 +5,11 @@
package admin
import (
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
@@ -235,6 +238,11 @@ func ChangeConfig(ctx *context.Context) {
cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()),
cfg.Theme.DisableRegistration.DynKey(): marshalBool,
cfg.Theme.CustomHomeHTML.DynKey(): marshalString(""),
cfg.Theme.CustomHomeTitle.DynKey(): marshalString(""),
cfg.Theme.CustomHomeTagline.DynKey(): marshalString(""),
cfg.Theme.PinnedOrgDisplayFormat.DynKey(): marshalString("condensed"),
}
_ = ctx.Req.ParseForm()
@@ -272,3 +280,167 @@ loop:
config.GetDynGetter().InvalidateCache()
ctx.JSONOK()
}
// ChangeThemeLogo handles homepage logo upload and custom URL
func ChangeThemeLogo(ctx *context.Context) {
cfg := setting.Config()
action := ctx.FormString("action")
if action == "reset" {
configSettings := map[string]string{
cfg.Theme.CustomHomeLogoURL.DynKey(): "\"\"",
}
if err := system_model.SetSettings(ctx, configSettings); err != nil {
ctx.ServerError("SetSettings", err)
return
}
config.GetDynGetter().InvalidateCache()
ctx.Flash.Success(ctx.Tr("admin.config.logo_reset_success"))
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
return
}
// Check for file upload first
file, header, err := ctx.Req.FormFile("logo_file")
if err == nil && header != nil {
defer file.Close()
ext := strings.ToLower(filepath.Ext(header.Filename))
allowedExts := map[string]bool{".svg": true, ".png": true, ".jpg": true, ".jpeg": true, ".gif": true}
if !allowedExts[ext] {
ctx.Flash.Error(ctx.Tr("admin.config.logo_invalid_type"))
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
return
}
customDir := filepath.Join(setting.CustomPath, "public", "assets", "img")
if err := os.MkdirAll(customDir, 0755); err != nil {
ctx.ServerError("MkdirAll", err)
return
}
fileName := "custom-home-logo" + ext
filePath := filepath.Join(customDir, fileName)
destFile, err := os.Create(filePath)
if err != nil {
ctx.ServerError("Create", err)
return
}
defer destFile.Close()
if _, err := io.Copy(destFile, file); err != nil {
ctx.ServerError("Copy", err)
return
}
fileURL := setting.AppSubURL + "/assets/img/" + fileName
marshaledValue, _ := json.Marshal(fileURL)
configSettings := map[string]string{
cfg.Theme.CustomHomeLogoURL.DynKey(): string(marshaledValue),
}
if err := system_model.SetSettings(ctx, configSettings); err != nil {
ctx.ServerError("SetSettings", err)
return
}
config.GetDynGetter().InvalidateCache()
ctx.Flash.Success(ctx.Tr("admin.config.logo_upload_success"))
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
return
}
// Check for custom URL
customURL := ctx.FormString("custom_logo_url")
marshaledValue, _ := json.Marshal(customURL)
configSettings := map[string]string{
cfg.Theme.CustomHomeLogoURL.DynKey(): string(marshaledValue),
}
if err := system_model.SetSettings(ctx, configSettings); err != nil {
ctx.ServerError("SetSettings", err)
return
}
config.GetDynGetter().InvalidateCache()
ctx.Flash.Success(ctx.Tr("admin.config.logo_url_success"))
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
}
// ChangeThemeIcon handles site icon (favicon + navbar) upload and custom URL
func ChangeThemeIcon(ctx *context.Context) {
cfg := setting.Config()
action := ctx.FormString("action")
if action == "reset" {
configSettings := map[string]string{
cfg.Theme.CustomSiteIconURL.DynKey(): "\"\"",
}
if err := system_model.SetSettings(ctx, configSettings); err != nil {
ctx.ServerError("SetSettings", err)
return
}
config.GetDynGetter().InvalidateCache()
ctx.Flash.Success(ctx.Tr("admin.config.icon_reset_success"))
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
return
}
// Check for file upload first
file, header, err := ctx.Req.FormFile("icon_file")
if err == nil && header != nil {
defer file.Close()
ext := strings.ToLower(filepath.Ext(header.Filename))
allowedExts := map[string]bool{".svg": true, ".png": true, ".ico": true}
if !allowedExts[ext] {
ctx.Flash.Error(ctx.Tr("admin.config.icon_invalid_type"))
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
return
}
customDir := filepath.Join(setting.CustomPath, "public", "assets", "img")
if err := os.MkdirAll(customDir, 0755); err != nil {
ctx.ServerError("MkdirAll", err)
return
}
fileName := "custom-site-icon" + ext
filePath := filepath.Join(customDir, fileName)
destFile, err := os.Create(filePath)
if err != nil {
ctx.ServerError("Create", err)
return
}
defer destFile.Close()
if _, err := io.Copy(destFile, file); err != nil {
ctx.ServerError("Copy", err)
return
}
fileURL := setting.AppSubURL + "/assets/img/" + fileName
marshaledValue, _ := json.Marshal(fileURL)
configSettings := map[string]string{
cfg.Theme.CustomSiteIconURL.DynKey(): string(marshaledValue),
}
if err := system_model.SetSettings(ctx, configSettings); err != nil {
ctx.ServerError("SetSettings", err)
return
}
config.GetDynGetter().InvalidateCache()
ctx.Flash.Success(ctx.Tr("admin.config.icon_upload_success"))
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
return
}
// Check for custom URL
customURL := ctx.FormString("custom_icon_url")
marshaledValue, _ := json.Marshal(customURL)
configSettings := map[string]string{
cfg.Theme.CustomSiteIconURL.DynKey(): string(marshaledValue),
}
if err := system_model.SetSettings(ctx, configSettings); err != nil {
ctx.ServerError("SetSettings", err)
return
}
config.GetDynGetter().InvalidateCache()
ctx.Flash.Success(ctx.Tr("admin.config.icon_url_success"))
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
}

View File

@@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
organization_model "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
@@ -61,6 +62,14 @@ func Home(ctx *context.Context) {
ctx.Data["PageIsHome"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
// Load pinned organizations for homepage
pinnedOrgs, err := organization_model.GetHomepagePinnedOrganizations(ctx)
if err != nil {
log.Error("GetHomepagePinnedOrganizations: %v", err)
} else {
ctx.Data["PinnedOrganizations"] = pinnedOrgs
}
ctx.HTML(http.StatusOK, tplHome)
}

View File

@@ -45,6 +45,7 @@ func Settings(ctx *context.Context) {
ctx.Data["PageIsSettingsOptions"] = true
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
ctx.Data["IsHomepagePinned"] = ctx.Org.Organization.IsHomepagePinned
ctx.Data["ContextUser"] = ctx.ContextUser
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
@@ -89,6 +90,14 @@ func SettingsPost(ctx *context.Context) {
opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
}
// Handle homepage pinning (admin only)
if ctx.Doer.IsAdmin {
if err := org.SetHomepagePinned(ctx, form.IsHomepagePinned); err != nil {
ctx.ServerError("SetHomepagePinned", err)
return
}
}
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
ctx.ServerError("UpdateUser", err)
return

View File

@@ -778,6 +778,8 @@ func registerWebRoutes(m *web.Router) {
m.Post("/test_mail", admin.SendTestMail)
m.Post("/test_cache", admin.TestCache)
m.Get("/settings", admin.ConfigSettings)
m.Post("/theme/logo", admin.ChangeThemeLogo)
m.Post("/theme/icon", admin.ChangeThemeIcon)
})
m.Group("/monitor", func() {

View File

@@ -26,6 +26,7 @@ type CreateOrgForm struct {
OrgName string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"`
Visibility structs.VisibleType
RepoAdminChangeTeamAccess bool
IsHomepagePinned bool
}
// Validate validates the fields
@@ -43,6 +44,7 @@ type UpdateOrgSettingForm struct {
Location string `binding:"MaxSize(50)"`
MaxRepoCreation int
RepoAdminChangeTeamAccess bool
IsHomepagePinned bool
}
// Validate validates the fields

View File

@@ -1,5 +1,7 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}
{{template "admin/config_settings/theme" .}}
{{template "admin/config_settings/avatars" .}}
{{template "admin/config_settings/repository" .}}

View File

@@ -0,0 +1,120 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.config.theme_config"}}
</h4>
<div class="ui attached table segment">
<dl class="admin-dl-horizontal">
<dt>{{ctx.Locale.Tr "admin.config.disable_registration"}}</dt>
<dd>
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_registration_desc"}}">
<input type="checkbox" data-config-dyn-key="theme.disable_registration" {{if .SystemConfig.Theme.DisableRegistration.Value ctx}}checked{{end}}><label></label>
</div>
</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.config.custom_site_icon"}}</dt>
<dd>
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/config/theme/icon" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
<div class="field">
<label>{{ctx.Locale.Tr "admin.config.current_icon"}}</label>
<img src="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}" alt="Current Icon" style="max-height: 32px; max-width: 32px; background: var(--color-secondary-bg); padding: 4px; border-radius: 4px;">
</div>
{{end}}
<div class="field">
<label>{{ctx.Locale.Tr "admin.config.icon_url"}}</label>
<input type="text" name="custom_icon_url" value="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_icon_url_placeholder"}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "admin.config.upload_icon"}}</label>
<input type="file" name="icon_file" accept="image/svg+xml,image/png,image/x-icon,image/vnd.microsoft.icon">
</div>
<div class="help">{{ctx.Locale.Tr "admin.config.site_icon_help"}}</div>
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
<button class="ui red button" type="submit" name="action" value="reset">{{ctx.Locale.Tr "admin.config.reset_icon"}}</button>
{{end}}
</form>
</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.config.custom_home_logo"}}</dt>
<dd>
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/config/theme/logo" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
{{if .SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}
<div class="field">
<label>{{ctx.Locale.Tr "admin.config.current_logo"}}</label>
<img src="{{.SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}" alt="Current Logo" style="max-height: 80px; max-width: 200px; background: var(--color-secondary-bg); padding: 8px; border-radius: 4px;">
</div>
{{end}}
<div class="field">
<label>{{ctx.Locale.Tr "admin.config.logo_url"}}</label>
<input type="text" name="custom_logo_url" value="{{.SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_logo_url_placeholder"}}">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "admin.config.upload_logo"}}</label>
<input type="file" name="logo_file" accept="image/svg+xml,image/png,image/jpeg,image/gif">
</div>
<div class="help">{{ctx.Locale.Tr "admin.config.home_logo_help"}}</div>
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
{{if .SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}
<button class="ui red button" type="submit" name="action" value="reset">{{ctx.Locale.Tr "admin.config.reset_logo"}}</button>
{{end}}
</form>
</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.config.custom_home_title"}}</dt>
<dd>
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
{{.CsrfTokenHtml}}
<input type="hidden" name="key" value="theme.custom_home_title">
<div class="field">
<input type="text" name="value" value="{{.SystemConfig.Theme.CustomHomeTitle.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_home_title_placeholder"}}" maxlength="100">
</div>
<div class="help">{{ctx.Locale.Tr "admin.config.custom_home_title_help"}}</div>
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</form>
</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.config.custom_home_tagline"}}</dt>
<dd>
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
{{.CsrfTokenHtml}}
<input type="hidden" name="key" value="theme.custom_home_tagline">
<div class="field">
<input type="text" name="value" value="{{.SystemConfig.Theme.CustomHomeTagline.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_home_tagline_placeholder"}}" maxlength="255">
</div>
<div class="help">{{ctx.Locale.Tr "admin.config.custom_home_tagline_help"}}</div>
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</form>
</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.config.pinned_org_display_format"}}</dt>
<dd>
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
{{.CsrfTokenHtml}}
<input type="hidden" name="key" value="theme.pinned_org_display_format">
<div class="field">
<select class="ui dropdown" name="value">
<option value="condensed" {{if eq (.SystemConfig.Theme.PinnedOrgDisplayFormat.Value ctx) "condensed"}}selected{{end}}>{{ctx.Locale.Tr "admin.config.pinned_org_format_condensed"}}</option>
<option value="regular" {{if eq (.SystemConfig.Theme.PinnedOrgDisplayFormat.Value ctx) "regular"}}selected{{end}}>{{ctx.Locale.Tr "admin.config.pinned_org_format_regular"}}</option>
</select>
</div>
<div class="help">{{ctx.Locale.Tr "admin.config.pinned_org_display_format_help"}}</div>
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</form>
</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.config.custom_home_html"}}</dt>
<dd>
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
{{.CsrfTokenHtml}}
<input type="hidden" name="key" value="theme.custom_home_html">
<div class="field">
<textarea id="custom-home-html-editor" name="value" rows="15" placeholder="{{ctx.Locale.Tr "admin.config.custom_home_html_placeholder"}}" style="font-family: var(--fonts-monospace); font-size: 13px;">{{.SystemConfig.Theme.CustomHomeHTML.Value ctx}}</textarea>
</div>
<div class="help">{{ctx.Locale.Tr "admin.config.custom_home_html_help"}}</div>
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</form>
</dd>
</dl>
</div>

View File

@@ -16,8 +16,12 @@
<link rel="alternate" type="application/atom+xml" title="" href="{{.FeedURL}}.atom">
<link rel="alternate" type="application/rss+xml" title="" href="{{.FeedURL}}.rss">
{{end}}
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
<link rel="icon" href="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}">
{{else}}
<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
<link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png" type="image/png">
{{end}}
{{template "base/head_opengraph" .}}
{{template "base/head_style" .}}
{{template "base/head_script" .}}

View File

@@ -2,7 +2,11 @@
<div class="navbar-left">
<!-- the logo -->
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}">
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
<img width="30" height="30" src="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
{{else}}
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
{{end}}
</a>
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->

View File

@@ -1,16 +1,77 @@
{{template "base/head" .}}
<div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}" class="page-content home">
{{if .SystemConfig.Theme.CustomHomeHTML.Value ctx}}
{{/* Custom homepage content */}}
{{.SystemConfig.Theme.CustomHomeHTML.Value ctx | SafeHTML}}
{{else}}
{{/* Default homepage content */}}
<div class="tw-mb-8 tw-px-8">
<div class="center">
{{if .SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}
<img class="logo" width="220" height="220" src="{{.SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}" alt="{{ctx.Locale.Tr "logo"}}">
{{else}}
<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}">
{{end}}
<div class="hero">
<h1 class="ui icon header title tw-text-balance">
{{AppName}}
{{if .SystemConfig.Theme.CustomHomeTitle.Value ctx}}
{{.SystemConfig.Theme.CustomHomeTitle.Value ctx}}
{{else}}
{{AppName}}
{{end}}
</h1>
<h2 class="tw-text-balance">{{ctx.Locale.Tr "startpage.app_desc"}}</h2>
<h2 class="tw-text-balance">
{{if .SystemConfig.Theme.CustomHomeTagline.Value ctx}}
{{.SystemConfig.Theme.CustomHomeTagline.Value ctx}}
{{else}}
{{ctx.Locale.Tr "startpage.app_desc"}}
{{end}}
</h2>
</div>
</div>
</div>
{{if .PinnedOrganizations}}
<div class="ui container tw-my-8">
{{if eq (.SystemConfig.Theme.PinnedOrgDisplayFormat.Value ctx) "regular"}}
{{/* Regular format: icon above, title below, description below that */}}
<div class="ui four doubling stackable cards">
{{range .PinnedOrganizations}}
<a class="ui card" href="{{.HomeLink}}">
<div class="content tw-text-center">
<div class="tw-mb-3">
{{ctx.AvatarUtils.Avatar . 64 "tw-rounded"}}
</div>
<div class="header">{{.DisplayName}}</div>
{{if .Description}}
<div class="meta tw-mt-2">{{.Description}}</div>
{{end}}
</div>
</a>
{{end}}
</div>
{{else}}
{{/* Condensed format (default): icon on left, title/description on right */}}
<div class="ui four doubling stackable cards">
{{range .PinnedOrganizations}}
<a class="ui card" href="{{.HomeLink}}">
<div class="content">
<div class="tw-flex tw-items-center tw-gap-3">
{{ctx.AvatarUtils.Avatar . 48 "tw-rounded"}}
<div class="tw-flex-1 tw-overflow-hidden">
<div class="header tw-truncate">{{.DisplayName}}</div>
{{if .Description}}
<div class="meta tw-truncate">{{.Description}}</div>
{{end}}
</div>
</div>
</div>
</a>
{{end}}
</div>
{{end}}
</div>
{{else}}
{{/* Only show promotional text when there are no pinned organizations */}}
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
@@ -47,5 +108,7 @@
</p>
</div>
</div>
{{end}}
{{end}}
</div>
{{template "base/footer" .}}

View File

@@ -46,6 +46,17 @@
<input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}">
<p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.homepage_pinning"}}</label>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="is_homepage_pinned" {{if .IsHomepagePinned}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.settings.pin_to_homepage"}}</label>
</div>
</div>
<p class="help">{{ctx.Locale.Tr "org.settings.pin_to_homepage_help"}}</p>
</div>
{{end}}
<div class="field">