{{- /* The masthead's light/dark switch, both halves of it. Reading the stored choice has to happen here - inline, in , above the stylesheet - and not from a deferred or end-of-body script: a script that runs after first paint means the page is painted in the wrong theme and then corrected, which is the white flash this whole arrangement exists to avoid. It is a handful of statements precisely because everything in front of it is blocked on them. What it writes is only ever an *override*: `data-theme` is absent until the reader asks for something other than what their OS says, and pressing the switch back to agreement removes it again rather than pinning the same value. So a machine that flips to dark at sunset carries the site with it, which pinning-by-default would quietly break. style.scss's `dark` mixin spells out the matching CSS condition. The two theme-color metas above are wired the same way. While the OS decides they keep their media queries and the browser picks; a pinned theme rewrites the pair so exactly one of them matches, since a browser takes the first theme-color whose media applies and would otherwise go on tinting its chrome from the OS preference alone. The click listener is delegated from the document rather than bound to the button: hx-boost morphs a freshly rendered masthead in on every navigation, and a listener attached to the element it replaces would go with it. Nothing is written onto the button either - which face the icon shows and which of its two labels is the accessible name both fall out of `data-theme` in CSS - so a swap has nothing of ours to revert. */}} {{ if or site.Params.noindex .Params.noindex }} {{ end }} {{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} · {{ site.Title }}{{ end }} {{- /* Priority: this page's own front matter > an explicit site-wide params.description override > i18n "bio", stripped of markdown down to plain text. Set params.description only when the SEO snippet should say something different from the colophon bio; otherwise bio is the single source of truth for both. */}} {{- $desc := .Description }} {{- if not $desc }}{{ $desc = site.Params.description }}{{ end }} {{- if not $desc }}{{ with i18n "bio" }}{{ $desc = . | markdownify | plainify | htmlUnescape }}{{ end }}{{ end }} {{- /* The bio's
leaves a line break behind once the tags are gone, and a newline inside the content attribute is not what a snippet wants. */}} {{- $desc = $desc | replaceRE `\s+` " " | strings.TrimSpace }} {{ with $desc -}} {{- end }} {{- /* Self-referencing, not pointed at one "main" language: each language build already says how it relates to the others via the hreflang loop below, so this just states what search engines should treat as the definitive URL for *this* copy - itself. */}} {{- $pageImage := partial "page-image.html" . }} {{ partial "opengraph.html" (dict "page" . "desc" $desc "image" $pageImage) }} {{ partial "jsonld.html" (dict "page" . "desc" $desc "image" $pageImage) }} {{- /* fingerprint appends a content hash to the filename (style.abc123.css) and gives us a real Subresource Integrity hash for free - without it this was the one asset with no cache-busting at all: same URL forever, so a browser (or CDN) holding a stale cached copy from before a deploy has no way to know a new one exists. */}} {{ with resources.Get "css/style.scss" }} {{ $opts := dict "transpiler" "dartsass" "outputStyle" "compressed" }} {{ with . | toCSS $opts | fingerprint }} {{ end }} {{ end }} {{- /* One hreflang tag per translated version of this page, so search engines serve readers the right language instead of always the one that happens to rank. x-default points at the lowest-weight (English) translation, since that's the site's default language. */}} {{- $translations := sort .AllTranslations "Language.Weight" }} {{- range $translations }} {{- end }} {{- with index $translations 0 }} {{- end }} {{- /* Declarative, not htmx.config.preload = {...} in a script: a tag is parsed with the rest of the document, before any script (deferred or not) runs, so htmx's constructor sees it synchronously and correctly every time - no dependency on inline-script/defer/DOMContentLoaded ordering at all. boostEvent switches hx-preload's trigger from the default mousedown/touchstart to mouseover, so the fetch for a boosted link starts the instant the cursor touches it rather than waiting for a press. */}} {{ with resources.Get "js/htmx.min.js" | fingerprint }} {{ end }} {{- /* Official htmx extension, vendored from the same htmx.org package as htmx.min.js above (node_modules/htmx.org/dist/ext/hx-preload.min.js - re-copy it from there on a version bump). Prefetches a boosted link's GET response, reused automatically if the click follows within 5s. Needs no hx-ext or other opt-in - just being loaded activates it for every boosted anchor. */}} {{ with resources.Get "js/hx-preload.min.js" | fingerprint }} {{ end }} {{- /* Loaded on every page, not just posts: hx-boost never reloads , so a visit that starts on the home page reaches its posts with only the scripts the home page had. */}} {{ with resources.Get "js/quote-share.js" | minify | fingerprint }} {{ end }} {{- /* htmx's own browser back/forward handling (#restoreHistory) is a separate code path from a boosted click - it calls htmx.ajax() directly with swap:"outerSync" hardcoded, a full body replace, not our innerMorph below. Left alone, a link click morphed smoothly but back/forward tore the masthead down and rebuilt it. Preempt it with the same config used everywhere else (kept as one Hugo value, $swapSpec, rather than duplicated literals, so the two can't drift apart), and abort a still-in-flight restore before starting the next one - the same guard htmx's own implementation has - so mashing back/forward can't let a slow, stale response land after a faster, newer one. `defer` only affects scripts with a `src` - htmx.min.js and hx-preload.min.js above genuinely wait until after parsing, but this inline script has no `src` and runs immediately in place during parsing, defer or not. It must not touch `htmx` at its top level, only from inside a listener that fires later - hence DOMContentLoaded, which is guaranteed to fire only after every deferred script, htmx.min.js and hx-preload.min.js included, has already run. */}} {{- $swapSpec := dict "swap" "innerMorph" "transition" true }} {{- /* Safari does not implement the CSS `d` property at all - it parses `transition: d` and then ignores it (MDN's compat note: "The property parses, but has no effect"), on iPadOS and macOS alike. So the masthead signal, which should flow from one page's SHA-256 curve into the next's when innerMorph rewrites the path's `d`, snapped instantly on an iPad while morphing smoothly on Chrome and Firefox. Feature detection is no help precisely because Safari *does* parse the property, so CSS.supports("d", ...) answers yes. Rather than keep two behaviours, the morph is driven from here on every browser and style.scss no longer declares `transition: d` at all - one code path, one result everywhere. The trigger is the attribute write itself, watched with a MutationObserver, and not an htmx event. That is not a stylistic preference: hx-boost wraps the swap in a View Transition, and `htmx:after:swap` only fires once that transition has finished - about 260ms after innerMorph actually rewrote `d`. Starting there produced two visible effects per navigation, the line snapping to the new curve with the swap and then jumping back to the old one to morph. The CSS transition this replaces had no such gap because it keyed off the attribute, so the observer restores the original timing. It also covers back/forward, which reaches the path through a different code path entirely, without either needing to know about the other. Interpolating is a plain lerp of the numbers in `d`: every signal is the same M + 31 C (32 hash bytes plotted, one curve per gap) and so the same 188 numbers in the same order, which makes pairing them up positionally exact rather than an approximation. The guards below leave the value htmx wrote alone whenever that does not hold - which is also why hugo.toml turns off SVG minification, since the minifier rewrites the command sequence. Should a swap ever replace the outright rather than patch it, there is no attribute mutation to see and the curve simply snaps, the same graceful nothing Safari used to get. No DOMContentLoaded wrapper, unlike the script above: this one never touches `htmx` itself, so the observer is safe to attach during parsing. */}} {{- /* GoatCounter records only a path, and every language is its own host (fr.mykiwi.blog/ and mykiwi.blog/ would both be "/"), so the host goes in front of it: https://www.goatcounter.com/help/domains. Must be set before count.js runs. */}} {{- /* count.js only sees the first full load; hx-boost navigations after that never reload , so each one is reported by hand. The initial load fires no swap event, so nothing is counted twice. The path is passed explicitly (count.js would otherwise read the that hx-boost left behind from the first page), and an explicit path skips the callback above, so the host is added here. */}} {{- /* /sw.js (_default/home.serviceworker.js) keeps a copy of what the reader opens, so it can be opened again without a connection. Registered once the page has loaded, so installing it - which fetches the stylesheet, the scripts and every font - never competes with the page for the network on the one visit where nothing is cached yet. Not under `hugo server`: a worker answering for the stylesheet is the last thing wanted while editing it, and `make prod-server` is there to try this against the real artifact. Not under ?ogcard either (the root was marked by the boot script above): that is the build photographing its own pages, and it should photograph them as they were just built. */}} {{- if not hugo.IsServer }} {{- end }} {{ block "head" . }}{{ end }} {{- /* Boosts every same-origin link/form into an AJAX navigation, inherited from body down (htmx 4 requires the ":inherited" suffix explicitly - it no longer inherits hx-* attributes implicitly). Boosted elements default to targeting the whole ; innerMorph diffs the new body onto the old one node-by-node instead of replacing it wholesale, so unchanged nodes (the masthead's "mykiwi.blog" link) are patched in place rather than torn down and re-flashed, while changed ones - the per-page signal SVG path, the swapped #main content - update normally. transition:true wraps that diff in a native View Transition for a soft cross-fade. */}}
{{ partial "masthead.html" . }}
{{ block "main" . }}{{ end }}
{{ partial "site-foot.html" . }}