diff --git a/blog/feeds.go b/blog/feeds.go
new file mode 100644
index 0000000..3b66fab
--- /dev/null
+++ b/blog/feeds.go
@@ -0,0 +1,115 @@
+package blog
+
+import (
+ "encoding/xml"
+ "fmt"
+ "strings"
+ "time"
+)
+
+const baseURL = "https://chaosmith.systems"
+
+func generateRSS(s *Store) string {
+ var b strings.Builder
+ b.WriteString(`` + "\n")
+ b.WriteString(`` + "\n")
+ b.WriteString(" \n")
+ b.WriteString(" Chaosmith Systems — Field Notes \n")
+ b.WriteString(" " + baseURL + "/blog/\n")
+ b.WriteString(" Engineering, music, and experiments from controlled chaos. \n")
+ b.WriteString(" en \n")
+ b.WriteString(` ` + "\n")
+
+ for _, p := range s.All() {
+ b.WriteString(" - \n")
+ b.WriteString("
" + xmlEscape(p.Title) + " \n")
+ b.WriteString(" " + baseURL + "/blog/posts/" + p.Slug + "\n")
+ b.WriteString(" " + baseURL + "/blog/posts/" + p.Slug + " \n")
+ b.WriteString(" " + xmlEscape(p.Description) + " \n")
+ b.WriteString(" " + p.Date.Format(time.RFC1123Z) + " \n")
+ b.WriteString(" \n")
+ }
+
+ b.WriteString(" \n \n")
+ return b.String()
+}
+
+func generateJSONFeed(s *Store) string {
+ var b strings.Builder
+ b.WriteString(`{"version":"https://jsonfeed.org/version/1.1","title":"Chaosmith Systems — Field Notes","home_page_url":"` + baseURL + `/blog/","feed_url":"` + baseURL + `/blog/feed.json","items":[`)
+
+ first := true
+ for _, p := range s.All() {
+ if !first {
+ b.WriteString(",")
+ }
+ first = false
+ b.WriteString(`{`)
+ b.WriteString(`"id":"` + baseURL + "/blog/posts/" + p.Slug + `",`)
+ b.WriteString(`"url":"` + baseURL + "/blog/posts/" + p.Slug + `",`)
+ b.WriteString(`"title":"` + jsonEscape(p.Title) + `",`)
+ b.WriteString(`"summary":"` + jsonEscape(p.Description) + `",`)
+ b.WriteString(`"date_published":"` + p.Date.Format(time.RFC3339) + `"`)
+ b.WriteString(`}`)
+ }
+
+ b.WriteString("]}")
+ return b.String()
+}
+
+func generateSitemap(s *Store) string {
+ var b strings.Builder
+ b.WriteString(`` + "\n")
+ b.WriteString(`` + "\n")
+
+ // Static pages
+ b.WriteString(" \n")
+ b.WriteString(" " + baseURL + "/ \n")
+ b.WriteString(" monthly \n")
+ b.WriteString(" \n")
+ b.WriteString(" \n")
+ b.WriteString(" " + baseURL + "/blog/ \n")
+ b.WriteString(" weekly \n")
+ b.WriteString(" \n")
+
+ for _, p := range s.All() {
+ b.WriteString(" \n")
+ b.WriteString(" " + baseURL + "/blog/posts/" + p.Slug + " \n")
+ b.WriteString(" " + p.Date.Format("2006-01-02") + " \n")
+ b.WriteString(" \n")
+ }
+
+ b.WriteString(" \n")
+ return b.String()
+}
+
+func xmlEscape(s string) string {
+ var b strings.Builder
+ xml.EscapeText(&b, []byte(s))
+ return b.String()
+}
+
+func jsonEscape(s string) string {
+ var b strings.Builder
+ for _, r := range s {
+ switch r {
+ case '"':
+ b.WriteString(`\"`)
+ case '\\':
+ b.WriteString(`\\`)
+ case '\n':
+ b.WriteString(`\n`)
+ case '\r':
+ b.WriteString(`\r`)
+ case '\t':
+ b.WriteString(`\t`)
+ default:
+ if r < 0x20 {
+ b.WriteString(fmt.Sprintf(`\u%04x`, r))
+ } else {
+ b.WriteRune(r)
+ }
+ }
+ }
+ return b.String()
+}
diff --git a/blog/routes.go b/blog/routes.go
index 7d31092..4a6c7db 100644
--- a/blog/routes.go
+++ b/blog/routes.go
@@ -34,13 +34,13 @@ const pageTpl = `
{{end}}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Page not found · GitHub · GitHub
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You can’t perform that action at this time.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+