Commit a5b8bd2151

a5b8bd215107b78649c253f3b52a1e6422157194

parent: 2baa586b20

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-24 16:38 UTC

web: identity — wordmark, favicon, footer

Header wordmark pairs an inline-SVG wave glyph with the site name.
/favicon.svg serves an embedded SVG (route word added to the reserved
username list). Footer carries a gitbay link and the binary's short
VCS revision via runtime/debug buildinfo. Footer sits at the viewport
bottom on short pages.

Layout: unified · split

internal/httpd/routes.go +1
@@ -28,6 +28,7 @@ func (s *Server) Routes() []Route {
28 routes = append(routes, 28 routes = append(routes,
29 Route{Method: "GET", Pattern: "/{$}", Handler: s.index}, 29 Route{Method: "GET", Pattern: "/{$}", Handler: s.index},
30 Route{Method: "GET", Pattern: "/static/style.css", Handler: s.stylesheet}, 30 Route{Method: "GET", Pattern: "/static/style.css", Handler: s.stylesheet},
31 Route{Method: "GET", Pattern: "/favicon.svg", Handler: s.favicon},
31 Route{Method: "GET", Pattern: "/{owner}", Handler: s.ownerPage}, 32 Route{Method: "GET", Pattern: "/{owner}", Handler: s.ownerPage},
32 Route{Method: "GET", Pattern: "/{owner}/{repo}", Handler: s.repoHome}, 33 Route{Method: "GET", Pattern: "/{owner}/{repo}", Handler: s.repoHome},
33 Route{Method: "GET", Pattern: "/{owner}/{repo}/tree/{ref}/{path...}", Handler: s.tree}, 34 Route{Method: "GET", Pattern: "/{owner}/{repo}/tree/{ref}/{path...}", Handler: s.tree},
internal/httpd/web.go +5
@@ -49,6 +49,11 @@ func (s *Server) stylesheet(w http.ResponseWriter, r *http.Request) {
49 w.Write(web.StyleCSS) 49 w.Write(web.StyleCSS)
50} 50}
51 51
52func (s *Server) favicon(w http.ResponseWriter, r *http.Request) {
53 w.Header().Set("Content-Type", "image/svg+xml")
54 w.Write(web.FaviconSVG)
55}
56
52// describedRepo pairs a repo with its description for listings. 57// describedRepo pairs a repo with its description for listings.
53type describedRepo struct { 58type describedRepo struct {
54 store.Repo 59 store.Repo
internal/policy/names.go +12 −11
@@ -11,17 +11,18 @@ import (
11// the httpd mux's top-level routes must be reflected here; the httpd package 11// the httpd mux's top-level routes must be reflected here; the httpd package
12// asserts this in its tests. 12// asserts this in its tests.
13var reservedNames = map[string]bool{ 13var reservedNames = map[string]bool{
14 "admin": true, 14 "admin": true,
15 "api": true, 15 "api": true,
16 "archive": true, 16 "archive": true,
17 "explore": true, 17 "explore": true,
18 "login": true, 18 "favicon.svg": true,
19 "logout": true, 19 "login": true,
20 "new": true, 20 "logout": true,
21 "raw": true, 21 "new": true,
22 "register": true, 22 "raw": true,
23 "settings": true, 23 "register": true,
24 "static": true, 24 "settings": true,
25 "static": true,
25} 26}
26 27
27// namePat matches valid user, org, and repo names: lowercase alphanumerics, 28// namePat matches valid user, org, and repo names: lowercase alphanumerics,
internal/web/static/favicon.svg added +1
@@ -0,0 +1 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="1" y="1" width="22" height="22" rx="6" fill="#0b6c80"/><path d="M5 9.5c2-2.5 4-2.5 6 0s4 2.5 6 0" stroke="#ffffff" stroke-width="2.2" fill="none" stroke-linecap="round"/><path d="M5 15c2-2.5 4-2.5 6 0s4 2.5 6 0" stroke="#ffffff" stroke-width="2.2" fill="none" stroke-linecap="round"/></svg>
internal/web/static/style.css +26 −1
@@ -70,7 +70,11 @@ body {
70 background: var(--bg); 70 background: var(--bg);
71 color: var(--fg); 71 color: var(--fg);
72 font: var(--fs-3)/1.6 var(--sans); 72 font: var(--fs-3)/1.6 var(--sans);
73 min-height: 100vh;
74 display: flex;
75 flex-direction: column;
73} 76}
77main { flex: 1; }
74 78
75a { color: var(--accent); text-decoration: none; } 79a { color: var(--accent); text-decoration: none; }
76a:hover { text-decoration: underline; } 80a:hover { text-decoration: underline; }
@@ -91,8 +95,16 @@ header {
91 border-bottom: 1px solid var(--faint); 95 border-bottom: 1px solid var(--faint);
92 padding: var(--sp-3) var(--sp-5); 96 padding: var(--sp-3) var(--sp-5);
93} 97}
94a.site { font-weight: 650; color: var(--fg); letter-spacing: -0.01em; } 98a.site {
99 font-weight: 650;
100 color: var(--fg);
101 letter-spacing: -0.01em;
102 display: inline-flex;
103 align-items: center;
104 gap: var(--sp-2);
105}
95a.site:hover { text-decoration: none; color: var(--accent); } 106a.site:hover { text-decoration: none; color: var(--accent); }
107a.site svg.mark { display: block; }
96main { 108main {
97 max-width: 64rem; 109 max-width: 64rem;
98 margin: 0 auto; 110 margin: 0 auto;
@@ -250,6 +262,19 @@ button {
250} 262}
251button:hover { filter: brightness(1.08); } 263button:hover { filter: brightness(1.08); }
252 264
265/* footer */
266footer {
267 max-width: 64rem;
268 margin: 0 auto;
269 padding: var(--sp-4) var(--sp-5);
270 border-top: 1px solid var(--faint);
271 color: var(--muted);
272 font-size: var(--fs-1);
273}
274footer p { margin: 0; }
275footer a { color: var(--muted); text-decoration: underline; }
276footer a:hover { color: var(--accent); }
277
253/* mobile */ 278/* mobile */
254@media (max-width: 40rem) { 279@media (max-width: 40rem) {
255 header { padding: var(--sp-3) var(--sp-4); } 280 header { padding: var(--sp-3) var(--sp-4); }
internal/web/templates/layout.html +5 −1
@@ -5,14 +5,18 @@
5<meta name="viewport" content="width=device-width, initial-scale=1"> 5<meta name="viewport" content="width=device-width, initial-scale=1">
6<title>{{template "title" .}}</title> 6<title>{{template "title" .}}</title>
7<link rel="stylesheet" href="/static/style.css"> 7<link rel="stylesheet" href="/static/style.css">
8<link rel="icon" href="/favicon.svg" type="image/svg+xml">
8</head> 9</head>
9<body> 10<body>
10<header> 11<header>
11 <nav><a class="site" href="/">{{.Site}}</a></nav> 12 <nav><a class="site" href="/"><svg class="mark" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><rect x="1" y="1" width="22" height="22" rx="6" fill="var(--accent)"/><path d="M5 9.5c2-2.5 4-2.5 6 0s4 2.5 6 0" stroke="var(--accent-fg)" stroke-width="2.2" fill="none" stroke-linecap="round"/><path d="M5 15c2-2.5 4-2.5 6 0s4 2.5 6 0" stroke="var(--accent-fg)" stroke-width="2.2" fill="none" stroke-linecap="round"/></svg>{{.Site}}</a></nav>
12</header> 13</header>
13<main> 14<main>
14{{template "content" .}} 15{{template "content" .}}
15</main> 16</main>
17<footer>
18 <p>powered by <a href="https://gitbay.org/krz/gitbay">gitbay</a>{{with gitbayVersion}} · <code>{{.}}</code>{{end}}</p>
19</footer>
16</body> 20</body>
17</html>{{end}} 21</html>{{end}}
18 22
internal/web/web.go +23 −1
@@ -6,6 +6,8 @@ import (
6 "embed" 6 "embed"
7 "html/template" 7 "html/template"
8 "io" 8 "io"
9 "runtime/debug"
10 "sync"
9) 11)
10 12
11//go:embed templates/*.html 13//go:embed templates/*.html
@@ -14,10 +16,30 @@ var templateFS embed.FS
14//go:embed static/style.css 16//go:embed static/style.css
15var StyleCSS []byte 17var StyleCSS []byte
16 18
19//go:embed static/favicon.svg
20var FaviconSVG []byte
21
22// version returns the short VCS revision baked into the binary, or "" when
23// built outside a checkout. Used by the layout footer.
24var version = sync.OnceValue(func() string {
25 info, ok := debug.ReadBuildInfo()
26 if !ok {
27 return ""
28 }
29 for _, s := range info.Settings {
30 if s.Key == "vcs.revision" && len(s.Value) >= 10 {
31 return s.Value[:10]
32 }
33 }
34 return ""
35})
36
37var funcs = template.FuncMap{"gitbayVersion": func() string { return version() }}
38
17// Render executes the named page template with the shared layout. 39// Render executes the named page template with the shared layout.
18func Render(w io.Writer, page string, data any) error { 40func Render(w io.Writer, page string, data any) error {
19 t, err := template.Must( 41 t, err := template.Must(
20 template.ParseFS(templateFS, "templates/layout.html"), 42 template.New("layout.html").Funcs(funcs).ParseFS(templateFS, "templates/layout.html"),
21 ).ParseFS(templateFS, "templates/"+page) 43 ).ParseFS(templateFS, "templates/"+page)
22 if err != nil { 44 if err != nil {
23 return err 45 return err