diff --git a/skills/sourcehut-custom-service/references/chrome.md b/skills/sourcehut-custom-service/references/chrome.md index 3cb3b5efb1784d41fab49967d87ec991f0a805f8..4092b52c5e7a6872c2b7b799aef213d295d9eb13 100644 --- a/skills/sourcehut-custom-service/references/chrome.md +++ b/skills/sourcehut-custom-service/references/chrome.md @@ -109,11 +109,7 @@ } chromeSvc := chrome.NewService(conf, "widget.sr.ht") // the literal config section chromeSvc.StyleHref = cssHref - if logo, err := assets.Resolve(staticFS, "static/logo.svg", assets.DefaultPrefix); err != nil { - return nil, err - } else if logo != "" { - chromeSvc.FaviconHref = template.URL(logo) - } + chromeSvc.FaviconHref = chrome.Favicon(faviconGlyph) // the glyph is a package const staticSub, err := fs.Sub(staticFS, "static") if err != nil { @@ -226,7 +222,7 @@ Two facts that bite: - **Embedding names the field `Page`.** A view struct that wants its own `Page` (a pagination counter) gets a compile error, not a silent shadow. Rename yours `PageNum`. -- **`FaviconHref` is `template.URL`.** `html/template` rewrites an `href` whose scheme is not http/https/mailto to `#ZgotmplZ`, and `chrome.DefaultFaviconHref` is a `data:` URI. The type is also the guard: a request value cannot land in it by accident. +- **`FaviconHref` is `template.URL`.** `html/template` rewrites an `href` whose scheme is not http/https/mailto to `#ZgotmplZ`, and every icon `chrome.Favicon` builds is a `data:` URI. The type is also the guard: a request value cannot land in it by accident. Page payload goes under `.Data`, never as a sibling field of the chrome — a page that wanted a `Username` of its own would otherwise silently replace the one the login block reads. @@ -340,7 +336,7 @@ **Why the content check parses each page standalone** (`Options.definesContent`): the obvious check — looking `"content"` up in the assembled set — works only while `layout.html` spells its hole `{{template "content" .}}` while its neighbours are `{{block "head"}}` / `{{block "scripts"}}`. `block` *defines* the name it invokes. The day somebody makes the three consistent — a tidying edit no reviewer would question — `Lookup` starts finding the layout's own empty default on every page and the guard silently stops guarding, producing exactly the failure it exists to prevent. Parsed on its own, a page has only what it defines itself, and no edit to the layout can reach it. Cost: one extra parse per page, once, at startup. **So: `{{template "content" .}}` in your layout. Never `{{block "content" .}}`.** -Two things deliberately **not** startup failures: a missing stylesheet (`assets.Resolve` answers `""`; a service that will not boot without a build artefact cannot be run from a checkout) and a missing favicon (`NewService` has already put a `data:` URI there). +A missing stylesheet is deliberately **not** a startup failure: `assets.Resolve` answers `""`, and a service that will not boot without a build artefact cannot be run from a checkout. The favicon has no such failure to survive any more — it is built from a constant rather than looked up. --- @@ -439,7 +435,28 @@ {{if .StyleHref}}{{end}} {{if .FaviconHref}}{{end}} ``` -A service adding a second asset owes its own template the same `{{if}}`. And resolve a favicon through `assets.Resolve`, not as a literal path: a path a template asserts is one that 404s on every page load if the file is renamed or hashed. Leaving `FaviconHref` unresolved keeps `chrome.DefaultFaviconHref`, a `data:` URI that costs no request and cannot 404. +A service adding a second asset owes its own template the same `{{if}}`. + +### The favicon: one frame, your glyph + +**Never link a favicon by path, and never resolve one out of the static tree.** Declare a glyph and let `chrome.Favicon` build the icon: + +```go +const faviconGlyph chrome.Glyph = `` + + `` + +chromeSvc.FaviconHref = chrome.Favicon(faviconGlyph) +``` + +The glyph is SVG shapes in the frame's own 32×32 coordinates. `Favicon` wraps them in the brand ring every service on the instance shares and returns a `data:` URI, so the icon costs no request, cannot 404, and does not vary with whether a static tree was built or installed. `NewService` starts you on `chrome.DefaultFaviconHref`, the bare frame — which is what a service that declares no glyph renders, so a tab that looks like every other tab is the symptom of a missing `Favicon` call. + +Three rules, each of them a bug that shipped here before the frame existed: + +- **No `currentColor`, ever.** A favicon is loaded as a document of its own — the browser paints it the way `` does, not the way an inline `` is part of the page — so `currentColor` resolves against nothing and collapses to black. It looks right in the page, in a file opened in a tab and in a design tool, and disappears into a dark tab strip, which is the one place it is actually seen. Three services shipped that in unison, each with a comment explaining that `currentColor` was the deliberate choice. The frame's `prefers-color-scheme` media query is what those comments intended. +- **Strokes, not fills.** The frame declares `fill='none'` and colours through `stroke`, so a shape carrying a `fill` of its own keeps that literal colour when the viewer's scheme flips. A shape that genuinely wants a solid body says `class='solid'`. +- **Draw for 16 pixels.** That is the size a favicon is seen at. A heavy ring, three colours, or a glyph with more than about four strokes all reduce to the same grey smudge there — and a smudge is what a service that shares its neighbour's icon already had. + +If you also keep the icon as a file (because `//go:embed static` needs the directory non-empty in a checkout, or because your handler tests want an asset known to be present), generate it — `go run sourcecraft.dev/bigbes/sr-ht-ecore/cmd/faviconsvg '' > web/static/logo.svg` — and assert it in a test against `chrome.FaviconSVG(faviconGlyph)`. A file and a builder are two copies of one picture, and that copy drifts where nobody looks: the tab keeps the current icon while `/static/logo.svg` serves last year's. --- diff --git a/skills/sourcehut-custom-service/references/ecore.md b/skills/sourcehut-custom-service/references/ecore.md index bcf3296ab948c84a5800c8372208459a08ec3835..3f20370cda5f4dff8c0d0d23e8955a580e7eb0f0 100644 --- a/skills/sourcehut-custom-service/references/ecore.md +++ b/skills/sourcehut-custom-service/references/ecore.md @@ -361,7 +361,10 @@ func (s *Service) SelfOrigin() string // also MetaOrigin, HubOrigin, SiteName, Environment func (s *Service) LoginURLFor(r *http.Request) string func Attach(t *template.Template) (*template.Template, error) // + MustAttach func Funcs() template.FuncMap // dict, shortsha, reltime, abstime -const DefaultFaviconHref template.URL +type Glyph string // SVG shapes, 32x32, stroke-only, no colours +func Favicon(g Glyph) template.URL // the shared frame + your glyph, as a data: URI +func FaviconSVG(g Glyph) string // the same picture as source, for a logo.svg +var DefaultFaviconHref = Favicon("") // the bare frame ``` Mutable `*Service` fields, set between `NewService` and the first `Page`: @@ -386,10 +389,18 @@ through the brand, which here is two links (site name → hub, red label → self) rather than upstream's one: chrome that does not name its own service is worse chrome. - `Sections`/`Tabs` render only for `username != ""`, matching the switcher's own rule. Not a permission check — public sections stay reachable by URL. -- `DefaultFaviconHref` is a `data:` URI, not a path: a `` at a build artifact - the binary does not ship 404s on every page load. The `template.URL` typing is - load-bearing (`html/template` rewrites any non-http/https/mailto href to - `#ZgotmplZ`) and doubles as the guard against a request-derived value landing there. +- **The favicon is built, never resolved.** `Favicon(glyph)` draws the shared + brand ring around your glyph and returns a `data:` URI — no path to 404 on + every page load, nothing that varies with whether a static tree was built. + `NewService` starts you on `DefaultFaviconHref`, the bare frame, which is + exactly what a service that declares no glyph renders; a tab identical to its + neighbours' is the symptom. The `template.URL` typing is load-bearing + (`html/template` rewrites any non-http/https/mailto href to `#ZgotmplZ`) and + doubles as the guard against a request-derived value landing there. + A glyph brings no colours of its own: `currentColor` in a favicon resolves + against nothing (it is its own document) and collapses to black, and a `fill` + outlives the frame's `prefers-color-scheme` rule. Keeping the icon as a file + too? Generate it with `cmd/faviconsvg` and assert it against `FaviconSVG`. - `chrome.Page` is meant to be embedded in a service's view struct; a same-named field collides at compile time, not silently. - `pages.Load` calls `chrome.Attach` internally — do not attach twice.