From 834bab3ad9e1e55550e079d826b7ab9e258d9542 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Thu, 19 Mar 2026 10:28:40 -0700 Subject: [PATCH 1/2] Extract shared head template from themes Move all content (meta tags, OG, structured data, CDN imports) into a shared _head.html template loaded alongside any theme. Themes now only control layout and navigation. Adds site_url and robots_tag settings so OG URLs and indexing are configurable per instance rather than hardcoded. Co-Authored-By: Claude Opus 4.6 (1M context) --- blog/blog_test.go | 6 +- goblog.go | 10 +++- templates/shared/_head.html | 79 ++++++++++++++++++++++++++ themes/default/templates/header.html | 83 +--------------------------- themes/forest/templates/header.html | 83 +--------------------------- themes/minimal/templates/header.html | 83 +--------------------------- tools/migrate.go | 2 + 7 files changed, 100 insertions(+), 246 deletions(-) create mode 100644 templates/shared/_head.html diff --git a/blog/blog_test.go b/blog/blog_test.go index 32a62b3..41424f5 100644 --- a/blog/blog_test.go +++ b/blog/blog_test.go @@ -175,7 +175,11 @@ func TestBlogWorkflow(t *testing.T) { router.SetFuncMap(template.FuncMap{ "rawHTML": func(s string) template.HTML { return template.HTML(s) }, }) - router.LoadHTMLGlob("../themes/default/templates/*") + tmpl := template.Must(template.New("").Funcs(template.FuncMap{ + "rawHTML": func(s string) template.HTML { return template.HTML(s) }, + }).ParseGlob("../templates/shared/*.html")) + template.Must(tmpl.ParseGlob("../themes/default/templates/*.html")) + router.SetHTMLTemplate(tmpl) a.On("IsAdmin", mock.Anything).Return(false).Once() a.On("IsLoggedIn", mock.Anything).Return(false) jsonValue, _ = json.Marshal("") diff --git a/goblog.go b/goblog.go index 331bc90..bca12ff 100644 --- a/goblog.go +++ b/goblog.go @@ -374,12 +374,18 @@ func main() { } themePath := filepath.Join("themes", theme) + "/" log.Println("Loading theme: " + theme) - tmpl, err := template.New("").Funcs(funcMap).ParseGlob(themePath + "templates/*.html") + // Load shared templates first, then theme templates + tmpl, err := template.New("").Funcs(funcMap).ParseGlob("templates/shared/*.html") + if err != nil { + log.Printf("Warning: failed to load shared templates: %v", err) + tmpl = template.New("").Funcs(funcMap) + } + tmpl, err = tmpl.ParseGlob(themePath + "templates/*.html") if err != nil { log.Printf("Warning: failed to load theme %q: %v — falling back to default", theme, err) theme = "default" themePath = "themes/default/" - tmpl = template.Must(template.New("").Funcs(funcMap).ParseGlob(themePath + "templates/*.html")) + tmpl = template.Must(template.Must(template.New("").Funcs(funcMap).ParseGlob("templates/shared/*.html")).ParseGlob(themePath + "templates/*.html")) } router.SetHTMLTemplate(tmpl) activeTheme = theme diff --git a/templates/shared/_head.html b/templates/shared/_head.html new file mode 100644 index 0000000..cdab6a5 --- /dev/null +++ b/templates/shared/_head.html @@ -0,0 +1,79 @@ +{{ define "_head" }} + + + + + + + + + + {{ if .post }} + + + + + + + + + + {{ range .post.Tags }} + + {{ end }} + {{ .settings.site_title.Value }}: {{ .post.Title }} + + {{ else }} + + {{ .settings.site_title.Value }}: {{ .title }} + {{ end }} + + + {{ if .admin_page }} + + + + + {{ end }} + {{ if or .admin_page .post }} + + + {{ end }} + + + + + + + {{ if .admin_page }} + + + + + {{ end }} + + {{ with index .settings "custom_header_code" }}{{ if .Value }} + {{ .Value | rawHTML }} + {{ end }}{{ end }} + {{ with .plugin_head_html }}{{ . | rawHTML }}{{ end }} + +{{ end }} diff --git a/themes/default/templates/header.html b/themes/default/templates/header.html index 9c29cd6..ffe0ea3 100644 --- a/themes/default/templates/header.html +++ b/themes/default/templates/header.html @@ -1,83 +1,4 @@ - - - - - - - - - - {{ if .post }} - - - - - - - - - - {{ range .post.Tags }} - - {{ end }} - {{ .settings.site_title.Value }}: {{ .post.Title }} - - {{ else }} - - {{ .settings.site_title.Value }}: {{ .title }} - {{ end }} - - - - - {{ if .admin_page }} - - - - - {{ end }} - {{ if or .admin_page .post }} - - - {{ end }} - - - - - - - {{ if .admin_page }} - - - - - {{ end }} - - - {{ with index .settings "custom_header_code" }}{{ if .Value }} - {{ .Value | rawHTML }} - {{ end }}{{ end }} - {{ with .plugin_head_html }}{{ . | rawHTML }}{{ end }} - +{{ template "_head" . }} -
\ No newline at end of file +
diff --git a/themes/forest/templates/header.html b/themes/forest/templates/header.html index 5d33b88..20153bb 100644 --- a/themes/forest/templates/header.html +++ b/themes/forest/templates/header.html @@ -1,83 +1,4 @@ - - - - - - - - - - {{ if .post }} - - - - - - - - - - {{ range .post.Tags }} - - {{ end }} - {{ .settings.site_title.Value }}: {{ .post.Title }} - - {{ else }} - - {{ .settings.site_title.Value }}: {{ .title }} - {{ end }} - - - - - {{ if .admin_page }} - - - - - {{ end }} - {{ if or .admin_page .post }} - - - {{ end }} - - - - - - - {{ if .admin_page }} - - - - - {{ end }} - - - {{ with index .settings "custom_header_code" }}{{ if .Value }} - {{ .Value | rawHTML }} - {{ end }}{{ end }} - {{ with .plugin_head_html }}{{ . | rawHTML }}{{ end }} - +{{ template "_head" . }}
-
\ No newline at end of file +
diff --git a/themes/minimal/templates/header.html b/themes/minimal/templates/header.html index 5191bbf..a27c7d0 100644 --- a/themes/minimal/templates/header.html +++ b/themes/minimal/templates/header.html @@ -1,83 +1,4 @@ - - - - - - - - - - {{ if .post }} - - - - - - - - - - {{ range .post.Tags }} - - {{ end }} - {{ .settings.site_title.Value }}: {{ .post.Title }} - - {{ else }} - - {{ .settings.site_title.Value }}: {{ .title }} - {{ end }} - - - - - {{ if .admin_page }} - - - - - {{ end }} - {{ if or .admin_page .post }} - - - {{ end }} - - - - - - - {{ if .admin_page }} - - - - - {{ end }} - - - {{ with index .settings "custom_header_code" }}{{ if .Value }} - {{ .Value | rawHTML }} - {{ end }}{{ end }} - {{ with .plugin_head_html }}{{ . | rawHTML }}{{ end }} - +{{ template "_head" . }}
-
\ No newline at end of file +
diff --git a/tools/migrate.go b/tools/migrate.go index 7369a81..4a49798 100644 --- a/tools/migrate.go +++ b/tools/migrate.go @@ -276,6 +276,8 @@ func seedDefaultSettings(db *gorm.DB) { {Key: "custom_header_code", Type: "textarea", Value: ""}, {Key: "custom_footer_code", Type: "textarea", Value: ""}, {Key: "theme", Type: "text", Value: "default"}, + {Key: "robots_tag", Type: "text", Value: "index, follow"}, + {Key: "site_url", Type: "text", Value: ""}, } for _, s := range defaults { db.Where("key = ?", s.Key).FirstOrCreate(&s) From 90594a68c0ef3bd738ec72b0fe899c44f3822ae8 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Thu, 19 Mar 2026 10:41:50 -0700 Subject: [PATCH 2/2] Address review feedback on shared head template - Fail fast (log.Fatalf) when shared templates fail to load, since all themes depend on _head being available - Seed site_url with https://www.example.com instead of empty string, so OG/structured data URLs are always absolute - Fall back to "index, follow" when robots_tag value is empty, not just when the setting key is missing Co-Authored-By: Claude Opus 4.6 (1M context) --- goblog.go | 3 +-- templates/shared/_head.html | 2 +- tools/migrate.go | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/goblog.go b/goblog.go index bca12ff..8e047ba 100644 --- a/goblog.go +++ b/goblog.go @@ -377,8 +377,7 @@ func main() { // Load shared templates first, then theme templates tmpl, err := template.New("").Funcs(funcMap).ParseGlob("templates/shared/*.html") if err != nil { - log.Printf("Warning: failed to load shared templates: %v", err) - tmpl = template.New("").Funcs(funcMap) + log.Fatalf("Failed to load shared templates: %v", err) } tmpl, err = tmpl.ParseGlob(themePath + "templates/*.html") if err != nil { diff --git a/templates/shared/_head.html b/templates/shared/_head.html index cdab6a5..9a34064 100644 --- a/templates/shared/_head.html +++ b/templates/shared/_head.html @@ -6,7 +6,7 @@ - + {{ if .post }} diff --git a/tools/migrate.go b/tools/migrate.go index 4a49798..aea8c3f 100644 --- a/tools/migrate.go +++ b/tools/migrate.go @@ -277,7 +277,7 @@ func seedDefaultSettings(db *gorm.DB) { {Key: "custom_footer_code", Type: "textarea", Value: ""}, {Key: "theme", Type: "text", Value: "default"}, {Key: "robots_tag", Type: "text", Value: "index, follow"}, - {Key: "site_url", Type: "text", Value: ""}, + {Key: "site_url", Type: "text", Value: "https://www.example.com"}, } for _, s := range defaults { db.Where("key = ?", s.Key).FirstOrCreate(&s)