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

3.9 KB · 141 lines · Go Raw History
  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    &gt;_
 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}