opt: gzip + no cdn shi + feeds
This commit is contained in:
115
blog/feeds.go
Normal file
115
blog/feeds.go
Normal file
@@ -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(`<?xml version="1.0" encoding="UTF-8"?>` + "\n")
|
||||||
|
b.WriteString(`<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">` + "\n")
|
||||||
|
b.WriteString(" <channel>\n")
|
||||||
|
b.WriteString(" <title>Chaosmith Systems — Field Notes</title>\n")
|
||||||
|
b.WriteString(" <link>" + baseURL + "/blog/</link>\n")
|
||||||
|
b.WriteString(" <description>Engineering, music, and experiments from controlled chaos.</description>\n")
|
||||||
|
b.WriteString(" <language>en</language>\n")
|
||||||
|
b.WriteString(` <atom:link href="` + baseURL + `/blog/feed.xml" rel="self" type="application/rss+xml"/>` + "\n")
|
||||||
|
|
||||||
|
for _, p := range s.All() {
|
||||||
|
b.WriteString(" <item>\n")
|
||||||
|
b.WriteString(" <title>" + xmlEscape(p.Title) + "</title>\n")
|
||||||
|
b.WriteString(" <link>" + baseURL + "/blog/posts/" + p.Slug + "</link>\n")
|
||||||
|
b.WriteString(" <guid>" + baseURL + "/blog/posts/" + p.Slug + "</guid>\n")
|
||||||
|
b.WriteString(" <description>" + xmlEscape(p.Description) + "</description>\n")
|
||||||
|
b.WriteString(" <pubDate>" + p.Date.Format(time.RFC1123Z) + "</pubDate>\n")
|
||||||
|
b.WriteString(" </item>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString(" </channel>\n</rss>\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(`<?xml version="1.0" encoding="UTF-8"?>` + "\n")
|
||||||
|
b.WriteString(`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">` + "\n")
|
||||||
|
|
||||||
|
// Static pages
|
||||||
|
b.WriteString(" <url>\n")
|
||||||
|
b.WriteString(" <loc>" + baseURL + "/</loc>\n")
|
||||||
|
b.WriteString(" <changefreq>monthly</changefreq>\n")
|
||||||
|
b.WriteString(" </url>\n")
|
||||||
|
b.WriteString(" <url>\n")
|
||||||
|
b.WriteString(" <loc>" + baseURL + "/blog/</loc>\n")
|
||||||
|
b.WriteString(" <changefreq>weekly</changefreq>\n")
|
||||||
|
b.WriteString(" </url>\n")
|
||||||
|
|
||||||
|
for _, p := range s.All() {
|
||||||
|
b.WriteString(" <url>\n")
|
||||||
|
b.WriteString(" <loc>" + baseURL + "/blog/posts/" + p.Slug + "</loc>\n")
|
||||||
|
b.WriteString(" <lastmod>" + p.Date.Format("2006-01-02") + "</lastmod>\n")
|
||||||
|
b.WriteString(" </url>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("</urlset>\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()
|
||||||
|
}
|
||||||
@@ -34,13 +34,13 @@ const pageTpl = `<!DOCTYPE html>
|
|||||||
<meta property="og:site_name" content="Chaosmith Systems">
|
<meta property="og:site_name" content="Chaosmith Systems">
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
{{end}}
|
{{end}}
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="stylesheet" href="/src/thirdparty/fonts/fonts.css" media="print" onload="this.media='all'">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="alternate" type="application/rss+xml" title="Chaosmith Systems — Field Notes" href="/blog/feed.xml">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Inter:wght@300;500;700&display=swap" rel="stylesheet">
|
<link rel="alternate" type="application/feed+json" title="Chaosmith Systems — Field Notes (JSON)" href="/blog/feed.json">
|
||||||
<link rel="stylesheet" href="/src/thirdparty/highlight/default.min.css">
|
<link rel="stylesheet" href="/src/thirdparty/highlight/default.min.css" media="print" onload="this.media='all'">
|
||||||
<script src="/src/thirdparty/highlight/highlight.min.js"></script>
|
<script defer src="/src/thirdparty/highlight/highlight.min.js"></script>
|
||||||
<script src="/src/lang/ergon.js"></script>
|
<script defer src="/src/lang/ergon.js"></script>
|
||||||
<script src="/src/lang/archasm.js"></script>
|
<script defer src="/src/lang/archasm.js"></script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #05030a;
|
--bg: #05030a;
|
||||||
@@ -137,6 +137,7 @@ const pageTpl = `<!DOCTYPE html>
|
|||||||
<a href="/blog/">Blog</a>
|
<a href="/blog/">Blog</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="/blog/feed.xml" style="margin-left:auto; font-size:13px; text-transform:uppercase; letter-spacing:0.1em; color:var(--text-dim); text-decoration:none;">RSS</a>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{{if .Post.HTML}}
|
{{if .Post.HTML}}
|
||||||
@@ -166,7 +167,7 @@ const pageTpl = `<!DOCTYPE html>
|
|||||||
<span>Chaosmith Systems</span>
|
<span>Chaosmith Systems</span>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script>hljs.highlightAll();</script>
|
<script>window.addEventListener('DOMContentLoaded',()=>hljs.highlightAll());</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
@@ -248,6 +249,30 @@ func RegisterRoutes(mux *http.ServeMux, contentDir string) error {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// RSS feed
|
||||||
|
mux.HandleFunc("/blog/feed.xml", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
|
||||||
|
w.Write([]byte(generateRSS(store)))
|
||||||
|
})
|
||||||
|
|
||||||
|
// JSON feed
|
||||||
|
mux.HandleFunc("/blog/feed.json", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/feed+json; charset=utf-8")
|
||||||
|
w.Write([]byte(generateJSONFeed(store)))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sitemap
|
||||||
|
mux.HandleFunc("/sitemap.xml", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||||
|
w.Write([]byte(generateSitemap(store)))
|
||||||
|
})
|
||||||
|
|
||||||
|
// robots.txt
|
||||||
|
mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.Write([]byte("User-agent: *\nAllow: /\n\nSitemap: https://chaosmith.systems/sitemap.xml\n"))
|
||||||
|
})
|
||||||
|
|
||||||
log.Printf("blog: loaded %d post(s) from %s, mounted at /blog/", store.Count(), contentDir)
|
log.Printf("blog: loaded %d post(s) from %s, mounted at /blog/", store.Count(), contentDir)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="description"
|
<meta name="description"
|
||||||
content="Chaosmith Systems - engineering, music, and experimental tools forged from controlled chaos.">
|
content="Chaosmith Systems - engineering, music, and experimental tools forged from controlled chaos.">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="stylesheet" href="/src/thirdparty/fonts/fonts.css" media="print" onload="this.media='all'">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Inter:wght@300;500;700&display=swap"
|
|
||||||
rel="stylesheet">
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #05030a;
|
--bg: #05030a;
|
||||||
@@ -336,7 +332,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<script src="src/thirdparty/htmx.min.js.js"></script>
|
<script defer src="src/thirdparty/htmx.min.js.js"></script>
|
||||||
<div class="noise"></div>
|
<div class="noise"></div>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<header>
|
<header>
|
||||||
|
|||||||
28
main.go
28
main.go
@@ -1,12 +1,38 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"chaosmith-site/blog"
|
"chaosmith-site/blog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type gzipResponseWriter struct {
|
||||||
|
io.Writer
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
return w.Writer.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func gzipHandler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
w.Header().Del("Content-Length")
|
||||||
|
gz := gzip.NewWriter(w)
|
||||||
|
defer gz.Close()
|
||||||
|
next.ServeHTTP(gzipResponseWriter{Writer: gz, ResponseWriter: w}, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -23,5 +49,5 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Println("listening on :8000")
|
log.Println("listening on :8000")
|
||||||
http.ListenAndServe(":8000", mux)
|
http.ListenAndServe(":8000", gzipHandler(mux))
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/thirdparty/fonts/fonts.css
vendored
Normal file
41
src/thirdparty/fonts/fonts.css
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/* Self-hosted fonts — no CDN */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/src/thirdparty/fonts/inter-300.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/src/thirdparty/fonts/inter-500.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/src/thirdparty/fonts/inter-700.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/src/thirdparty/fonts/jetbrains-mono-400.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/src/thirdparty/fonts/jetbrains-mono-600.woff2') format('woff2');
|
||||||
|
}
|
||||||
1115
src/thirdparty/fonts/inter-300.woff2
vendored
Normal file
1115
src/thirdparty/fonts/inter-300.woff2
vendored
Normal file
File diff suppressed because one or more lines are too long
1115
src/thirdparty/fonts/inter-500.woff2
vendored
Normal file
1115
src/thirdparty/fonts/inter-500.woff2
vendored
Normal file
File diff suppressed because one or more lines are too long
1115
src/thirdparty/fonts/inter-700.woff2
vendored
Normal file
1115
src/thirdparty/fonts/inter-700.woff2
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
src/thirdparty/fonts/jetbrains-mono-400.woff2
vendored
Normal file
BIN
src/thirdparty/fonts/jetbrains-mono-400.woff2
vendored
Normal file
Binary file not shown.
BIN
src/thirdparty/fonts/jetbrains-mono-600.woff2
vendored
Normal file
BIN
src/thirdparty/fonts/jetbrains-mono-600.woff2
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user