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
22
33import (
44 "fmt"
5 "io/fs"
56 "net"
67 "net/http"
78 "strings"
9
10 "gitbay.org/gitbay/internal/web"
811)
912
1013// Route is one entry in the explicit route table. The view-only guarantee is
@@ -41,11 +44,15 @@ func (s *Server) Routes() []Route {
4144 Route{Method: "GET", Pattern: "/healthz", Handler: s.healthz},
4245 Route{Method: "GET", Pattern: "/privacy", Handler: s.privacy},
4346 Route{Method: "GET", Pattern: "/static/style.css", Handler: s.stylesheet},
44 // Literal per-file routes: a {name} wildcard is ambiguous against
45 // /{owner}/{repo}/... patterns in ServeMux precedence.
46 Route{Method: "GET", Pattern: "/static/fonts/plex-sans.woff2", Handler: s.font},
47 Route{Method: "GET", Pattern: "/static/fonts/plex-mono-400.woff2", Handler: s.font},
48 Route{Method: "GET", Pattern: "/static/fonts/plex-mono-500.woff2", Handler: s.font},
47 )
48 // One literal route per embedded font file: a {name} wildcard is
49 // ambiguous against /{owner}/{repo}/... patterns in ServeMux precedence,
50 // and hand-typed names drifted from the files once (#102).
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,
4956 Route{Method: "GET", Pattern: "/favicon.svg", Handler: s.favicon},
5057 Route{Method: "GET", Pattern: "/{owner}", Handler: s.ownerPage},
5158 Route{Method: "GET", Pattern: "/{owner}/{repo}", Handler: s.repoHome},