Describe the feature
I spent a day working on customization according to the docs, only to discover customization is broken on the newest Gogs versions.
I believe customization is very important for a system like Gogs. It is a fundamental requirement for our use.
The main requirements are:
- Use our logo in the proper aspect ratio
- Adjust highlight colors to match our branding
- Replace the landing page with custom content
I spent some time with Grok to understand the difficulty of these changes. The solution description below is 100% generated by Grok. I apologize if this offends your sensibilities.
Describe the solution you'd like
Proposal: React UI customization via custom/ directory
Summary
Gogs is migrating toward a React frontend (web/), but the documented customization path (custom/templates/inject/head.tmpl + custom/public/) only applies to legacy Go-template pages. React pages are served through a separate HTML shell (dist/index.html) that is rewritten per request by renderIndex() and does not include any injection hook.
This proposal recommends a small set of changes so administrators can brand the React UI using the same custom/ overlay model already documented for legacy pages—without maintaining a fork or reverse-proxy HTML injection.
Problem
Administrators commonly need to:
- Display a company logo in the navbar on every page
- Adjust accent/highlight colors to match company branding
- Replace the landing page body with organization-specific content
Today, these goals are difficult or impossible on React pages:
| Goal |
Current state on React pages |
| Company logo |
Navbar hard-codes /img/favicon.png at 28×28 px, conflating favicon and brand mark |
| Brand colors |
Design tokens ship in the bundled index.css; no supported override path |
| Custom landing |
Landing.tsx is a hardcoded component; custom/public/ cannot replace it |
The custom templates documentation describes inject/head.tmpl and custom/public/css/custom.css, but that injection point is wired only into templates/base/head.tmpl (legacy UI). React routes (/, /user/sign-in, /:owner/:repo/commit/:sha, etc.) are served via ServeWeb() and never load inject/head.
Additionally, using favicon.png for navbar branding is a poor fit: favicons are typically square bitmaps (e.g. 256×256) meant for browser tabs and bookmarks, while navbar logos are often wide SVG wordmarks that need to preserve aspect ratio at a fixed height.
Proposed solution
Extend the existing custom/ overlay model to the React HTML shell and introduce a dedicated brand logo path separate from the favicon.
1. Inject head.tmpl / footer.tmpl into the React shell
In renderIndex() (cmd/gogs/internal/web/web.go), after applying the existing {{.WebContext}} substitution, read and splice:
custom/templates/inject/head.tmpl before </head>
custom/templates/inject/footer.tmpl before </body>
Execute inject templates with the same template functions available to legacy pages (at minimum AppSubURL), so subpath deployments work correctly.
Optionally add a placeholder to web/index.html for clarity:
Admin usage (unchanged from docs, but now effective on React pages):
custom/templates/inject/head.tmpl
custom/public/css/custom.css
Example head.tmpl:
<link rel="stylesheet" href="{{AppSubURL}}/css/custom.css">
This mirrors the legacy injection model documented in custom-templates.mdx and reuses infrastructure admins already know.
2. Separate brand logo from favicon
Introduce a dedicated brand mark path for navbar (and optional hero) rendering:
| Asset |
Path |
Purpose |
Format |
| Brand mark |
/img/logo.svg |
Navbar, optional landing hero |
SVG, any aspect ratio |
| Favicon |
/img/favicon.png |
Browser tab / bookmarks only |
Square bitmap (e.g. 256×256) |
Code change: Replace the hard-coded favicon <img> in web/src/components/Navbar.tsx with a BrandLogo component that:
- Loads
suburl("/sitelet?url=https%3A%2F%2Fgithub.com%2Fimg%2Flogo.svg")
- Renders at fixed height (
h-7 / 28px) with width: auto and a reasonable max-width so aspect ratio is preserved
- Falls back to
favicon.png via onError when no custom logo.svg is provided (stock installs)
Admin usage:
custom/public/img/logo.svg # company wordmark or mark
custom/public/img/favicon.png # square tab icon (unchanged purpose)
No frontend rebuild required for logo swaps after the code change ships; custom/public/ already takes priority over embedded static files.
3. Brand colors via CSS custom properties
Once inject works, administrators can override React design tokens in custom/public/css/custom.css without touching bundled assets:
:root {
--color-primary: #0066cc;
--color-ring: #0066cc;
--color-diff-added: #1a7f37;
--color-diff-removed: #cf222e;
}
:root.dark {
--color-primary: #4493f8;
--color-ring: #4493f8;
}
React components already consume these tokens from web/src/index.css. This approach avoids overriding hashed /assets/*.css bundles, which change every build.
4. Optional custom landing page body
For organizations that need more than a hero image swap, allow an optional custom landing body:
custom/public/landing.html
Code change: In web/src/pages/Landing.tsx, fetch /landing.html (via subUrl). If present, render its contents inside <main> (e.g. dangerouslySetInnerHTML with appropriate sanitization policy, or serve as trusted admin-controlled content). If absent, render the default terminal-style landing.
Navbar and footer remain from RootLayout in router.tsx; only the main content area is replaced.
Why this approach
- Consistent with existing docs. Reuses
custom/templates/inject/ and custom/public/ rather than introducing a parallel configuration system.
- Low maintenance for admins. File drops survive upgrades; no proxy rules or binary rebuilds for CSS/logo changes.
- Small, localized code changes. The React shell already passes through
renderIndex() per request; the comment in webapp_prod.go notes this is the intended place for future runtime injection.
- Separates favicon from branding. SVG logos scale cleanly; favicons remain square bitmaps for browser chrome.
- Forward-compatible. As more pages migrate from legacy templates to React, the same customization path applies.
Scope
In scope:
- React pages served via
ServeWeb() / renderIndex()
inject/head.tmpl, inject/footer.tmpl, custom/public/css/, custom/public/img/logo.svg
- Optional
custom/public/landing.html
Out of scope (for this proposal):
- Legacy Semantic UI page overrides (expected to be retired as pages migrate)
- Replacing or overriding hashed Vite bundle files under
/assets/
- Per-user or per-organization theming (instance-wide admin customization only)
- Runtime theme configuration via
app.ini (could be a follow-up)
Implementation sketch
renderIndex() (Go)
// After existing {{.WebContext}} and subpath rewrites:
html := strings.NewReplacer(pairs...).Replace(string(index))
html = injectCustom(html, "head", "</head>")
html = injectCustom(html, "footer", "</body>")
return []byte(html), nil
injectCustom reads custom/templates/inject/{name}.tmpl, executes as a Go template with AppSubURL, and splices before the closing tag. Missing files are a no-op.
BrandLogo (React)
// Fixed height, auto width, fallback on 404
<img
src={subUrl("/img/logo.svg")}
className="h-7 w-auto max-w-36 object-contain object-left"
onError={/* fall back to /img/favicon.png */}
/>
Landing.tsx (React, optional)
const custom = await fetch(subUrl("/landing.html"));
if (custom.ok) return <main dangerouslySetInnerHTML={{ __html: await custom.text() }} />;
return <DefaultLanding />;
Acceptance criteria
Alternatives considered
| Alternative |
Drawback |
| Reverse-proxy HTML injection |
Operational complexity; fragile across upgrades |
Override /assets/*.css bundles |
Hashed filenames change every build |
Fork and patch web/src/index.css |
Requires maintaining a frontend build pipeline |
Continue using favicon.png for navbar |
Poor fit for non-square logos; conflates two purposes |
Related context
- React shell rendering:
cmd/gogs/internal/web/web.go (renderIndex), webapp_prod.go, webapp_dev.go
- Legacy inject point:
templates/base/head.tmpl line 79
- Navbar logo today:
web/src/components/Navbar.tsx
- Design tokens:
web/src/index.css
CHANGELOG.md (0.15.0+dev) notes legacy template customization is being removed; this proposal provides a React-era replacement
Submitted as a community recommendation. Happy to help refine scope or contribute a PR if maintainers are interested.
Describe alternatives you've considered
I tried the legacy customization recommendations. They are broken.
I will need to look for another git sever system if we can't implement this level of customization.
Additional context
I tried forgejo, its customization is also broken right now.
Code of Conduct
Describe the feature
I spent a day working on customization according to the docs, only to discover customization is broken on the newest Gogs versions.
I believe customization is very important for a system like Gogs. It is a fundamental requirement for our use.
The main requirements are:
I spent some time with Grok to understand the difficulty of these changes. The solution description below is 100% generated by Grok. I apologize if this offends your sensibilities.
Describe the solution you'd like
Proposal: React UI customization via
custom/directorySummary
Gogs is migrating toward a React frontend (
web/), but the documented customization path (custom/templates/inject/head.tmpl+custom/public/) only applies to legacy Go-template pages. React pages are served through a separate HTML shell (dist/index.html) that is rewritten per request byrenderIndex()and does not include any injection hook.This proposal recommends a small set of changes so administrators can brand the React UI using the same
custom/overlay model already documented for legacy pages—without maintaining a fork or reverse-proxy HTML injection.Problem
Administrators commonly need to:
Today, these goals are difficult or impossible on React pages:
/img/favicon.pngat 28×28 px, conflating favicon and brand markindex.css; no supported override pathLanding.tsxis a hardcoded component;custom/public/cannot replace itThe custom templates documentation describes
inject/head.tmplandcustom/public/css/custom.css, but that injection point is wired only intotemplates/base/head.tmpl(legacy UI). React routes (/,/user/sign-in,/:owner/:repo/commit/:sha, etc.) are served viaServeWeb()and never loadinject/head.Additionally, using
favicon.pngfor navbar branding is a poor fit: favicons are typically square bitmaps (e.g. 256×256) meant for browser tabs and bookmarks, while navbar logos are often wide SVG wordmarks that need to preserve aspect ratio at a fixed height.Proposed solution
Extend the existing
custom/overlay model to the React HTML shell and introduce a dedicated brand logo path separate from the favicon.1. Inject
head.tmpl/footer.tmplinto the React shellIn
renderIndex()(cmd/gogs/internal/web/web.go), after applying the existing{{.WebContext}}substitution, read and splice:custom/templates/inject/head.tmplbefore</head>custom/templates/inject/footer.tmplbefore</body>Execute inject templates with the same template functions available to legacy pages (at minimum
AppSubURL), so subpath deployments work correctly.Optionally add a placeholder to
web/index.htmlfor clarity:{{.InjectHead}}Admin usage (unchanged from docs, but now effective on React pages):
Example
head.tmpl:This mirrors the legacy injection model documented in custom-templates.mdx and reuses infrastructure admins already know.
2. Separate brand logo from favicon
Introduce a dedicated brand mark path for navbar (and optional hero) rendering:
/img/logo.svg/img/favicon.pngCode change: Replace the hard-coded favicon
<img>inweb/src/components/Navbar.tsxwith aBrandLogocomponent that:suburl("/sitelet?url=https%3A%2F%2Fgithub.com%2Fimg%2Flogo.svg")h-7/ 28px) withwidth: autoand a reasonablemax-widthso aspect ratio is preservedfavicon.pngviaonErrorwhen no customlogo.svgis provided (stock installs)Admin usage:
No frontend rebuild required for logo swaps after the code change ships;
custom/public/already takes priority over embedded static files.3. Brand colors via CSS custom properties
Once inject works, administrators can override React design tokens in
custom/public/css/custom.csswithout touching bundled assets:React components already consume these tokens from
web/src/index.css. This approach avoids overriding hashed/assets/*.cssbundles, which change every build.4. Optional custom landing page body
For organizations that need more than a hero image swap, allow an optional custom landing body:
Code change: In
web/src/pages/Landing.tsx, fetch/landing.html(viasubUrl). If present, render its contents inside<main>(e.g.dangerouslySetInnerHTMLwith appropriate sanitization policy, or serve as trusted admin-controlled content). If absent, render the default terminal-style landing.Navbar and footer remain from
RootLayoutinrouter.tsx; only the main content area is replaced.Why this approach
custom/templates/inject/andcustom/public/rather than introducing a parallel configuration system.renderIndex()per request; the comment inwebapp_prod.gonotes this is the intended place for future runtime injection.Scope
In scope:
ServeWeb()/renderIndex()inject/head.tmpl,inject/footer.tmpl,custom/public/css/,custom/public/img/logo.svgcustom/public/landing.htmlOut of scope (for this proposal):
/assets/app.ini(could be a follow-up)Implementation sketch
renderIndex()(Go)injectCustomreadscustom/templates/inject/{name}.tmpl, executes as a Go template withAppSubURL, and splices before the closing tag. Missing files are a no-op.BrandLogo(React)Landing.tsx(React, optional)Acceptance criteria
custom/templates/inject/head.tmplis included in<head>on React pages (landing, sign-in, commit view, 404)custom/public/css/custom.csslinked from inject overrides--color-primaryon React pagescustom/public/img/logo.svgat correct aspect ratiocustom/public/img/favicon.pngis used only for the browser tab icon, not the navbar{{AppSubURL}}in inject templates)custom/public/landing.htmlreplaces the default landing bodyAlternatives considered
/assets/*.cssbundlesweb/src/index.cssfavicon.pngfor navbarRelated context
cmd/gogs/internal/web/web.go(renderIndex),webapp_prod.go,webapp_dev.gotemplates/base/head.tmplline 79web/src/components/Navbar.tsxweb/src/index.cssCHANGELOG.md(0.15.0+dev) notes legacy template customization is being removed; this proposal provides a React-era replacementSubmitted as a community recommendation. Happy to help refine scope or contribute a PR if maintainers are interested.
Describe alternatives you've considered
I tried the legacy customization recommendations. They are broken.
I will need to look for another git sever system if we can't implement this level of customization.
Additional context
I tried forgejo, its customization is also broken right now.
Code of Conduct