1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
<!doctype html>
<html lang="{{ site.Language.Lang }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#f5f2ec" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#15171a" media="(prefers-color-scheme: dark)">
<title>{{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} · {{ site.Title }}{{ end }}</title>
{{- /*
Priority: this page's own front matter > an explicit site-wide
params.description override > params.profile.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 site.Params.profile.bio }}{{ $desc = . | markdownify | plainify }}{{ end }}{{ end }}
{{ with $desc -}}
<meta name="description" content="{{ . }}">
{{- end }}
<!-- Fonts are self-hosted, so there is no third-party DNS + TLS before first paint. -->
<link rel="preload" href="/fonts/Newsreader-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/JetBrainsMono-Regular.woff2" as="font" type="font/woff2" crossorigin>
{{ with resources.Get "css/style.scss" }}
{{ $opts := dict "transpiler" "dartsass" "outputStyle" "compressed" }}
{{ with . | toCSS $opts }}
<link rel="stylesheet" type="text/css" href="{{ .RelPermalink }}">
{{ end }}
{{ end }}
<link rel="alternate" type="application/rss+xml" title="{{ site.Title }}" href="{{ "index.xml" | absURL }}">
{{- /*
Declarative, not htmx.config.preload = {...} in a script: a <meta> 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.
*/}}
<meta name="htmx-config" content="preload.boostEvent:mouseover">
<script src="{{ "js/htmx.min.js" | relURL }}" defer></script>
{{- /*
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.
*/}}
<script src="{{ "js/hx-preload.min.js" | relURL }}" defer></script>
{{- /*
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 }}
<script>
document.addEventListener("DOMContentLoaded", () => {
let restoreAbort;
document.addEventListener("htmx:before:history:restore", (e) => {
e.preventDefault();
restoreAbort?.abort();
restoreAbort = new AbortController();
htmx.ajax("GET", e.detail.path, {
target: document.body,
swap: "{{ $swapSpec.swap }}",
transition: {{ $swapSpec.transition }},
request: {headers: {}, signal: restoreAbort.signal},
});
});
});
</script>
{{ block "head" . }}{{ end }}
</head>
{{- /*
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 <body>; 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.
*/}}
<body hx-boost:inherited="swap:{{ $swapSpec.swap }} transition:{{ $swapSpec.transition }}">
<a class="skip-link" href="#main">Skip to content</a>
<div class="wrap">
{{ partial "masthead.html" . }}
<main id="main">{{ block "main" . }}{{ end }}</main>
{{ partial "site-foot.html" . }}
</div>
</body>
</html>
|