Commit e5973a00dd

e5973a00ddcc4b23056bd048417420301a7e6f81

parent: aa16b9b9dd

Verified · cmc ci/build: success ci/test: success ci/vuln: success

cmc <hello@cleberg.net> · 2026-09-03 05:51 UTC

web: serve the fonts the stylesheet asks for

The route table named plex-*.woff2 while the embedded files and the
@font-face rules are atkinson-*.woff2, so every font request on
gitbay.org was a 404 and pages rendered in the fallback stack.

Font routes are now one literal per file in the embedded directory, and
a test asserts every url() in the stylesheet has a route that serves it.

Closes #102

Layout: unified · split

internal/httpd/fonts_test.go added +45
@@ -0,0 +1,45 @@
1package httpd
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "regexp"
7 "testing"
8
9 "gitbay.org/gitbay/internal/config"
10 "gitbay.org/gitbay/internal/web"
11)
12
13// TestStylesheetFontsAreServed: every font URL the embedded stylesheet asks
14// for has a route, and that route answers with the file. The route names
15// and the @font-face URLs were once maintained by hand and drifted, so
16// gitbay.org served no web font at all (#102).
17func TestStylesheetFontsAreServed(t *testing.T) {
18 s := New(config.Default(), nil)
19 byPattern := map[string]http.HandlerFunc{}
20 for _, r := range s.Routes() {
21 if r.Method == "GET" {
22 byPattern[r.Pattern] = r.Handler
23 }
24 }
25 urls := regexp.MustCompile(`url\((/static/fonts/[^)]+)\)`).FindAllSubmatch(web.StyleCSS, -1)
26 if len(urls) == 0 {
27 t.Fatal("stylesheet declares no font URLs")
28 }
29 for _, m := range urls {
30 u := string(m[1])
31 h, ok := byPattern[u]
32 if !ok {
33 t.Errorf("%s: stylesheet asks for it, route table has no route", u)
34 continue
35 }
36 rec := httptest.NewRecorder()
37 h(rec, httptest.NewRequest("GET", u, nil))
38 if rec.Code != http.StatusOK || rec.Body.Len() == 0 {
39 t.Errorf("%s: status %d, %d bytes", u, rec.Code, rec.Body.Len())
40 }
41 if ct := rec.Header().Get("Content-Type"); ct != "font/woff2" {
42 t.Errorf("%s: content-type %q", u, ct)
43 }
44 }
45}
internal/httpd/routes.go +12 −5
@@ -2,9 +2,12 @@ package httpd
2 2
3import ( 3import (
4 "fmt" 4 "fmt"
5 "io/fs"
5 "net" 6 "net"
6 "net/http" 7 "net/http"
7 "strings" 8 "strings"
9
10 "gitbay.org/gitbay/internal/web"
8) 11)
9 12
10// Route is one entry in the explicit route table. The view-only guarantee is 13// Route is one entry in the explicit route table. The view-only guarantee is
@@ -41,11 +44,15 @@ func (s *Server) Routes() []Route {
41 Route{Method: "GET", Pattern: "/healthz", Handler: s.healthz}, 44 Route{Method: "GET", Pattern: "/healthz", Handler: s.healthz},
42 Route{Method: "GET", Pattern: "/privacy", Handler: s.privacy}, 45 Route{Method: "GET", Pattern: "/privacy", Handler: s.privacy},
43 Route{Method: "GET", Pattern: "/static/style.css", Handler: s.stylesheet}, 46 Route{Method: "GET", Pattern: "/static/style.css", Handler: s.stylesheet},
44 // Literal per-file routes: a {name} wildcard is ambiguous against 47 )
45 // /{owner}/{repo}/... patterns in ServeMux precedence. 48 // One literal route per embedded font file: a {name} wildcard is
46 Route{Method: "GET", Pattern: "/static/fonts/plex-sans.woff2", Handler: s.font}, 49 // ambiguous against /{owner}/{repo}/... patterns in ServeMux precedence,
47 Route{Method: "GET", Pattern: "/static/fonts/plex-mono-400.woff2", Handler: s.font}, 50 // and hand-typed names drifted from the files once (#102).
48 Route{Method: "GET", Pattern: "/static/fonts/plex-mono-500.woff2", Handler: s.font}, 51 fonts, _ := fs.ReadDir(web.FontFS, "static/fonts")
52 for _, f := range fonts {
53 routes = append(routes, Route{Method: "GET", Pattern: "/static/fonts/" + f.Name(), Handler: s.font})
54 }
55 routes = append(routes,
49 Route{Method: "GET", Pattern: "/favicon.svg", Handler: s.favicon}, 56 Route{Method: "GET", Pattern: "/favicon.svg", Handler: s.favicon},
50 Route{Method: "GET", Pattern: "/{owner}", Handler: s.ownerPage}, 57 Route{Method: "GET", Pattern: "/{owner}", Handler: s.ownerPage},
51 Route{Method: "GET", Pattern: "/{owner}/{repo}", Handler: s.repoHome}, 58 Route{Method: "GET", Pattern: "/{owner}/{repo}", Handler: s.repoHome},