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// Package web holds the HTTP pieces every site in this repo needs: resolving
2// Vite's content-hashed filenames, request logging, security headers, and a
3// server that shuts down cleanly.
4package web
5
6import (
7 "encoding/json"
8 "fmt"
9 "io/fs"
10 "sync"
11)
12
13// Assets resolves a Vite entry name to the content-hashed files Vite emitted,
14// by reading dist/.vite/manifest.json.
15type Assets struct {
16 entries map[string]manifestEntry
17 once sync.Once
18}
19
20type manifestEntry struct {
21 File string `json:"file"`
22 Src string `json:"src"`
23 IsEntry bool `json:"isEntry"`
24 CSS []string `json:"css"`
25 Assets []string `json:"assets"`
26}
27
28// LoadAssets reads the manifest out of a dist filesystem. A missing manifest is
29// an error rather than a warning, because serving a page whose script tag points
30// at a file that was never built is worse than refusing to start.
31func LoadAssets(dist fs.FS) (*Assets, error) {
32 raw, err := fs.ReadFile(dist, ".vite/manifest.json")
33 if err != nil {
34 return nil, fmt.Errorf("read vite manifest: %w (did you run `make build` in frontend/?)", err)
35 }
36
37 var entries map[string]manifestEntry
38 if err := json.Unmarshal(raw, &entries); err != nil {
39 return nil, fmt.Errorf("parse vite manifest: %w", err)
40 }
41
42 return &Assets{entries: entries}, nil
43}
44
45// Script returns the public URL of the JS bundle for a Vite entry, e.g.
46// Script("index.js") -> "/static/base-aDClsiBF.js".
47func (a *Assets) Script(entry string) string {
48 e, ok := a.entries[entry]
49 if !ok {
50 return ""
51 }
52 return "/static/" + e.File
53}
54
55// Hashed reports every file Vite emitted with a content hash in its name, which
56// is the set safe to cache for a year. Files copied through from publicDir keep
57// their original names and stay out of it, or an updated resume would never
58// reach anybody.
59func (a *Assets) Hashed() map[string]bool {
60 hashed := make(map[string]bool, len(a.entries)*2)
61 for _, e := range a.entries {
62 if e.File != "" {
63 hashed[e.File] = true
64 }
65 for _, css := range e.CSS {
66 hashed[css] = true
67 }
68 for _, asset := range e.Assets {
69 hashed[asset] = true
70 }
71 }
72 return hashed
73}
74
75// Styles returns the public URLs of every stylesheet Vite extracted from an
76// entry. Plural because a code-split entry can pull in more than one.
77func (a *Assets) Styles(entry string) []string {
78 e, ok := a.entries[entry]
79 if !ok {
80 return nil
81 }
82 out := make([]string, 0, len(e.CSS))
83 for _, css := range e.CSS {
84 out = append(out, "/static/"+css)
85 }
86 return out
87}