feat: OpenGraph metadata + auto-generated OG card images

- Per-post OG tags (title, description, image, date) in template head
- Twitter Card support (summary_large_image)
- Auto-generate 1200x630 card images from embedded base PNG
- Title, description, domain, date drawn on image via fogleman/gg
- Fonts embedded via go:embed (OpenSans Bold + Regular)
- Optional og-image: and og-description: front matter overrides
- Layout constants in ogimage.go for easy repositioning
- Generated images cached in content/og/ (gitignored)

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>
This commit is contained in:
2026-06-26 16:30:30 +03:00
parent cc80502a05
commit bd5eb15ade
9 changed files with 281 additions and 11 deletions

View File

@@ -4,6 +4,7 @@ import (
"html/template"
"log"
"net/http"
"path/filepath"
"strings"
)
@@ -13,6 +14,25 @@ const pageTpl = `<!DOCTYPE html>
<meta charset="UTF-8">
<title>{{.Title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
{{if .Post.HTML}}
<meta property="og:title" content="{{.Post.Title}}">
<meta property="og:description" content="{{.Post.Description}}">
<meta property="og:type" content="article">
<meta property="og:url" content="https://chaosmith.systems/blog/posts/{{.Post.Slug}}">
<meta property="og:image" content="https://chaosmith.systems{{.Post.OGImage}}">
<meta property="og:site_name" content="Chaosmith Systems">
<meta property="article:published_time" content="{{.Post.Date.Format "2006-01-02T15:04:05Z07:00"}}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{.Post.Title}}">
<meta name="twitter:description" content="{{.Post.Description}}">
<meta name="twitter:image" content="https://chaosmith.systems{{.Post.OGImage}}">
{{else}}
<meta property="og:title" content="{{.Title}}">
<meta property="og:type" content="website">
<meta property="og:url" content="https://chaosmith.systems/blog/">
<meta property="og:site_name" content="Chaosmith Systems">
<meta name="twitter:card" content="summary">
{{end}}
<link rel="preconnect" href="https://fonts.googleapis.com">
<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">
@@ -176,10 +196,14 @@ func render(w http.ResponseWriter, data pageData) {
}
func RegisterRoutes(mux *http.ServeMux, contentDir string) error {
ogDir := filepath.Join(contentDir, "..", "og")
store := NewStore()
if err := store.Load(contentDir); err != nil {
log.Printf("blog: warning: could not load posts from %s: %v", contentDir, err)
}
store.GenerateOGImages(ogDir)
mux.Handle("/blog/og/", http.StripPrefix("/blog/og/", http.FileServer(http.Dir(ogDir))))
mux.HandleFunc("/blog/admin/reload", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
@@ -190,6 +214,7 @@ func RegisterRoutes(mux *http.ServeMux, contentDir string) error {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
store.GenerateOGImages(ogDir)
w.Write([]byte("reloaded\n"))
})