Commit d4d1ff98e9
d4d1ff98e92b3ee021a17e319dcbc553319989dd
parent: 6bbced84d6
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-23 23:16 UTC
landing: the signup-to-release recording
Autoplays muted with controls above the merge request picture. The static image route serves gif, webm and mp4 by extension through ServeContent, which answers Range; Safari will not play video without it.
Ref #253
internal/httpd/fonts_test.go
+13 −3
| @@ -47,7 +47,7 @@ func TestStylesheetFontsAreServed(t *testing.T) { |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | 49 | // TestLandingImagesAreServed: every file under static/img has a route |
| 50 | | // that answers 200 with an image type and the stylesheet's cache policy. |
| 50 | // that answers 200 with an image or video type, and a video answers Range. |
| 51 | 51 | func TestLandingImagesAreServed(t *testing.T) { |
| 52 | 52 | s := New(config.Default(), nil) |
| 53 | 53 | byPattern := map[string]http.HandlerFunc{} |
| @@ -72,8 +72,18 @@ func TestLandingImagesAreServed(t *testing.T) { |
| 72 | 72 | } |
| 73 | 73 | rec := httptest.NewRecorder() |
| 74 | 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")) |
| 75 | ct := rec.Header().Get("Content-Type") |
| 76 | if rec.Code != http.StatusOK || !(strings.HasPrefix(ct, "image/") || strings.HasPrefix(ct, "video/")) { |
| 77 | t.Errorf("%s: %d %q", e.Name(), rec.Code, ct) |
| 78 | } |
| 79 | if strings.HasPrefix(ct, "video/") { |
| 80 | rec := httptest.NewRecorder() |
| 81 | req := httptest.NewRequest("GET", u, nil) |
| 82 | req.Header.Set("Range", "bytes=0-1") |
| 83 | h(rec, req) |
| 84 | if rec.Code != http.StatusPartialContent || rec.Body.Len() != 2 { |
| 85 | t.Errorf("%s: Range answered %d with %d bytes", e.Name(), rec.Code, rec.Body.Len()) |
| 86 | } |
| 77 | 87 | } |
| 78 | 88 | } |
| 79 | 89 | } |
internal/httpd/web.go
+13 −4
| @@ -117,16 +117,25 @@ func (s *Server) font(w http.ResponseWriter, r *http.Request) { |
| 117 | 117 | w.Write(data) |
| 118 | 118 | } |
| 119 | 119 | |
| 120 | | // image serves the embedded landing pictures with the font cache policy. |
| 120 | var staticTypes = map[string]string{ |
| 121 | ".png": "image/png", |
| 122 | ".gif": "image/gif", |
| 123 | ".webm": "video/webm", |
| 124 | ".mp4": "video/mp4", |
| 125 | } |
| 126 | |
| 127 | // image serves the embedded landing pictures and recording with the font |
| 128 | // cache policy. ServeContent answers Range, which Safari needs to play video. |
| 121 | 129 | func (s *Server) image(w http.ResponseWriter, r *http.Request) { |
| 122 | | data, err := web.ImageFS.ReadFile("static" + r.URL.Path[len("/static"):]) |
| 130 | name := "static" + r.URL.Path[len("/static"):] |
| 131 | data, err := web.ImageFS.ReadFile(name) |
| 123 | 132 | if err != nil { |
| 124 | 133 | http.NotFound(w, r) |
| 125 | 134 | return |
| 126 | 135 | } |
| 127 | | w.Header().Set("Content-Type", "image/png") |
| 136 | w.Header().Set("Content-Type", staticTypes[path.Ext(name)]) |
| 128 | 137 | w.Header().Set("Cache-Control", "public, max-age=604800, immutable") |
| 129 | | w.Write(data) |
| 138 | http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(data)) |
| 130 | 139 | } |
| 131 | 140 | |
| 132 | 141 | // notFound renders the designed 404 page with a 404 status. Falls back to |
internal/web/static/img/demo.gif
added
Binary file not shown.
internal/web/static/img/demo.mp4
added
Binary file not shown.
internal/web/static/img/demo.webm
added
Binary file not shown.
internal/web/static/style.css
+1
| @@ -1704,6 +1704,7 @@ details.refmenu .refdrop a.allrefs { |
| 1704 | 1704 | pre.quickstart { margin: 0 0 var(--sp-4); } |
| 1705 | 1705 | .shot { border: 1px solid var(--line); border-radius: var(--r-card); overflow: hidden; margin: var(--sp-5) 0; } |
| 1706 | 1706 | .shot img { display: block; width: 100%; height: auto; max-height: 420px; object-fit: cover; object-position: top; } |
| 1707 | .shot video { display: block; width: 100%; height: auto; } |
| 1707 | 1708 | .shot figcaption { |
| 1708 | 1709 | border-top: 1px solid var(--line); |
| 1709 | 1710 | background: var(--surface); |
internal/web/templates/landing.html
+6
| @@ -7,6 +7,12 @@ |
| 7 | 7 | <pre class="quickstart" tabindex="0">ssh git@{{.Host}} help # every command, no client to install |
| 8 | 8 | git clone ssh://git@{{.Host}}/owner/repo.git</pre> |
| 9 | 9 | <div class="routes"><a class="button primary" href="/explore">Explore repositories</a>{{if .Signup}}<a class="button btn" href="/register">Create an account</a>{{end}}</div> |
| 10 | <figure class="shot"><video autoplay muted loop playsinline controls width="1248" height="688" aria-describedby="demo-caption"> |
| 11 | <source src="/static/img/demo.webm" type="video/webm"> |
| 12 | <source src="/static/img/demo.mp4" type="video/mp4"> |
| 13 | </video> |
| 14 | <figcaption id="demo-caption">Signup to release on gitbay.org from a terminal: register an SSH key, verify the mailed code, create a repository and push, file an issue, open and merge a request that closes it, then tag and publish a release.</figcaption> |
| 15 | </figure> |
| 10 | 16 | <figure class="shot"><picture> |
| 11 | 17 | <source srcset="/static/img/mr-dark.png" media="(prefers-color-scheme: dark)"> |
| 12 | 18 | <img src="/static/img/mr-light.png" width="1280" height="900" alt="A merged merge request: the description on the left, and on the right two CI checks reporting success with how long each ran, an approval, and the source and target branches."> |
internal/web/web.go
+1 −1
| @@ -29,7 +29,7 @@ var FaviconSVG []byte |
| 29 | 29 | //go:embed static/fonts/*.woff2 |
| 30 | 30 | var FontFS embed.FS |
| 31 | 31 | |
| 32 | | //go:embed static/img/*.png |
| 32 | //go:embed static/img/*.png static/img/*.gif static/img/*.webm static/img/*.mp4 |
| 33 | 33 | var ImageFS embed.FS |
| 34 | 34 | |
| 35 | 35 | // version returns the short VCS revision baked into the binary, or "" when |