~bigbes/sr-ht-compare

6b1f4da2790421dca4fcd5f0395266381718c6f0 — bigbes 9 days ago e01e9ed
chrome: the head links from ecore, and no shipped favicon

layout.html hardcoded <link rel="icon" href="/static/logo.svg">, and
web/static/logo.svg was this service's copy of the brand circle chrome
now inlines as a data: URI. Both go: the head renders srht-head-links,
which emits the stylesheet and the icon guarded on emptiness, and the
icon costs no request and cannot 404.

The static tree is two hashed artefacts now, so the arm of
TestStaticBundleAndCSS that asserted an hour-cacheable unhashed asset has
nothing left to ask about; TestFaviconIsInline takes its place and also
pins that the data: URI reaches the page rather than #ZgotmplZ.
5 files changed, 32 insertions(+), 29 deletions(-)

M README.md
D web/static/logo.svg
M web/templates.go
M web/templates/layout.html
M web/web_test.go
M README.md => README.md +1 -1
@@ 223,7 223,7 @@ compare.sr.ht deploys like any other SourceHut web service. On the instance:
   ```sh
   make install PREFIX=/usr/local
   # → /usr/local/bin/comparesrht
   # → /usr/share/sourcehut/compare.sr.ht/static/{bundle.<hash>.js,main.min.<hash>.css,logo.svg}
   # → /usr/share/sourcehut/compare.sr.ht/static/{bundle.<hash>.js,main.min.<hash>.css}
   ```

2. **Config.** Append the `[compare.sr.ht]` section (`origin=` and

D web/static/logo.svg => web/static/logo.svg +0 -14
@@ 1,14 0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
	<style>
		@media (prefers-color-scheme: light) {
			#outline { display: none }
			#logo { stroke: black }
		}
		@media (prefers-color-scheme: dark) {
			#outline { display: none }
			#logo { stroke: white }
		}
	</style>
	<circle id="outline" cx="64" cy="64" r="50" fill="none" stroke-width="14" stroke="#888" />
	<circle id="logo" cx="64" cy="64" r="50" fill="none" stroke-width="10" stroke="white" />
</svg>

M web/templates.go => web/templates.go +3 -2
@@ 19,8 19,9 @@ import (
//go:embed templates/*.html
var tmplFS embed.FS

// staticFS holds the built front-end assets (the hashed bundle.<hash>.js, the
// hashed stylesheet, logo.svg). It is served read-only under /static/.
// staticFS holds the built front-end assets: the hashed bundle.<hash>.js and
// the hashed stylesheet, and nothing else since the favicon became chrome's
// inlined one. It is served read-only under /static/.
//
//go:embed static
var staticFS embed.FS

M web/templates/layout.html => web/templates/layout.html +8 -7
@@ 19,13 19,14 @@
    <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 a page load per page load. assets.Resolve answers ""
         for a binary built without `make css`, which is a checkout run from
         source; a deployed build always has the artefact and always takes
         this. */}}
    {{if .StyleHref}}<link rel="stylesheet" href="{{.StyleHref}}">{{end}}
    {{/* The stylesheet and the icon, both guarded rather than emitted empty:
         <link href=""> re-requests the page it is on, which is a page load per
         page load. assets.Resolve answers "" for a binary built without `make
         css`, which is a checkout run from source; a deployed build always has
         the artefact. The icon is chrome's own brand circle, inlined as a
         data: URI — it cannot 404 and costs no request, which the copy of the
         same circle this service shipped under /static/logo.svg did. */}}
    {{template "srht-head-links" .}}
    <style>
      /* Semantic status badge for the changed-files table. The default table
         cell colour is too dim on the dark chrome, so each git status gets an

M web/web_test.go => web/web_test.go +20 -5
@@ 451,12 451,27 @@ func TestStaticBundleAndCSS(t *testing.T) {
		"a hashed stylesheet must be cacheable forever")
	assert.Empty(t, rec.Header().Get("Vary"),
		"an asset served identically to everybody must not be declared to vary on Cookie")
}

// TestFaviconIsInline pins that the icon costs no request and no file. It used
// to be /static/logo.svg — this service's copy of the brand circle chrome now
// inlines as a data: URI, which is also the only value html/template will let
// through a href without rewriting it to #ZgotmplZ.
func TestFaviconIsInline(t *testing.T) {
	root, _ := gitFixture(t)
	h := testServer(t, root, demoAuthorizer())

	// An asset whose name carries no hash cannot be immutable: the next deploy
	// serves different bytes at the same URL.
	rec = get(t, h, "/static/logo.svg", "")
	rec := get(t, h, "/", "")
	require.Equal(t, http.StatusOK, rec.Code)
	assert.Equal(t, "public, max-age=3600", rec.Header().Get("Cache-Control"))
	// html/template writes the '+' of "svg+xml" as &#43;, which is why the
	// prefix this looks for stops short of it.
	assert.Contains(t, rec.Body.String(), `<link rel="icon" href="data:image/svg`)
	assert.NotContains(t, rec.Body.String(), "#ZgotmplZ",
		"a data: URI that arrived as a plain string would have been rewritten")
	assert.NotContains(t, rec.Body.String(), "logo.svg")

	assert.Equal(t, http.StatusNotFound, get(t, h, "/static/logo.svg", "").Code,
		"the shipped copy of the icon is gone")
}

// TestStaticListingRefused pins what the hand-rolled static route published


@@ 475,7 490,7 @@ func TestStaticListingRefused(t *testing.T) {
			// A Go file-server listing is a list of <a href="name">name</a>; the
			// stylesheet's own <link> in the chrome is not, which is why the
			// entry and not the name is what this looks for.
			assert.NotContains(t, rec.Body.String(), `<a href="logo.svg">`,
			assert.NotContains(t, rec.Body.String(), `<a href="`+bundleName(t)+`">`,
				"the response is a directory listing")
			// And it is a page, not net/http's plain-text dead end.
			assert.Contains(t, rec.Body.String(), "navbar-brand")