@@ 2,6 2,7 @@ package web
import (
"fmt"
+ "html/template"
"io/fs"
"net/http"
"os"
@@ 35,6 36,11 @@ const (
devStyleFile = "main.css"
)
+// faviconFile is our own icon in the static tree. Unhashed, so it is linked by
+// name and not through assets.Resolve — it changes about as often as the
+// service is renamed.
+const faviconFile = "logo.svg"
+
// app bundles the parsed templates, the shared chrome and the injected config.
// Handlers are methods on *app so they share this state without a global.
type app struct {
@@ 127,6 133,15 @@ func newApp(cfg Config) (*app, error) {
chromeSvc := chrome.NewService(cfg.Conf, serviceName)
chromeSvc.StyleHref = styleHref
+ // Our own logo when this build ships one, and NewService's built-in data:
+ // URI when it does not. The href used to be written into the layout, which
+ // meant a deployment without a static tree — a test, a binary run out of a
+ // working copy — requested a file that was not there once per page. The
+ // existence check is the stylesheet's, for the same reason.
+ if _, err := fs.Stat(static, faviconFile); err == nil {
+ chromeSvc.FaviconHref = template.URL(assets.NormalizePrefix(assets.DefaultPrefix) + faviconFile)
+ }
+
return &app{
cfg: cfg,
pages: set,
@@ 24,10 24,12 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title>
- <link rel="icon" type="image/svg+xml" href="/static/logo.svg" />
- {{/* Guarded rather than emitted empty: <link href=""> re-requests the page
- it is on, which is one extra page load per page load. */}}
- {{if .StyleHref}}<link rel="stylesheet" href="{{.StyleHref}}">{{end}}
+ {{/* The stylesheet and the favicon both come from the shared partial, which
+ guards each rather than emitting it empty: <link href=""> re-requests
+ the page it is on, one extra page load per page load. The favicon href
+ is the chrome's, so a build that ships no logo links ecore's built-in
+ data: URI instead of 404ing once per page. */}}
+ {{template "srht-head-links" .}}
</head>
<body>
{{template "srht-env-banner" .}}
@@ 902,6 902,41 @@ func TestStylesheetFallsBackToTheUnhashedBuildOnlyWhenItExists(t *testing.T) {
assert.NotContains(t, bare.Body.String(), `rel="stylesheet"`)
}
+// The favicon is the chrome's href now, not a literal in the layout: our own
+// logo when the build ships one, and ecore's built-in data: URI when it does
+// not. The href used to be written into the template unconditionally, so a
+// deployment without a static tree asked for a file that was not there once per
+// page.
+func TestFaviconIsOursWhenShippedAndTheBuiltInOtherwise(t *testing.T) {
+ dir := t.TempDir()
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "logo.svg"), []byte("<svg/>"), 0o644))
+
+ shipped := newHarnessWithStatic(t, dir).do("GET", "/", nil, nil)
+ require.Equal(t, http.StatusOK, shipped.Code)
+ assert.Contains(t, shipped.Body.String(), `rel="icon" href="/static/logo.svg"`)
+
+ bare := newHarnessWithStatic(t, t.TempDir()).do("GET", "/", nil, nil)
+ require.Equal(t, http.StatusOK, bare.Code)
+ assert.Contains(t, bare.Body.String(), `rel="icon" href="data:`,
+ "a build with no logo links the shared data: URI, never a 404")
+ assert.NotContains(t, bare.Body.String(), `href="/static/logo.svg"`)
+}
+
+// The listing partial's Updated and Meta are optional in practice and not only
+// in ecore's doc comment: this service has no timestamp in its schema, leaves
+// both zero, and must get a card with no muted footer rather than "0001-01-01".
+func TestDatabaseListingRendersNoTimestampBlock(t *testing.T) {
+ h := newHarness(t)
+ h.store.add(&core.Repo{Name: "pub", OwnerID: 1, OwnerName: "alice", Path: "/p", Visibility: core.VisibilityPublic})
+
+ rec := h.do("GET", "/~alice", nil, nil)
+ require.Equal(t, http.StatusOK, rec.Code)
+ body := rec.Body.String()
+ assert.Contains(t, body, "/~alice/pub")
+ assert.NotContains(t, body, "0001-01-01", "a zero Updated must render nothing at all")
+ assert.NotContains(t, body, `<small class="text-muted">`)
+}
+
func TestLogAndTablePages(t *testing.T) {
h := newHarness(t)
h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic})