thermograph/frontend/server/internal/render/templates/base.html.tmpl

169 lines
14 KiB
Cheetah
Raw Normal View History

{{/*
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=
{{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 &amp; 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 &middot; NASA POWER &middot; 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}}