Sitelet https://github.com/gogs/gogs/issues/8369
Skip to content

Updated Customization #8369

Description

@bvwj

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:

  1. Use our logo in the proper aspect ratio
  2. Adjust highlight colors to match our branding
  3. 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:

  1. Display a company logo in the navbar on every page
  2. Adjust accent/highlight colors to match company branding
  3. 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:

{{.InjectHead}}

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

  • custom/templates/inject/head.tmpl is included in <head> on React pages (landing, sign-in, commit view, 404)
  • custom/public/css/custom.css linked from inject overrides --color-primary on React pages
  • Navbar renders custom/public/img/logo.svg at correct aspect ratio
  • custom/public/img/favicon.png is used only for the browser tab icon, not the navbar
  • Subpath deployments work ({{AppSubURL}} in inject templates)
  • Missing custom files are no-ops; stock Gogs appearance is unchanged
  • (Optional) custom/public/landing.html replaces the default landing body

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

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    🎯 featureCategorizes as related to a new feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions