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 "bytes"
5 "encoding/xml"
6 "fmt"
7 "net/http"
8 "strings"
9)
10
11// robots.txt, the sitemap and the feed. None of these are HTML, so they use
12// encoding/xml and an explicit escape rather than html/template.
13
14// wrapTitle greedily breaks a title into at most maxLines lines of about
15// maxChars.
16func wrapTitle(title string, maxChars, maxLines int) []string {
17 var lines []string
18 current := ""
19 for _, word := range strings.Fields(title) {
20 switch {
21 case current == "":
22 current = word
23 case len(current)+len(word)+1 > maxChars:
24 lines = append(lines, current)
25 current = word
26 default:
27 current += " " + word
28 }
29 }
30 if current != "" {
31 lines = append(lines, current)
32 }
33 if len(lines) > maxLines {
34 lines = lines[:maxLines]
35 }
36 return lines
37}
38
39func xmlEscape(s string) string {
40 var buf bytes.Buffer
41 _ = xml.EscapeText(&buf, []byte(s))
42 return buf.String()
43}
44
45// Generated rather than a file, so its two colours stay the stylesheet's two.
46const faviconSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 16 16">
47 <defs>
48 <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
49 <stop offset="0%" stop-color="rgb(14, 63, 180)"/>
50 <stop offset="100%" stop-color="rgb(107, 158, 120)"/>
51 </linearGradient>
52 </defs>
53 <rect width="16" height="16" rx="2" fill="url(#g)"/>
54 <text x="8" y="11.5" text-anchor="middle" font-family="monospace" font-weight="bold" font-size="9" fill="rgba(255,255,255,0.9)">
55 >_
56 </text>
57</svg>
58`
59
60func favicon(w http.ResponseWriter, r *http.Request) {
61 w.Header().Set("Content-Type", "image/svg+xml")
62 w.Header().Set("Cache-Control", "public, max-age=86400")
63 _, _ = w.Write([]byte(faviconSVG))
64}
65
66// robots keeps a staging hostname out of the index. It duplicates the noindex
67// meta tag, which a crawler told not to fetch the page never sees.
68func robots(w http.ResponseWriter, r *http.Request) {
69 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
70 if Staging {
71 _, _ = fmt.Fprint(w, "User-agent: *\nDisallow: /\n")
72 return
73 }
74 _, _ = fmt.Fprintf(w, "User-agent: *\nAllow: /\n\nSitemap: %s/sitemap.xml\n", baseURL)
75}
76
77type urlEntry struct {
78 Loc string `xml:"loc"`
79 LastMod string `xml:"lastmod,omitempty"`
80 ChangeFreq string `xml:"changefreq,omitempty"`
81}
82
83type urlSet struct {
84 XMLName xml.Name `xml:"urlset"`
85 NS string `xml:"xmlns,attr"`
86 URLs []urlEntry `xml:"url"`
87}
88
89func (s *site) sitemap(w http.ResponseWriter, r *http.Request) {
90 published, tags, years := s.lib.Published()
91
92 set := urlSet{NS: "http://www.sitemaps.org/schemas/sitemap/0.9"}
93 set.URLs = append(set.URLs,
94 urlEntry{Loc: baseURL + "/", ChangeFreq: "weekly"},
95 urlEntry{Loc: baseURL + "/blog/", ChangeFreq: "weekly"},
96 )
97
98 // A tag or year page is as fresh as its newest post.
99 tagLastMod := map[string]string{}
100 yearLastMod := map[string]string{}
101 for _, post := range published {
102 set.URLs = append(set.URLs, urlEntry{
103 Loc: baseURL + post.URL(), LastMod: post.Date, ChangeFreq: "yearly",
104 })
105 for _, tag := range post.Tags {
106 if post.Date > tagLastMod[tag] {
107 tagLastMod[tag] = post.Date
108 }
109 }
110 if len(post.Date) >= 4 {
111 if year := post.Date[:4]; post.Date > yearLastMod[year] {
112 yearLastMod[year] = post.Date
113 }
114 }
115 }
116 for _, tag := range tags {
117 set.URLs = append(set.URLs, urlEntry{
118 Loc: baseURL + tag.URL, LastMod: tagLastMod[tag.Name], ChangeFreq: "monthly",
119 })
120 }
121 for _, year := range years {
122 set.URLs = append(set.URLs, urlEntry{
123 Loc: baseURL + yearURL(year), LastMod: yearLastMod[year], ChangeFreq: "yearly",
124 })
125 }
126
127 var buf bytes.Buffer
128 buf.WriteString(xml.Header)
129 encoder := xml.NewEncoder(&buf)
130 encoder.Indent("", " ")
131 if err := encoder.Encode(set); err != nil {
132 http.Error(w, "internal server error", http.StatusInternalServerError)
133 return
134 }
135 buf.WriteByte('\n')
136
137 w.Header().Set("Content-Type", "application/xml; charset=utf-8")
138 w.Header().Set("Cache-Control", "public, max-age=3600")
139 _, _ = buf.WriteTo(w)
140}