Commit 6fdbe81e72
6fdbe81e72d980cedcb8f426e271d9fe77c7e938
parent: 185f434bf2
Verified · cmc ci/build: success ci/test: success
cmc <hello@cleberg.net> · 2026-09-18 02:09 UTC
web: landing picture, light and dark
Closes #218
e2e/design_test.go
+2
| @@ -163,6 +163,8 @@ func TestLandingRoutes(t *testing.T) { |
| 163 | 163 | `class="button primary" href="/explore">Explore repositories</a>`, |
| 164 | 164 | `class="button btn" href="/register">Create an account</a>`, |
| 165 | 165 | "web login</code>", |
| 166 | "/static/img/mr-dark.png", |
| 167 | "<picture>", |
| 166 | 168 | } { |
| 167 | 169 | if !strings.Contains(body, want) { |
| 168 | 170 | t.Errorf("landing lacks %q", want) |
internal/httpd/fonts_test.go
+34
| @@ -1,9 +1,11 @@ |
| 1 | 1 | package httpd |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | "io/fs" |
| 4 | 5 | "net/http" |
| 5 | 6 | "net/http/httptest" |
| 6 | 7 | "regexp" |
| 8 | "strings" |
| 7 | 9 | "testing" |
| 8 | 10 | |
| 9 | 11 | "gitbay.org/gitbay/internal/config" |
| @@ -43,3 +45,35 @@ func TestStylesheetFontsAreServed(t *testing.T) { |
| 43 | 45 | } |
| 44 | 46 | } |
| 45 | 47 | } |
| 48 | |
| 49 | // TestLandingImagesAreServed: every file under static/img has a route |
| 50 | // that answers 200 with an image type and the stylesheet's cache policy. |
| 51 | func TestLandingImagesAreServed(t *testing.T) { |
| 52 | s := New(config.Default(), nil) |
| 53 | byPattern := map[string]http.HandlerFunc{} |
| 54 | for _, r := range s.Routes() { |
| 55 | if r.Method == "GET" { |
| 56 | byPattern[r.Pattern] = r.Handler |
| 57 | } |
| 58 | } |
| 59 | entries, err := fs.ReadDir(web.ImageFS, "static/img") |
| 60 | if err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | if len(entries) == 0 { |
| 64 | t.Fatal("no images embedded") |
| 65 | } |
| 66 | for _, e := range entries { |
| 67 | u := "/static/img/" + e.Name() |
| 68 | h, ok := byPattern[u] |
| 69 | if !ok { |
| 70 | t.Errorf("%s: no route", u) |
| 71 | continue |
| 72 | } |
| 73 | rec := httptest.NewRecorder() |
| 74 | h(rec, httptest.NewRequest("GET", u, nil)) |
| 75 | if rec.Code != http.StatusOK || !strings.HasPrefix(rec.Header().Get("Content-Type"), "image/") { |
| 76 | t.Errorf("%s: %d %s", e.Name(), rec.Code, rec.Header().Get("Content-Type")) |
| 77 | } |
| 78 | } |
| 79 | } |
internal/httpd/routes.go
+4
| @@ -53,6 +53,10 @@ func (s *Server) Routes() []Route { |
| 53 | 53 | for _, f := range fonts { |
| 54 | 54 | routes = append(routes, Route{Method: "GET", Pattern: "/static/fonts/" + f.Name(), Handler: s.font}) |
| 55 | 55 | } |
| 56 | images, _ := fs.ReadDir(web.ImageFS, "static/img") |
| 57 | for _, f := range images { |
| 58 | routes = append(routes, Route{Method: "GET", Pattern: "/static/img/" + f.Name(), Handler: s.image}) |
| 59 | } |
| 56 | 60 | routes = append(routes, |
| 57 | 61 | Route{Method: "GET", Pattern: "/favicon.svg", Handler: s.favicon}, |
| 58 | 62 | Route{Method: "GET", Pattern: "/{owner}", Handler: s.ownerPage}, |
internal/httpd/web.go
+19 −2
| @@ -8,6 +8,7 @@ import ( |
| 8 | 8 | "fmt" |
| 9 | 9 | "hash/fnv" |
| 10 | 10 | "io" |
| 11 | "io/fs" |
| 11 | 12 | "log" |
| 12 | 13 | "math" |
| 13 | 14 | "os" |
| @@ -105,6 +106,18 @@ func (s *Server) font(w http.ResponseWriter, r *http.Request) { |
| 105 | 106 | w.Write(data) |
| 106 | 107 | } |
| 107 | 108 | |
| 109 | // image serves the embedded landing pictures with the font cache policy. |
| 110 | func (s *Server) image(w http.ResponseWriter, r *http.Request) { |
| 111 | data, err := web.ImageFS.ReadFile("static" + r.URL.Path[len("/static"):]) |
| 112 | if err != nil { |
| 113 | http.NotFound(w, r) |
| 114 | return |
| 115 | } |
| 116 | w.Header().Set("Content-Type", "image/png") |
| 117 | w.Header().Set("Cache-Control", "public, max-age=604800, immutable") |
| 118 | w.Write(data) |
| 119 | } |
| 120 | |
| 108 | 121 | // notFound renders the designed 404 page with a 404 status. Falls back to |
| 109 | 122 | // the stock plain-text response if the template fails. |
| 110 | 123 | func (s *Server) notFound(w http.ResponseWriter, r *http.Request) { |
| @@ -172,8 +185,12 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) { |
| 172 | 185 | } |
| 173 | 186 | |
| 174 | 187 | // landingPicture says whether the landing page's screenshot images exist to |
| 175 | | // show. Task 15 replaces this with a check of the embedded images. |
| 176 | | var landingPicture = false |
| 188 | // show, checked once against the embedded images. |
| 189 | var landingPicture = func() bool { |
| 190 | _, e1 := fs.Stat(web.ImageFS, "static/img/mr-dark.png") |
| 191 | _, e2 := fs.Stat(web.ImageFS, "static/img/mr-light.png") |
| 192 | return e1 == nil && e2 == nil |
| 193 | }() |
| 177 | 194 | |
| 178 | 195 | func (s *Server) dashboard(w http.ResponseWriter, r *http.Request, viewer store.User) { |
| 179 | 196 | pinned, _ := s.st.PinnedRepos(viewer.ID) |
internal/web/static/img/mr-dark.png
added
Binary file not shown.
internal/web/static/img/mr-light.png
added
Binary file not shown.
internal/web/web.go
+3
| @@ -29,6 +29,9 @@ var FaviconSVG []byte |
| 29 | 29 | //go:embed static/fonts/*.woff2 |
| 30 | 30 | var FontFS embed.FS |
| 31 | 31 | |
| 32 | //go:embed static/img/*.png |
| 33 | var ImageFS embed.FS |
| 34 | |
| 32 | 35 | // version returns the short VCS revision baked into the binary, or "" when |
| 33 | 36 | // built outside a checkout. Used by the layout footer. |
| 34 | 37 | var version = sync.OnceValue(func() string { |