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

2.7 KB · 98 lines · Go Raw History
 1package main
 2
 3import (
 4	"io/fs"
 5	"net/http"
 6	"path"
 7	"sort"
 8	"strconv"
 9	"strings"
10)
11
12// The site ran on Next.js until 2026 and its image optimiser is still being
13// asked for pictures that are still here, under a different extension and with
14// the width baked into the name instead of the query string. The old URL was
15// /_next/image?url=/static/images/art/acrylic-pours/000.webp&w=960, and that
16// picture is now /static/images/art/acrylic-pours/000-960.avif.
17
18// nextImageIndex holds the widths available for each image base path.
19type nextImageIndex struct {
20	widths map[string][]int
21	plain  map[string]string
22}
23
24func newNextImageIndex(dist fs.FS) *nextImageIndex {
25	idx := &nextImageIndex{widths: map[string][]int{}, plain: map[string]string{}}
26
27	_ = fs.WalkDir(dist, "images", func(p string, d fs.DirEntry, err error) error {
28		if err != nil || d.IsDir() {
29			return nil
30		}
31		ext := path.Ext(p)
32		stem := strings.TrimSuffix(p, ext)
33		if base, w, ok := splitWidth(stem); ok {
34			idx.widths[base] = append(idx.widths[base], w)
35			return nil
36		}
37		idx.plain[stem] = p
38		return nil
39	})
40	for k := range idx.widths {
41		sort.Ints(idx.widths[k])
42	}
43	return idx
44}
45
46// splitWidth pulls the trailing -<width> off a built name. A stem that ends in
47// a number for its own reasons keeps it, since the part before the dash has to
48// survive as a name.
49func splitWidth(stem string) (base string, width int, ok bool) {
50	i := strings.LastIndex(stem, "-")
51	if i <= 0 {
52		return "", 0, false
53	}
54	w, err := strconv.Atoi(stem[i+1:])
55	if err != nil || w <= 0 {
56		return "", 0, false
57	}
58	return stem[:i], w, true
59}
60
61// resolve maps an old optimiser request onto a file that exists now. It takes
62// the smallest width at least as large as the one asked for, so the picture is
63// never upscaled, and the largest available when the request wants more than
64// anything here has.
65func (n *nextImageIndex) resolve(rawURL string, want int) (string, bool) {
66	rest, ok := strings.CutPrefix(path.Clean(rawURL), "/static/")
67	if !ok {
68		return "", false
69	}
70	stem := strings.TrimSuffix(rest, path.Ext(rest))
71
72	if widths := n.widths[stem]; len(widths) > 0 {
73		pick := widths[len(widths)-1]
74		for _, w := range widths {
75			if w >= want {
76				pick = w
77				break
78			}
79		}
80		return "/static/" + stem + "-" + strconv.Itoa(pick) + ".avif", true
81	}
82	if p, ok := n.plain[stem]; ok {
83		return "/static/" + p, true
84	}
85	return "", false
86}
87
88func (s *site) nextImage(w http.ResponseWriter, r *http.Request) {
89	want, _ := strconv.Atoi(r.URL.Query().Get("w"))
90	target, ok := s.nextImages.resolve(r.URL.Query().Get("url"), want)
91	if !ok {
92		s.notFound(w, r)
93		return
94	}
95	w.Header().Set("Cache-Control", "public, max-age=86400")
96	http.Redirect(w, r, target, http.StatusMovedPermanently)
97}