Some checks failed
secrets-guard / encrypted (push) Successful in 24s
shell-lint / shellcheck (push) Successful in 26s
Deploy frontend to LAN dev server / build (push) Successful in 2m11s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 2m20s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 2m28s
Deploy backend to LAN dev server / build (push) Successful in 2m45s
PR build (required check) / changes (pull_request) Successful in 16s
secrets-guard / encrypted (pull_request) Successful in 16s
shell-lint / shellcheck (pull_request) Successful in 15s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
Deploy frontend to LAN dev server / deploy (push) Successful in 39s
PR build (required check) / build-backend (pull_request) Successful in 1m21s
PR build (required check) / gate (pull_request) Successful in 3s
Deploy backend to LAN dev server / deploy (push) Failing after 2m31s
169 lines
No EOL
14 KiB
Cheetah
169 lines
No EOL
14 KiB
Cheetah
{{/*
|
||
Ported from templates/base.html.j2 (Jinja, trim_blocks + lstrip_blocks +
|
||
keep_trailing_newline=False). The Jinja layout used {% extends %}/{% block %};
|
||
in a single html/template parse set a per-page {{define "title"}} would
|
||
collide across the 10 pages, so the layout is instead split into sequential
|
||
chunks that every page template stitches together in order:
|
||
|
||
base_head_start doctype .. the manifest <link>. title/description/
|
||
canonical/og:* are data-driven (.PageTitle,
|
||
.PageDescription, .CanonicalPath) — the og:title /
|
||
og:url duplication Jinja did via self.title() falls
|
||
out for free.
|
||
[page head_extra] inline in the page file (home: leaflet css)
|
||
base_head_end the style.css <link>
|
||
[page jsonld] inline in the page file (city/records/glossary_term/home)
|
||
base_body_start </head> .. <body> header + nav .. <main ...>
|
||
[page content]
|
||
base_body_end </main> .. footer
|
||
[page body_scripts] base_scripts_default, or the page's own (home)
|
||
base_tail digest/ios-install scripts, </body></html>
|
||
|
||
Whitespace mirrors the Jinja env's exact output byte-for-byte (golden-diff
|
||
contract): control tags on their own line vanished entirely under
|
||
trim_blocks/lstrip_blocks, so here the equivalent actions are glued to the
|
||
start of the following content line; keep_trailing_newline=False means the
|
||
document ends at </html> with no final newline (see base_tail).
|
||
|
||
HTML comments that must SURVIVE into the output go through the `comment`
|
||
FuncMap helper, because html/template strips literal <!-- --> comments.
|
||
|
||
Data contract (all pages): .Base .AssetBase .AssetBaseURL .BaseURL
|
||
.UnitDefault .Section .BrandTag ("h1"; home passes "p") .MainClass
|
||
("content"; home passes "") .PageTitle .PageDescription .CanonicalPath
|
||
.Fmt (unit-bound formatter: Temp/TempBare/TempClass) and, on pages that have
|
||
one, .Breadcrumb []contentapi.Crumb.
|
||
*/}}
|
||
{{define "base_head_start" -}}
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
{{head_verify}}
|
||
<title>{{.PageTitle}}</title>
|
||
<meta name="description" content="{{.PageDescription}}" />
|
||
<meta name="theme-color" content="#f0803c" />
|
||
{{comment `iOS/Android home-screen install: run standalone (no browser chrome) when
|
||
added to the Home Screen. On iOS 16.4+ this is what unlocks Web Push.`}}
|
||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||
<meta name="mobile-web-app-capable" content="yes" />
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||
<meta name="apple-mobile-web-app-title" content="Thermograph" />
|
||
<link rel="canonical" href="{{.BaseURL}}{{.CanonicalPath}}" />
|
||
<meta property="og:type" content="website" />
|
||
<meta property="og:site_name" content="Thermograph" />
|
||
<meta property="og:title" content="{{.PageTitle}}" />
|
||
<meta property="og:description" content="{{.PageDescription}}" />
|
||
<meta property="og:url" content="{{.BaseURL}}{{.CanonicalPath}}" />
|
||
<meta property="og:image" content="{{.AssetBaseURL}}/logo.png?v=3" />
|
||
<meta property="og:image:width" content="512" />
|
||
<meta property="og:image:height" content="512" />
|
||
<meta property="og:image:alt" content="Thermograph logo" />
|
||
<meta name="twitter:card" content="summary" />
|
||
<link rel="icon" href="{{.AssetBase}}/favicon.svg?v=3" type="image/svg+xml" />
|
||
<link rel="icon" href="{{.AssetBase}}/favicon-48.png?v=3" type="image/png" sizes="48x48" />
|
||
<link rel="icon" href="{{.AssetBase}}/favicon-32.png?v=3" type="image/png" sizes="32x32" />
|
||
<link rel="icon" href="{{.AssetBase}}/favicon-16.png?v=3" type="image/png" sizes="16x16" />
|
||
<link rel="apple-touch-icon" href="{{.AssetBase}}/apple-touch-icon.png?v=3" />
|
||
<link rel="manifest" href="{{.AssetBase}}/manifest.webmanifest" />
|
||
{{end}}
|
||
{{define "base_head_end" -}}
|
||
<link rel="stylesheet" href="{{.AssetBase}}/style.css" />
|
||
{{end}}
|
||
{{/* The brand lockup (logo svg + wordmark) shared by both brand-tag variants
|
||
below. */}}
|
||
{{define "brand_link"}}<a href="{{.Base}}/"><span class="logo"><svg viewBox="0 0 512 512" width="28" height="28" aria-hidden="true"><rect x="32" y="32" width="448" height="448" rx="96" fill="#10141B" stroke="#FFFFFF" stroke-opacity="0.08" stroke-width="4"/><rect x="115" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="163" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="211" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="259" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="307" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="355" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="403" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="163" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="211" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="259" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="307" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="355" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="403" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="67" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="163" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="211" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="259" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="307" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="355" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="403" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="67" y="259" width="42" height="42" rx="10" fill="#131C1F"/><rect x="211" y="259" width="42" height="42" rx="10" fill="#131C1F"/><rect x="403" y="259" width="42" height="42" rx="10" fill="#131C1F"/><rect x="67" y="211" width="42" height="42" rx="10" fill="#1E1E22"/><rect x="115" y="211" width="42" height="42" rx="10" fill="#1E1E22"/><rect x="211" y="211" width="42" height="42" rx="10" fill="#1E1E22"/><rect x="307" y="211" width="42" height="42" rx="10" fill="#1E1E22"/><rect x="67" y="163" width="42" height="42" rx="10" fill="#1D1C1E"/><rect x="115" y="163" width="42" height="42" rx="10" fill="#1D1C1E"/><rect x="307" y="163" width="42" height="42" rx="10" fill="#1D1C1E"/><rect x="355" y="163" width="42" height="42" rx="10" fill="#1D1C1E"/><rect x="67" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="115" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="163" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="211" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="259" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="307" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="355" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="67" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="115" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="163" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="211" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="259" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="307" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="355" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="67" y="403" width="42" height="42" rx="10" fill="#2166ac"/><rect x="67" y="355" width="42" height="42" rx="10" fill="#4393c3"/><rect x="115" y="355" width="42" height="42" rx="10" fill="#4393c3"/><rect x="115" y="307" width="42" height="42" rx="10" fill="#92c5de"/><rect x="115" y="259" width="42" height="42" rx="10" fill="#4a9d5b"/><rect x="163" y="259" width="42" height="42" rx="10" fill="#4a9d5b"/><rect x="163" y="211" width="42" height="42" rx="10" fill="#f6c18a"/><rect x="163" y="163" width="42" height="42" rx="10" fill="#ef9351"/><rect x="211" y="163" width="42" height="42" rx="10" fill="#ef9351"/><rect x="259" y="163" width="42" height="42" rx="10" fill="#ef9351"/><rect x="259" y="211" width="42" height="42" rx="10" fill="#f6c18a"/><rect x="259" y="259" width="42" height="42" rx="10" fill="#4a9d5b"/><rect x="307" y="259" width="42" height="42" rx="10" fill="#4a9d5b"/><rect x="355" y="259" width="42" height="42" rx="10" fill="#4a9d5b"/><rect x="355" y="211" width="42" height="42" rx="10" fill="#f6c18a"/><rect x="403" y="211" width="42" height="42" rx="10" fill="#f6c18a"/><rect x="403" y="163" width="42" height="42" rx="10" fill="#ef9351"/><rect x="403" y="115" width="42" height="42" rx="10" fill="#dd6b52"/><rect x="403" y="67" width="42" height="42" rx="10" fill="#8f0e20"/></svg></span>Thermograph</a>{{end}}
|
||
{{define "base_body_start" -}}
|
||
</head>
|
||
<body{{if .UnitDefault}} data-unit-default="{{.UnitDefault}}"{{end}}>
|
||
<header>
|
||
<div class="brand">
|
||
<div>{{/* The brand is the page's h1 everywhere EXCEPT the homepage, where the
|
||
hero headline owns the sole h1 and the brand degrades to a <p>. The
|
||
.brand h1, .brand .site-name selector pair in style.css keeps the
|
||
lockup looking identical either way. (The Jinja original wrote
|
||
<{{ brand_tag|default('h1') }} …>; html/template forbids actions in
|
||
tag names, hence two literal branches around one shared lockup.) */}}
|
||
{{if eq .BrandTag "p"}}<p class="site-name">{{template "brand_link" .}}</p>{{else}}<h1 class="site-name">{{template "brand_link" .}}</h1>{{end}}
|
||
<p class="tag">How unusual is the weather? Graded against ~45 years of local climate history.</p>
|
||
</div>
|
||
<details class="nav-menu">
|
||
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
||
<div class="nav-panel">{{/* data-view marks the links nav.js rewrites to carry the current
|
||
location hash, so moving between views keeps the place you picked.
|
||
Climate and Alerts deliberately opt out (they aren't cell-scoped).
|
||
Inert on the SEO pages, which never load nav.js. */}}
|
||
<nav class="view-nav" aria-label="Views">
|
||
<a href="{{.Base}}/" data-view="map"{{if eq .Section "home"}} class="active"{{end}}>Weekly</a>
|
||
<a href="{{.AssetBase}}/calendar" data-view="calendar">Calendar</a>
|
||
<a href="{{.AssetBase}}/day" data-view="day">Day Detail</a>
|
||
<a href="{{.AssetBase}}/compare" data-view="compare">Compare</a>
|
||
<a href="{{.AssetBase}}/score" data-view="score"{{if eq .Section "score"}} class="active"{{end}}>Score</a>
|
||
<a href="{{.Base}}/climate"{{if eq .Section "climate"}} class="active"{{end}}>Climate</a>
|
||
<a href="{{.AssetBase}}/alerts">Alerts</a>
|
||
</nav>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</header>
|
||
|
||
<main{{if .MainClass}} class="{{.MainClass}}"{{end}}>
|
||
{{end}}
|
||
{{define "base_body_end" -}}
|
||
</main>
|
||
|
||
<footer class="site-footer">{{/* The compact digest signup is the site-wide footer pattern, not a
|
||
homepage-only surface: every page is a possible entry point.
|
||
Hidden for now; restore by uncommenting the form block below
|
||
(kept verbatim from the Jinja original — {{ asset_base }} becomes
|
||
{{.AssetBase}} when it comes back):
|
||
|
||
<form class="digest-form digest-compact" method="post" action="{{ asset_base }}/digest"
|
||
data-digest aria-labelledby="footer-digest-label">
|
||
<label id="footer-digest-label" for="footer-digest-email">Your city's weather, graded — monthly</label>
|
||
<div class="digest-row">
|
||
<input id="footer-digest-email" name="email" type="email" required
|
||
autocomplete="email" placeholder="you@example.com" />
|
||
<button type="submit">Get the digest</button>
|
||
</div>
|
||
<p class="digest-note muted">No spam, no sharing your address, unsubscribe anytime.</p>
|
||
<p class="digest-status" role="status" aria-live="polite" hidden></p>
|
||
</form>
|
||
*/}}
|
||
<nav aria-label="Footer">
|
||
<a href="{{.Base}}/climate">City climates</a>
|
||
<a href="{{.Base}}/glossary">Weather glossary</a>
|
||
<a href="{{.Base}}/about">About & methodology</a>
|
||
<a href="{{.Base}}/privacy">Privacy</a>
|
||
<a href="{{.Base}}/">Open the tool</a>
|
||
</nav>
|
||
<p class="muted">Free. No ads. No tracking. No account needed. Built by one person in the open.</p>
|
||
<p class="muted">Thermograph grades weather by its percentile against ~45 years of local
|
||
climate history. Data: ERA5 via Open-Meteo · NASA POWER · MET Norway.</p>
|
||
<p class="muted">Made by <a href="https://emigriffith.dev">emigriffith.dev</a>
|
||
<a href="mailto:emerytgriffith@gmail.com" aria-label="Email" title="emerytgriffith@gmail.com">@</a></p>
|
||
</footer>
|
||
{{end}}
|
||
{{define "base_scripts_default" -}}
|
||
{{comment `Header parity with the interactive pages: the °F/°C toggle, account + bell,
|
||
and the client-side temperature converter. All self-contained modules.`}}
|
||
<script type="module" src="{{.AssetBase}}/units.js"></script>
|
||
<script type="module" src="{{.AssetBase}}/account.js"></script>
|
||
<script type="module" src="{{.AssetBase}}/climate.js"></script>
|
||
{{end}}
|
||
{{define "base_tail" -}}
|
||
<script type="module" src="{{.AssetBase}}/digest.js"></script>
|
||
<script type="module" src="{{.AssetBase}}/ios-install.js"></script>
|
||
</body>
|
||
</html>{{end}}
|
||
{{/* Shared breadcrumb nav (each Jinja page repeated this line verbatim; the
|
||
output is identical, so it is factored here). The separator is emitted
|
||
BEFORE every non-first crumb — same byte stream as Jinja's
|
||
`{% if not loop.last %}` after-each-but-last. Crumb.Href is *string
|
||
(terminal crumb: null). */}}
|
||
{{define "breadcrumb" -}}
|
||
<nav class="breadcrumb" aria-label="Breadcrumb">
|
||
{{range $i, $c := .Breadcrumb}}{{if $i}} <span class="sep">›</span> {{end}}{{if $c.Href}}<a href="{{$c.Href}}">{{$c.Name}}</a>{{else}}<span>{{$c.Name}}</span>{{end}}{{end -}}
|
||
</nav>
|
||
{{end}} |