Commit a5b8bd2151
a5b8bd215107b78649c253f3b52a1e6422157194
parent: 2baa586b20
Verified · cmc
cmc <hello@cleberg.net> · 2026-08-24T16:38:06Z
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.
internal/httpd/routes.go
+1
| @@ -28,6 +28,7 @@ func (s *Server) Routes() []Route { |
| 28 | 28 | routes = append(routes, |
| 29 | 29 | Route{Method: "GET", Pattern: "/{$}", Handler: s.index}, |
| 30 | 30 | Route{Method: "GET", Pattern: "/static/style.css", Handler: s.stylesheet}, |
| 31 | Route{Method: "GET", Pattern: "/favicon.svg", Handler: s.favicon}, |
| 31 | 32 | Route{Method: "GET", Pattern: "/{owner}", Handler: s.ownerPage}, |
| 32 | 33 | Route{Method: "GET", Pattern: "/{owner}/{repo}", Handler: s.repoHome}, |
| 33 | 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 | 49 | w.Write(web.StyleCSS) |
| 50 | 50 | } |
| 51 | 51 | |
| 52 | func (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 | 57 | // describedRepo pairs a repo with its description for listings. |
| 53 | 58 | type describedRepo struct { |
| 54 | 59 | store.Repo |
internal/policy/names.go
+12 −11
| @@ -11,17 +11,18 @@ import ( |
| 11 | 11 | // the httpd mux's top-level routes must be reflected here; the httpd package |
| 12 | 12 | // asserts this in its tests. |
| 13 | 13 | var reservedNames = map[string]bool{ |
| 14 | | "admin": true, |
| 15 | | "api": true, |
| 16 | | "archive": true, |
| 17 | | "explore": true, |
| 18 | | "login": true, |
| 19 | | "logout": true, |
| 20 | | "new": true, |
| 21 | | "raw": true, |
| 22 | | "register": true, |
| 23 | | "settings": true, |
| 24 | | "static": true, |
| 14 | "admin": true, |
| 15 | "api": true, |
| 16 | "archive": true, |
| 17 | "explore": true, |
| 18 | "favicon.svg": true, |
| 19 | "login": true, |
| 20 | "logout": true, |
| 21 | "new": true, |
| 22 | "raw": true, |
| 23 | "register": true, |
| 24 | "settings": true, |
| 25 | "static": true, |
| 25 | 26 | } |
| 26 | 27 | |
| 27 | 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 | 70 | background: var(--bg); |
| 71 | 71 | color: var(--fg); |
| 72 | 72 | font: var(--fs-3)/1.6 var(--sans); |
| 73 | min-height: 100vh; |
| 74 | display: flex; |
| 75 | flex-direction: column; |
| 73 | 76 | } |
| 77 | main { flex: 1; } |
| 74 | 78 | |
| 75 | 79 | a { color: var(--accent); text-decoration: none; } |
| 76 | 80 | a:hover { text-decoration: underline; } |
| @@ -91,8 +95,16 @@ header { |
| 91 | 95 | border-bottom: 1px solid var(--faint); |
| 92 | 96 | padding: var(--sp-3) var(--sp-5); |
| 93 | 97 | } |
| 94 | | a.site { font-weight: 650; color: var(--fg); letter-spacing: -0.01em; } |
| 98 | a.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 | } |
| 95 | 106 | a.site:hover { text-decoration: none; color: var(--accent); } |
| 107 | a.site svg.mark { display: block; } |
| 96 | 108 | main { |
| 97 | 109 | max-width: 64rem; |
| 98 | 110 | margin: 0 auto; |
| @@ -250,6 +262,19 @@ button { |
| 250 | 262 | } |
| 251 | 263 | button:hover { filter: brightness(1.08); } |
| 252 | 264 | |
| 265 | /* footer */ |
| 266 | footer { |
| 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 | } |
| 274 | footer p { margin: 0; } |
| 275 | footer a { color: var(--muted); text-decoration: underline; } |
| 276 | footer a:hover { color: var(--accent); } |
| 277 | |
| 253 | 278 | /* mobile */ |
| 254 | 279 | @media (max-width: 40rem) { |
| 255 | 280 | header { padding: var(--sp-3) var(--sp-4); } |
internal/web/templates/layout.html
+5 −1
| @@ -5,14 +5,18 @@ |
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | 6 | <title>{{template "title" .}}</title> |
| 7 | 7 | <link rel="stylesheet" href="/static/style.css"> |
| 8 | <link rel="icon" href="/favicon.svg" type="image/svg+xml"> |
| 8 | 9 | </head> |
| 9 | 10 | <body> |
| 10 | 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 | 13 | </header> |
| 13 | 14 | <main> |
| 14 | 15 | {{template "content" .}} |
| 15 | 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 | 20 | </body> |
| 17 | 21 | </html>{{end}} |
| 18 | 22 | |
internal/web/web.go
+23 −1
| @@ -6,6 +6,8 @@ import ( |
| 6 | 6 | "embed" |
| 7 | 7 | "html/template" |
| 8 | 8 | "io" |
| 9 | "runtime/debug" |
| 10 | "sync" |
| 9 | 11 | ) |
| 10 | 12 | |
| 11 | 13 | //go:embed templates/*.html |
| @@ -14,10 +16,30 @@ var templateFS embed.FS |
| 14 | 16 | //go:embed static/style.css |
| 15 | 17 | var StyleCSS []byte |
| 16 | 18 | |
| 19 | //go:embed static/favicon.svg |
| 20 | var 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. |
| 24 | var 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 | |
| 37 | var funcs = template.FuncMap{"gitbayVersion": func() string { return version() }} |
| 38 | |
| 17 | 39 | // Render executes the named page template with the shared layout. |
| 18 | 40 | func Render(w io.Writer, page string, data any) error { |
| 19 | 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 | 43 | ).ParseFS(templateFS, "templates/"+page) |
| 22 | 44 | if err != nil { |
| 23 | 45 | return err |