socle

kicker: deep pages re-assert whose exhibit this is

commit 9f3639044e82add0c3ae0f5a37ee21ee6b70fede · parent 2d5549287a · browse this tree
Ximianth, 2026-10-30 14:40:44 UTC

Tree, file, log, and commit pages carry the repo name above their heading,
in the provenance register, linking home: a file's name matters, but so does
the project context it sits in.  Omitted on the root listing, whose heading
is already the repo name.
diff --git a/chrome.go b/chrome.go
index a813562..47f1d5e 100644
--- a/chrome.go
+++ b/chrome.go
@@ -338,6 +338,16 @@ footer.ground .colophon {
 	margin: .5rem 0 1.1rem;
 }
 
+/* The kicker: which project this sheet belongs to, re-asserted above the heading
+   on pages deep in a repo, in the provenance register; the name links home. */
+article .kicker {
+	margin: 0 0 -.6rem;
+	font-size: .78rem;
+	letter-spacing: .16em;
+	color: var(--ink-soft);
+}
+article .kicker a { color: inherit; }
+
 /* The ledger: provenance rows on the log page.  Subjects are prose and wrap
    freely; the attribution line stays soft and small.  Links wear ink, not accent:
    a hundred accent rows would shout; hover answers the "is it live" question. */
diff --git a/pages.go b/pages.go
index 4060649..139b168 100644
--- a/pages.go
+++ b/pages.go
@@ -333,7 +333,7 @@ func (s *server) renderReadme(rp *repo, ref, sha string) (template.HTML, readmeI
 // The catalogue: listings keep their tabular bones, with light hand-ruled row
 // separators and a drawer-tab mark ahead of directory names (the mark is the
 // link's own background, so it is part of the tap target).
-var treeTmpl = template.Must(template.New("tree").Parse(`<h1>{{.Title}}</h1>
+var treeTmpl = template.Must(template.New("tree").Parse(`{{template "kicker" .Kicker}}<h1>{{.Title}}</h1>
 <table class="tree">
 <tbody>
 {{range .Rows}}<tr><td class="name{{if .Dir}} dir{{end}}">{{if .Href}}<a href="{{.Href}}">{{.Name}}</a>{{else}}{{.Name}}{{end}}</td><td class="size">{{.Size}}</td></tr>
@@ -341,6 +341,12 @@ var treeTmpl = template.Must(template.New("tree").Parse(`<h1>{{.Title}}</h1>
 </table>
 `))
 
+// kickerTmpl is the shared context line above a page heading: which project this
+// sheet belongs to, linking home.  Deep pages re-assert it because a file's name
+// matters, but so does whose exhibit it is; empty means the heading already says so.
+var kickerTmpl = template.Must(template.New("kicker").Parse(`{{if .}}<p class="kicker"><a href="/{{.}}/">{{.}}</a></p>
+{{end}}`))
+
 type treeRow struct {
 	Name string
 	Href string // empty renders unlinked (submodules)
@@ -349,8 +355,9 @@ type treeRow struct {
 }
 
 type treeData struct {
-	Title string
-	Rows  []treeRow
+	Title  string
+	Kicker string // the repo name, when the title doesn't already say it
+	Rows   []treeRow
 }
 
 func (s *server) serveTreeListing(w http.ResponseWriter, snap *snapshot, rp *repo, ref, sha, pth string) {
@@ -359,9 +366,10 @@ func (s *server) serveTreeListing(w http.ResponseWriter, snap *snapshot, rp *rep
 		httpError(w, err)
 		return
 	}
-	d := treeData{Title: pth}
+	d := treeData{Title: pth, Kicker: rp.name}
 	if pth == "" {
-		d.Title = rp.name
+		// The root listing's heading is the repo name itself; a kicker would stutter.
+		d.Title, d.Kicker = rp.name, ""
 	}
 	if pth != "" {
 		d.Rows = append(d.Rows, treeRow{Name: "..", Href: treeHref(rp.name, ref, parentDir(pth)) + "/"})
@@ -468,22 +476,26 @@ var (
 	preTmpl    = template.Must(template.New("pre").Parse(`<pre class="file">{{.}}</pre>`))
 	plaqueTmpl = template.Must(template.New("plaque").Parse(`<p class="plaque">{{.Path}} · {{.Size}} · <code>{{.ShortSha}}</code></p>
 `))
-	imageTmpl = template.Must(template.New("image").Parse(`<h1>{{.Name}}</h1>
+	imageTmpl = template.Must(template.New("image").Parse(`{{template "kicker" .Kicker}}<h1>{{.Name}}</h1>
 <div class="exhibit"><p><img class="file" src="{{.RawHref}}" alt="{{.Name}}"></p></div>
 {{template "plaque" .Plaque}}`))
-	fileTmpl = template.Must(template.New("file").Parse(`<h1>{{.Name}}</h1>
+	fileTmpl = template.Must(template.New("file").Parse(`{{template "kicker" .Kicker}}<h1>{{.Name}}</h1>
 <div class="exhibit">{{.Body}}</div>
 {{template "plaque" .Plaque}}`))
-	rawLinkTmpl = template.Must(template.New("rawlink").Parse(`<h1>{{.Name}}</h1>
+	rawLinkTmpl = template.Must(template.New("rawlink").Parse(`{{template "kicker" .Kicker}}<h1>{{.Name}}</h1>
 <p>{{.Size}} bytes of {{.Kind}}; <a href="{{.RawHref}}">view raw</a>.</p>
 `))
 )
 
 func init() {
-	// The plaque fragment is shared by the file-view templates.
+	// The plaque fragment is shared by the file-view templates,
+	// and the kicker by every template that re-asserts its project above the h1.
 	for _, t := range []*template.Template{imageTmpl, fileTmpl} {
 		template.Must(t.AddParseTree("plaque", plaqueTmpl.Tree))
 	}
+	for _, t := range []*template.Template{treeTmpl, imageTmpl, fileTmpl, rawLinkTmpl, logTmpl, commitTmpl} {
+		template.Must(t.AddParseTree("kicker", kickerTmpl.Tree))
+	}
 }
 
 // plaqueData is the museum label under a file exhibit.
@@ -516,16 +528,16 @@ func (s *server) serveFileView(w http.ResponseWriter, snap *snapshot, rp *repo,
 	plq := plaqueData{Path: pth, Size: humanSize(size), ShortSha: shortSha(sha)}
 	if isImagePath(pth) {
 		s.render(w, snap, imageTmpl, struct {
-			Name, RawHref string
-			Plaque        plaqueData
-		}{name, raw, plq}, pd)
+			Name, Kicker, RawHref string
+			Plaque                plaqueData
+		}{name, rp.name, raw, plq}, pd)
 		return
 	}
 	rawLink := func(kind string) {
 		s.render(w, snap, rawLinkTmpl, struct {
-			Name, Kind, RawHref string
-			Size                int64
-		}{name, kind, raw, size}, pd)
+			Name, Kicker, Kind, RawHref string
+			Size                        int64
+		}{name, rp.name, kind, raw, size}, pd)
 	}
 	if size > display.MaxRenderSize {
 		rawLink("file, too large to display inline")
@@ -547,10 +559,10 @@ func (s *server) serveFileView(w http.ResponseWriter, snap *snapshot, rp *repo,
 			return
 		}
 		s.render(w, snap, fileTmpl, struct {
-			Name   string
-			Body   template.HTML
-			Plaque plaqueData
-		}{name, body, plq}, pd)
+			Name, Kicker string
+			Body         template.HTML
+			Plaque       plaqueData
+		}{name, rp.name, body, plq}, pd)
 		return
 	}
 	var pre bytes.Buffer
@@ -559,10 +571,10 @@ func (s *server) serveFileView(w http.ResponseWriter, snap *snapshot, rp *repo,
 		return
 	}
 	s.render(w, snap, fileTmpl, struct {
-		Name   string
-		Body   template.HTML
-		Plaque plaqueData
-	}{name, template.HTML(pre.String()), plq}, pd)
+		Name, Kicker string
+		Body         template.HTML
+		Plaque       plaqueData
+	}{name, rp.name, template.HTML(pre.String()), plq}, pd)
 }
 
 // looksBinary applies the classic cheap heuristic: a NUL byte in the leading bytes means
@@ -581,7 +593,7 @@ func looksBinary(b []byte) bool {
 // line -- the subject as prose, free to wrap, and a soft second line carrying the
 // sha (a chip-outlined button to the commit page), author, and absolute date.
 // This shape is also why the page survives phone widths.
-var logTmpl = template.Must(template.New("log").Parse(`<h1>log: {{.Title}}</h1>
+var logTmpl = template.Must(template.New("log").Parse(`{{template "kicker" .Kicker}}<h1>log: {{.Title}}</h1>
 <p class="provenance">provenance · newest first</p>
 <ul class="ledger">
 {{range .Rows}}<li>
@@ -602,6 +614,7 @@ type logRow struct {
 
 type logData struct {
 	Title  string
+	Kicker string
 	Rows   []logRow
 	Capped bool
 	Limit  int
@@ -610,7 +623,7 @@ type logData struct {
 // renderLog writes the log page.  Commit shas link to the tree at that commit:
 // commit-diff pages are a later stretch goal, and browsing the snapshot is useful today.
 func (s *server) renderLog(w http.ResponseWriter, snap *snapshot, rp *repo, ref string, cs []commitInfo) {
-	d := logData{Title: shortSha(ref), Capped: len(cs) == logLimit, Limit: logLimit}
+	d := logData{Title: shortSha(ref), Kicker: rp.name, Capped: len(cs) == logLimit, Limit: logLimit}
 	for _, c := range cs {
 		d.Rows = append(d.Rows, logRow{
 			Sha:     shortSha(c.hash),
@@ -632,7 +645,7 @@ func (s *server) renderLog(w http.ResponseWriter, snap *snapshot, rp *repo, ref
 
 // ---- commit ----
 
-var commitTmpl = template.Must(template.New("commit").Parse(`<h1>{{.Subject}}</h1>
+var commitTmpl = template.Must(template.New("commit").Parse(`{{template "kicker" .Kicker}}<h1>{{.Subject}}</h1>
 <p>commit <code>{{.Sha}}</code>{{range .Parents}} · parent <a href="{{.Href}}"><code>{{.Name}}</code></a>{{end}}
 · <a href="{{.TreeHref}}">browse this tree</a><br>
 {{.Author}}, {{.Date}}</p>
@@ -644,6 +657,7 @@ var commitTmpl = template.Must(template.New("commit").Parse(`<h1>{{.Subject}}</h
 
 type commitData struct {
 	Subject   string
+	Kicker    string
 	Rest      string // the message past its subject line, if any
 	Sha       string
 	Author    string
@@ -659,6 +673,7 @@ func (s *server) renderCommit(w http.ResponseWriter, snap *snapshot, rp *repo, c
 	subject, rest, _ := strings.Cut(c.message, "\n")
 	d := commitData{
 		Subject:  subject,
+		Kicker:   rp.name,
 		Rest:     strings.TrimSpace(rest),
 		Sha:      c.hash,
 		Author:   c.author,
diff --git a/testdata/pages.md b/testdata/pages.md
index cf7a652..b83feb6 100644
--- a/testdata/pages.md
+++ b/testdata/pages.md
@@ -202,7 +202,8 @@ Content-Type: text/html; charset=utf-8
 <a class="vb" href="/fixturama/log/main" title="commit log of main" style="--dy:18px">log</a>
 <a class="vc" href="/fixturama/" title="repo home" style="--dy:2px">home</a>
 </nav>
-<main><article><h1>docs</h1>
+<main><article><p class="kicker"><a href="/fixturama/">fixturama</a></p>
+<h1>docs</h1>
 <table class="tree">
 <tbody>
 <tr><td class="name"><a href="/fixturama/tree/main/">..</a></td><td class="size"></td></tr>
@@ -258,7 +259,8 @@ Content-Type: text/html; charset=utf-8
 <a class="vc" href="/fixturama/log/main" title="commit log of main" style="--dy:18px">log</a>
 <a class="va" href="/fixturama/" title="repo home" style="--dy:2px">home</a>
 </nav>
-<main><article><h1>README.md</h1>
+<main><article><p class="kicker"><a href="/fixturama/">fixturama</a></p>
+<h1>README.md</h1>
 <div class="exhibit"><h1 id="fixturama">Fixturama<a href="#fixturama" class="anchor">🔗</a></h1>
 <p>A repo that exists to be looked at.</p>
 </div>
@@ -304,7 +306,8 @@ Content-Type: text/html; charset=utf-8
 <a class="vc" href="/fixturama/log/main" title="commit log of main" style="--dy:18px">log</a>
 <a class="va" href="/fixturama/" title="repo home" style="--dy:2px">home</a>
 </nav>
-<main><article><h1>with space.txt</h1>
+<main><article><p class="kicker"><a href="/fixturama/">fixturama</a></p>
+<h1>with space.txt</h1>
 <div class="exhibit"><pre class="file">even this filename works
 </pre></div>
 <p class="plaque">docs/with space.txt · 25 B · <code>ced91a8519</code></p>
@@ -350,7 +353,8 @@ Content-Type: text/html; charset=utf-8
 <a class="vc" href="/fixturama/log/ced91a8519b59966124301e6383532f24223528b" title="commit log of ced91a8519" style="--dy:18px">log</a>
 <a class="va" href="/fixturama/" title="repo home" style="--dy:2px">home</a>
 </nav>
-<main><article><h1>README.md</h1>
+<main><article><p class="kicker"><a href="/fixturama/">fixturama</a></p>
+<h1>README.md</h1>
 <div class="exhibit"><h1 id="fixturama">Fixturama<a href="#fixturama" class="anchor">🔗</a></h1>
 <p>A repo that exists to be looked at.</p>
 </div>
@@ -398,7 +402,8 @@ Content-Type: text/html; charset=utf-8
 <a class="va" href="/fixturama/tree/main/" title="browse the tree at main" style="--dy:18px">files</a>
 <a class="vb" href="/fixturama/" title="repo home" style="--dy:2px">home</a>
 </nav>
-<main><article><h1>log: main</h1>
+<main><article><p class="kicker"><a href="/fixturama/">fixturama</a></p>
+<h1>log: main</h1>
 <p class="provenance">provenance · newest first</p>
 <ul class="ledger">
 <li>
@@ -457,7 +462,8 @@ Content-Type: text/html; charset=utf-8
 <a class="vb" href="/fixturama/log/ced91a8519b59966124301e6383532f24223528b" title="the log from this commit" style="--dy:18px">log</a>
 <a class="vc" href="/fixturama/" title="repo home" style="--dy:2px">home</a>
 </nav>
-<main><article><h1>add docs and src</h1>
+<main><article><p class="kicker"><a href="/fixturama/">fixturama</a></p>
+<h1>add docs and src</h1>
 <p>commit <code>ced91a8519b59966124301e6383532f24223528b</code> · parent <a href="/fixturama/commit/64ac67a35d36079e167cb8f929fd57f859c8b5d1"><code>64ac67a35d</code></a>
 · <a href="/fixturama/tree/ced91a8519b59966124301e6383532f24223528b/">browse this tree</a><br>
 Ada Fixture, 2026-01-02 03:04:05 UTC</p>
@@ -570,7 +576,8 @@ Content-Type: text/html; charset=utf-8
 <a class="vb" href="/fixturama/log/64ac67a35d36079e167cb8f929fd57f859c8b5d1" title="the log from this commit" style="--dy:18px">log</a>
 <a class="vc" href="/fixturama/" title="repo home" style="--dy:2px">home</a>
 </nav>
-<main><article><h1>inaugurate with a readme</h1>
+<main><article><p class="kicker"><a href="/fixturama/">fixturama</a></p>
+<h1>inaugurate with a readme</h1>
 <p>commit <code>64ac67a35d36079e167cb8f929fd57f859c8b5d1</code>
 · <a href="/fixturama/tree/64ac67a35d36079e167cb8f929fd57f859c8b5d1/">browse this tree</a><br>
 Ada Fixture, 2026-01-02 03:04:05 UTC</p>