Add Technical Kiwi website with Go, templ, and HTMX.

Single-page site with gallery by album and event, contact form over SMTP,
Docker dev/prod setup, and on-server image derivatives. Gallery photos stay
local (app/images/ is gitignored).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-25 23:57:59 +12:00
parent c21be097b0
commit 509e7ccb43
33 changed files with 2635 additions and 1 deletions

View File

@@ -0,0 +1,61 @@
package middleware
import (
"compress/gzip"
"io"
"net/http"
"strconv"
"strings"
"time"
)
type gzipResponseWriter struct {
http.ResponseWriter
io.Writer
}
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
// Gzip compresses responses when the client accepts it.
func Gzip(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("Vary", "Accept-Encoding")
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r)
})
}
// CacheStatic wraps a file server with long-lived cache headers.
func CacheStatic(dir string, maxAge time.Duration) http.Handler {
fs := http.FileServer(http.Dir(dir))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "public, max-age="+formatMaxAge(maxAge)+", immutable")
fs.ServeHTTP(w, r)
})
}
// CacheImages wraps image serving; thumbs cache longer than originals.
func CacheImages(dir string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rel := strings.TrimPrefix(r.URL.Path, "/")
if strings.HasPrefix(rel, "thumbs/") || strings.HasPrefix(rel, "hero/") {
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
} else {
w.Header().Set("Cache-Control", "public, max-age=86400")
}
http.FileServer(http.Dir(dir)).ServeHTTP(w, r)
})
}
func formatMaxAge(d time.Duration) string {
return strconv.Itoa(int(d.Seconds()))
}