orchard
mirrorEvery 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
1{{define "panel-open"}}
2<section class="panel" data-panel="{{.Slug}}">
3 <div class="bracket tl"></div><div class="bracket tr"></div>
4 <div class="bracket bl"></div><div class="bracket br"></div>
5 <header class="panel-head">
6 <span class="idx">{{.Index}}</span>
7 <h2>{{.Title}}</h2>
8 <span class="rule"></span>
9 {{if .Meta}}<span class="panel-meta">{{.Meta}}</span>{{end}}
10 </header>
11{{end}}
12
13{{define "card"}}
14<article class="card" data-dir="{{.Direction}}" data-key="{{.Key}}">
15 <header>
16 <h3>{{.Label}}</h3>
17 <span class="sym">{{if .Note}}{{.Note}}{{else}}{{.Symbol}}{{end}}</span>
18 </header>
19 {{if .Unavailable}}
20 <p class="price dim">NO SIGNAL</p>
21 <p class="move"><span class="pts">—</span></p>
22 {{else}}
23 <p class="price" data-price>{{.Price}}</p>
24 <p class="move">
25 <span class="glyph"></span>
26 <span class="pts">{{.Change}}</span>
27 <span class="pct">{{.Percent}}</span>
28 </p>
29 {{end}}
30 {{template "spark" .Spark}}
31</article>
32{{end}}
33
34{{/*
35 The baseline runs the full width and the line only as far as the session has
36 got, so the empty right hand side is the rest of the day rather than missing
37 data. preserveAspectRatio is none, so a circle would come out an ellipse and
38 the cursor at the live end has to be a vertical rule.
39*/}}
40{{define "spark"}}
41<svg class="spark" viewBox="0 0 100 32" preserveAspectRatio="none" aria-hidden="true" focusable="false">
42 {{if .Line}}
43 <path class="spark-area" d="{{path .Area}}"></path>
44 {{if .HasBase}}<line class="spark-base" x1="0" x2="100" y1="{{.Baseline}}" y2="{{.Baseline}}"></line>{{end}}
45 <path class="spark-line" d="{{path .Line}}"></path>
46 {{if .Closed}}<line class="spark-shut" x1="{{.Span}}" x2="{{.Span}}" y1="0" y2="32"></line>
47 {{else if .Partial}}<line class="spark-now" x1="{{.Span}}" x2="{{.Span}}" y1="0" y2="32"></line>{{end}}
48 {{end}}
49</svg>
50{{end}}
51
52{{define "stories"}}
53<ol class="stories">
54 {{range $i, $s := .}}
55 <li>
56 <span class="rank">{{printf "%02d" (inc $i)}}</span>
57 <div class="body">
58 <a class="title" href="{{$s.URL}}" rel="noopener noreferrer" target="_blank">{{$s.Title}}</a>
59 <div class="meta">
60 {{if $s.Host}}<span class="host">{{$s.Host}}</span>{{end}}
61 <span class="pts">{{$s.Points}}<span class="unit">PTS</span></span>
62 <a class="cmt" href="{{$s.Comments}}" rel="noopener noreferrer" target="_blank">{{$s.Count}}<span class="unit">CMT</span></a>
63 {{if $s.Age}}<span class="age">{{$s.Age}}</span>{{end}}
64 </div>
65 </div>
66 </li>
67 {{else}}
68 <li class="empty"><span class="rank">--</span><div class="body">AWAITING FEED</div></li>
69 {{end}}
70</ol>
71{{end}}
72
73{{define "headline"}}
74<li>
75 <span class="tag">{{.Source}}</span>
76 <a href="{{.URL}}" rel="noopener noreferrer" target="_blank">{{.Title}}</a>
77 <span class="age">{{.Age}}</span>
78</li>
79{{end}}
80
81{{define "condition"}}
82<li data-state="{{.State}}">
83 <span class="k">{{.Label}}</span>
84 <span class="v">{{.Value}}</span>
85 <span class="n">{{.Note}}</span>
86 <span class="meter">
87 <i class="fill" style="width:{{.Fill}}%"></i>
88 {{range .Ticks}}<i class="tick" style="left:{{.}}%"></i>{{end}}
89 </span>
90</li>
91{{end}}
92
93{{define "earning-reported"}}
94<li class="done" data-dir="{{.Dir}}">
95 <span class="tkr">{{.Symbol}}</span>
96 <span class="co">{{.Name}}</span>
97 <span class="move">{{.Move}}</span>
98 <span class="detail">
99 <b data-verdict="{{.Verdict}}">{{.Verdict}}</b>
100 <span class="figs">{{.Actual}} v {{.Forecast}}</span>
101 <span class="on">{{.Day}}</span>
102 {{if .Note}}<span class="note">{{.Note}}</span>{{end}}
103 </span>
104</li>
105{{end}}
106
107{{define "earning-upcoming"}}
108<li class="next">
109 <span class="tkr">{{.Symbol}}</span>
110 <span class="co">{{.Name}}</span>
111 <span class="day">{{.Day}}{{if .When}} <span class="when">{{.When}}</span>{{end}}</span>
112 <span class="detail">
113 <span class="figs">EST {{.Est}}</span>
114 {{if .Ests}}<span class="on">{{.Ests}} ESTS</span>{{end}}
115 </span>
116</li>
117{{end}}
118
119{{define "outlook"}}
120<li data-verdict="{{.Verdict}}">
121 <div class="head">
122 <span class="d">{{.Day}} {{.Date}}</span>
123 <span class="temps">{{.High}}°<i>/</i>{{.Low}}°</span>
124 <span class="v">{{.Verdict}}</span>
125 </div>
126 <div class="factors">
127 {{range .Factors}}
128 <div class="factor" data-score="{{.Score}}">
129 <span class="fk">{{.Label}}</span>
130 <span class="fbar"></span>
131 <span class="fv">{{.Value}}</span>
132 <span class="fd">{{.Detail}}</span>
133 </div>
134 {{end}}
135 </div>
136</li>
137{{end}}
138
139{{/*
140 The bar is the IMDb score and the tomatometer averaged onto one scale, since
141 the two numbers together are what anyone reads and a bar is quicker to compare
142 down a column than a pair of figures is. Both figures stay underneath it.
143*/}}
144{{define "title"}}
145<li>
146 {{if .URL}}<a class="name" href="{{.URL}}" rel="noopener noreferrer" target="_blank">{{.Name}}</a>{{else}}<span class="name">{{.Name}}</span>{{end}}
147 <span class="svc">{{.Provider}}</span>
148 <span class="rating" data-band="{{.ScoreBand}}">
149 <span class="pct">{{.Score}}%</span>
150 <span class="track"><i style="width:{{.Score}}%"></i></span>
151 <span class="n">{{.ScoreFrom}}</span>
152 </span>
153 <span class="meta">
154 <span class="kind">{{.Kind}}{{if .Year}} {{.Year}}{{end}}</span>
155 <span class="score imdb" data-grade="{{.IMDbState}}">IMDB {{.IMDb}}</span>
156 {{if .Tomato}}<span class="score rt" data-grade="{{.TomatoState}}">RT {{.Tomato}}</span>{{end}}
157 </span>
158</li>
159{{end}}
160
161{{/*
162 The review line is Valve's own verdict and the count behind it, since a
163 percentage on its own says nothing about whether nine people or four hundred
164 thousand produced it.
165*/}}
166{{define "game"}}
167<li>
168 <a href="{{.URL}}" rel="noopener noreferrer" target="_blank">{{.Name}}</a>
169 <span class="pr">{{if gt .Discount 0}}<span class="disc">-{{.Discount}}%</span> {{end}}{{.Price}}</span>
170 {{if .Reviewed}}
171 <span class="rating" data-band="{{band .Rating}}" title="{{.Verdict}}">
172 <span class="pct">{{.Rating}}%</span>
173 <span class="track"><i style="width:{{.Rating}}%"></i></span>
174 <span class="n">{{.Reviews}}</span>
175 </span>
176 {{end}}
177 <span class="meta">
178 {{range .Tags}}<span class="tag">{{.}}</span>{{end}}
179 {{if .Players}}<span class="players">{{.Players}} PLAYING</span>{{end}}
180 </span>
181</li>
182{{end}}
183
184{{/*
185 The figures under one hour of the plot. The bar itself lives in the plot above
186 so that all eight can share a floor and a half way rule, which is what makes a
187 tall one readable rather than only comparable to its neighbours.
188*/}}
189{{define "hour"}}
190<div class="hour" data-warm="{{.Warm}}" data-wet="{{if ge .Rain 50}}yes{{else}}no{{end}}">
191 <span class="hp">{{.Rain}}%</span>
192 <span class="hv">{{.Temp}}°</span>
193 <span class="ht">{{.Label}}</span>
194</div>
195{{end}}
196
197{{define "gauge"}}
198<div class="gauge" data-level="{{.Level}}">
199 <span class="gk">{{.Key}}</span>
200 {{if .Known}}
201 <span class="gv">{{.Value}}</span>
202 <span class="gbar"><i style="width:{{.Fill}}%"></i></span>
203 <span class="gb">{{.Band}}</span>
204 {{if .Note}}<span class="gn">{{.Note}}</span>{{end}}
205 {{else}}
206 <span class="gv dim">—</span>
207 <span class="gbar"></span>
208 <span class="gb">NO SIGNAL</span>
209 {{end}}
210</div>
211{{end}}