repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

8.8 KB · 190 lines · HTML Raw History
  1{{/* Default blocks. Go requires every {{template}} target to exist, so the
  2     optional ones are defined empty here and overridden by whichever page
  3     wants them: layouts are parsed before the page, and a later definition of
  4     the same name wins. This is the Go equivalent of Jinja's {% block %}
  5     defaults. */}}
  6
  7{{ define "extra_head" }}{{ end }}
  8{{ define "extra_css" }}{{ end }}
  9{{ define "extra_js" }}{{ end }}
 10{{ define "breadcrumbs" }}{{ end }}
 11
 12{{/* The bar itself is a block, not just its contents, so the home page can
 13     drop it entirely. Rendering an empty bar would leave a stray rule under
 14     the navbar with nothing in it. */}}
 15{{ define "breadcrumb_bar" }}
 16<div class="breadcrumb-bar py-2 overflow-auto d-print-none">
 17  <div class="container">
 18    {{ template "breadcrumbs" . }}
 19  </div>
 20</div>
 21{{ end }}
 22
 23{{ define "social" }}
 24<meta property="og:type" content="website">
 25<meta property="og:site_name" content="{{ .SiteName }}">
 26<meta property="og:title" content="{{ .Title }}">
 27<meta property="og:description" content="{{ .Description }}">
 28<meta property="og:url" content="{{ .Canonical }}">
 29{{/* A PNG, not SVG: every social platform refuses image/svg+xml here, so a
 30     vector card is a card nobody sees. Compiled from typst/card.typ at build
 31     time and dropped into dist, which is why it needs no route of its own. */}}
 32<meta property="og:image" content="{{ .OGImage }}">
 33<meta property="og:image:width" content="1200">
 34<meta property="og:image:height" content="630">
 35{{/* summary_large_image, not summary: the card is 1200x630, and the small
 36     variant crops it to a square thumbnail. */}}
 37<meta name="twitter:card" content="summary_large_image">
 38<meta name="twitter:title" content="{{ .Title }}">
 39<meta name="twitter:description" content="{{ .Description }}">
 40<meta name="twitter:image" content="{{ .OGImage }}">
 41{{ end }}
 42
 43{{/* Self-tracking. CollectorID is empty on staging, which renders nothing at
 44     all rather than filing phantom sessions into the property the production
 45     dashboard reads. */}}
 46{{ define "collector" }}
 47{{ if .CollectorID }}
 48<script>
 49  (function(m,e,t,r,i,c,s){m.collectorQueue = m.collectorQueue || r;
 50  m.collectorServer = c; m.collectorId = s; var script = e.createElement(t);
 51  script.src = c + i; e.head.appendChild(script);
 52  })(window,document,'script',[],'/static/collector.js',
 53  '{{ .CollectorServer }}','{{ .CollectorID }}');
 54</script>
 55{{ end }}
 56{{ end }}
 57
 58{{ define "logo" }}
 59<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
 60  <rect x="6"  y="38" width="10" height="22" rx="1.5" fill="#6b9e78"/>
 61  <rect x="20" y="28" width="10" height="32" rx="1.5" fill="#6b9e78"/>
 62  <rect x="34" y="18" width="10" height="42" rx="1.5" fill="#6b9e78"/>
 63  <rect x="48" y="8"  width="10" height="52" rx="1.5" fill="#6b9e78"/>
 64  <rect x="48" y="8"  width="10" height="6"  rx="1.5" fill="#c9a84c"/>
 65</svg>
 66{{ end }}
 67
 68{{ define "nav" }}
 69<nav class="navbar navbar-dark navbar-expand-md d-print-none">
 70  <div class="container">
 71    <a class="navbar-brand" href="/">
 72      <span class="logo" aria-hidden="true">{{ template "logo" . }}</span>
 73      <span class="brand-text">{{ .SiteName }}</span>
 74      <span class="brand-dot" aria-hidden="true"></span>
 75    </a>
 76    <button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navMain" aria-controls="navMain" aria-expanded="false" aria-label="Toggle navigation">
 77      <span class="navbar-toggler-icon"></span>
 78    </button>
 79    <div class="collapse navbar-collapse" id="navMain">
 80      <ul class="navbar-nav ms-3">
 81        {{ if .Authenticated }}
 82        <li class="nav-item"><a class="nav-link {{ if eq .Path "/properties" }}active{{ end }}" href="/properties">Properties</a></li>
 83        <li class="nav-item"><a class="nav-link {{ if eq .Path "/documentation" }}active{{ end }}" href="/documentation">Docs</a></li>
 84        {{ else }}
 85        <li class="nav-item"><a class="nav-link {{ if eq .Path "/" }}active{{ end }}" href="/">Home</a></li>
 86        <li class="nav-item"><a class="nav-link {{ if eq .Path "/documentation" }}active{{ end }}" href="/documentation">Docs</a></li>
 87        {{ end }}
 88      </ul>
 89      <ul class="navbar-nav ms-auto">
 90        {{ if .Authenticated }}
 91        <li class="nav-item d-flex align-items-center">
 92          {{/* Signing out ends the session for every site at once, which is the
 93               only sensible meaning of it when one cookie covers all of them. */}}
 94          <a href="https://auth.bythewood.me/" class="btn btn-sm btn-outline-light">Account</a>
 95        </li>
 96        {{ else }}
 97        <li class="nav-item d-flex align-items-center">
 98          <a href="/login" class="btn btn-sm btn-outline-light">Sign in</a>
 99        </li>
100        {{ end }}
101      </ul>
102    </div>
103  </div>
104</nav>
105{{ end }}
106
107{{ define "footer" }}
108<footer class="d-print-none">
109  <div class="container">
110    <div class="row links">
111      <div class="col-12 col-lg-6 mb-4 mb-lg-0">
112        <div class="h5 mb-3">// {{ .SiteName }}</div>
113        <p>Self-hosted web analytics by <a href="https://isaacbythewood.com/">{{ .AuthorName }}</a>.
114          Page views, sessions, and custom events, with GeoIP maps, UTM
115          attribution, and PDF reports, all through one endpoint you run.</p>
116      </div>
117      <div class="col-6 col-lg-2">
118        <div class="h5 mb-3">// Elsewhere</div>
119        <ul class="list-unstyled">
120          <li class="mb-2"><a href="https://isaacbythewood.com/" class="link-footer">Portfolio</a></li>
121          <li class="mb-2"><a href="https://blog.bythewood.me/" class="link-footer">Blog</a></li>
122          <li class="mb-2"><a href="https://github.com/overshard" class="link-footer" target="_blank" rel="noopener">GitHub</a></li>
123        </ul>
124      </div>
125      <div class="col-6 col-lg-2">
126        <div class="h5 mb-3">// Pages</div>
127        <ul class="list-unstyled">
128          <li class="mb-2"><a href="/" class="link-footer">Home</a></li>
129          <li class="mb-2"><a href="/documentation" class="link-footer">Documentation</a></li>
130          <li class="mb-2"><a href="{{ .SourceURL }}" class="link-footer" target="_blank" rel="noopener">Source</a></li>
131        </ul>
132      </div>
133      <div class="col-6 col-lg-2">
134        <div class="h5 mb-3">// Operator</div>
135        <ul class="list-unstyled">
136          {{ if .Authenticated }}
137          <li class="mb-2"><a href="/properties" class="link-footer">Properties</a></li>
138          <li class="mb-2">
139            <a href="https://auth.bythewood.me/" class="link-footer">Account</a>
140          </li>
141          {{ else }}
142          <li class="mb-2"><a href="/login" class="link-footer">Sign in</a></li>
143          {{ end }}
144        </ul>
145      </div>
146    </div>
147  </div>
148</footer>
149
150<div class="footer-bar d-print-none">
151  <div class="container">
152    <div class="row align-items-center">
153      <div class="col-sm-6 d-flex align-items-center justify-content-center justify-content-sm-start order-1 order-sm-0">
154        <small>&copy; {{ .Year }} {{ .AuthorName }} · Some rights reserved · <a href="https://db-ip.com" class="link-footer" target="_blank" rel="noopener">IP Geolocation by DB-IP</a></small>
155      </div>
156      <div class="col-sm-6 d-flex justify-content-center justify-content-sm-end py-3 py-sm-0">
157        <a href="{{ .SourceURL }}" target="_blank" rel="noopener" class="footer-bar-link" aria-label="GitHub">
158          <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
159            <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
160          </svg>
161        </a>
162      </div>
163    </div>
164  </div>
165</div>
166{{ end }}
167
168{{/* One rank list panel. Takes a title, a column heading, the items, and
169     optionally a property id to make each label a filter link. */}}
170{{ define "rankList" }}
171{{ if .items }}
172<div class="col-12 col-sm-6 col-lg-4">
173  <div class="rank-list">
174    <div class="rank-list-header"><span class="rank-list-title">{{ .title }}</span><span class="rank-list-col">{{ .col }}</span></div>
175    <div class="rank-list-body">
176      {{ $pid := .filterFor }}
177      {{ range .items }}
178      <div class="rank-list-row">
179        <span class="rank-label">
180          {{ if $pid }}<a href="/{{ $pid }}?filter_url={{ .Label }}">{{ .Label }}</a>{{ else }}{{ .Label }}{{ end }}
181        </span>
182        <span class="rank-count">{{ .Count }}</span>
183      </div>
184      {{ end }}
185    </div>
186  </div>
187</div>
188{{ end }}
189{{ end }}