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
1package main
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "io/fs"
7 "sync"
8)
9
10// Cache busting for the hand written assets.
11//
12// The Vite sites here get content hashed filenames, so `immutable` on their
13// static handler is safe. This site writes app.css and app.js by hand at fixed
14// paths, and `immutable` on a fixed path means Cloudflare holds the old file for
15// a year and a stylesheet change is invisible at the edge while being correct in
16// the container. A hash of the bytes in the query string gives the same
17// guarantee without a build step, since Cloudflare keys its cache on the whole
18// url.
19
20var (
21 assetOnce sync.Once
22 assetVers map[string]string
23)
24
25// assetURL is the template function. A missing file falls through to the bare
26// path rather than failing the render, since a broken stylesheet link is easier
27// to read than a blank page.
28func assetURL(path string) string {
29 assetOnce.Do(loadAssetVersions)
30 if v, ok := assetVers[path]; ok {
31 return "/" + path + "?v=" + v
32 }
33 return "/" + path
34}
35
36func loadAssetVersions() {
37 assetVers = map[string]string{}
38 _ = fs.WalkDir(assets(), "static", func(p string, d fs.DirEntry, err error) error {
39 if err != nil || d.IsDir() {
40 return nil
41 }
42 b, err := fs.ReadFile(assets(), p)
43 if err != nil {
44 return nil
45 }
46 sum := sha256.Sum256(b)
47 assetVers[p] = hex.EncodeToString(sum[:])[:10]
48 return nil
49 })
50}
51
52// resetAssetVersions is what the development reload path calls, so editing CSS
53// off disk still changes the url rather than serving the version read at boot.
54func resetAssetVersions() {
55 assetOnce = sync.Once{}
56}