diff --git a/admin/settings.go b/admin/settings.go
index cb78e87..d08fe8b 100644
--- a/admin/settings.go
+++ b/admin/settings.go
@@ -60,6 +60,7 @@ var settingGroups = []settingGroupDef{
{Key: "favicon", Label: "Favicon", Help: "Icon shown in the browser tab."},
{Key: "custom_header_code", Label: "Custom header code", Help: "HTML added to the
of every page."},
{Key: "custom_footer_code", Label: "Custom footer code", Help: "HTML added before on every page."},
+ {Key: "show_powered_by", Label: "Show \"Powered by goblog\"", Help: "Credit line with a link to goblog.live in the footer."},
}},
{ID: "comments", Title: "Comments", Settings: []settingMeta{
{Key: "comments_require_login", Label: "Require login to comment", Help: "When off, anyone can leave a comment."},
diff --git a/admin/settings_test.go b/admin/settings_test.go
index eaafb24..a0ac91b 100644
--- a/admin/settings_test.go
+++ b/admin/settings_test.go
@@ -14,7 +14,7 @@ var seededSettings = map[string]string{
"landing_page_image": "file", "favicon": "file", "custom_header_code": "textarea", "custom_footer_code": "textarea",
"theme": "text", "robots_tag": "text", "site_url": "text", "comments_require_login": "checkbox",
"plugin_directory_url": "text", "theme_directory_url": "text",
- "site_description": "textarea", "site_image": "file",
+ "site_description": "textarea", "site_image": "file", "show_powered_by": "checkbox",
}
func seededSettingMap() map[string]blog.Setting {
diff --git a/plugins/docs/content/writing-a-theme.md b/plugins/docs/content/writing-a-theme.md
index fc2108d..f34ca42 100644
--- a/plugins/docs/content/writing-a-theme.md
+++ b/plugins/docs/content/writing-a-theme.md
@@ -8,7 +8,7 @@ This page is about the templates: how they are loaded, what data they get, how t
At startup, and again whenever the `theme` setting changes, goblog builds one Go [`html/template`](https://pkg.go.dev/html/template) set in three layers, each parsed on top of the last:
-1. **Shared:** `templates/shared/*.html` — partials goblog owns because they carry behaviour rather than look. `_head.html` defines `_head`, the `` every page starts with (meta tags, the CDN stylesheets, `/theme/css/goblog.css`, the `custom_header_code` setting and the plugins' head HTML). `_search_results.html` defines `_search_results`, the search page's result list: the count, the no-results message, and one entry per result — posts and whatever installed plugins contribute (the plugin and theme directories, the docs). A theme includes it from `search.html` and never needs to know which kinds of result exist; a release that adds a new kind shows up on every theme.
+1. **Shared:** `templates/shared/*.html` — partials goblog owns because they carry behaviour rather than look. `_head.html` defines `_head`, the `` every page starts with (meta tags, the CDN stylesheets, `/theme/css/goblog.css`, the `custom_header_code` setting and the plugins' head HTML). `_search_results.html` defines `_search_results`, the search page's result list: the count, the no-results message, and one entry per result — posts and whatever installed plugins contribute (the plugin and theme directories, the docs). A theme includes it from `search.html` and never needs to know which kinds of result exist; a release that adds a new kind shows up on every theme. `_powered_by.html` defines `_powered_by`, the "Powered by goblog" credit: a `` holding the text and the link, or nothing when the site has switched the `show_powered_by` setting off. Include it from your `footer.html` inside the element you style the line with, rather than writing the link yourself, so the setting works on your theme too.
2. **Default:** `themes/default/templates/*.html`.
3. **Yours:** `/templates/*.html`.
diff --git a/plugins/docs/content_test.go b/plugins/docs/content_test.go
index 27947ef..089a668 100644
--- a/plugins/docs/content_test.go
+++ b/plugins/docs/content_test.go
@@ -131,7 +131,7 @@ func knownIdentifiers() map[string]bool {
// Setting keys seeded in tools/migrate.go and read via
// blog.SettingValue / ctx.Settings elsewhere.
"plugin_directory_url", "theme_directory_url", "refresh_minutes", "github_token",
- "site_url", "site_description", "site_image", "custom_header_code", "custom_footer_code",
+ "site_url", "site_description", "site_image", "custom_header_code", "custom_footer_code", "show_powered_by",
// comments_require_login is both a template data key (blog/blog.go
// post.html render calls) and the setting blog.CommentsRequireLogin
diff --git a/templates/shared/_powered_by.html b/templates/shared/_powered_by.html
new file mode 100644
index 0000000..0c93f30
--- /dev/null
+++ b/templates/shared/_powered_by.html
@@ -0,0 +1,11 @@
+{{/*
+_powered_by renders the "Powered by goblog" credit, unless the
+show_powered_by setting is off. A theme's footer includes it with
+{{ template "_powered_by" . }} inside whatever element it styles the line
+with, so the link and the setting live here rather than in every theme.
+The wizard pages have no settings yet and always show it.
+*/}}
+{{ define "_powered_by" -}}
+{{ $show := true }}{{ with .settings }}{{ if eq (index . "show_powered_by").Value "false" }}{{ $show = false }}{{ end }}{{ end -}}
+{{ if $show }}Powered by goblog {{ .version }}{{ end }}
+{{- end }}
diff --git a/templates_powered_by_test.go b/templates_powered_by_test.go
new file mode 100644
index 0000000..ddd3dcd
--- /dev/null
+++ b/templates_powered_by_test.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "goblog/blog"
+ "goblog/theme"
+)
+
+// TestPoweredBy: the shared _powered_by partial shows the credit unless the
+// show_powered_by setting is "false" — including before the setting exists
+// (an upgraded site, the wizard pages).
+func TestPoweredBy(t *testing.T) {
+ tmpl, _, err := theme.Load(theme.DefaultName, theme.FuncMap())
+ if err != nil {
+ t.Fatalf("load default theme: %v", err)
+ }
+ setting := func(v string) map[string]blog.Setting {
+ return map[string]blog.Setting{"show_powered_by": {Key: "show_powered_by", Type: "checkbox", Value: v}}
+ }
+ for name, tc := range map[string]struct {
+ data gin.H
+ want bool
+ }{
+ "no settings (wizard)": {gin.H{"version": "v1"}, true},
+ "setting not seeded": {gin.H{"version": "v1", "settings": map[string]blog.Setting{}}, true},
+ "on": {gin.H{"version": "v1", "settings": setting("true")}, true},
+ "off": {gin.H{"version": "v1", "settings": setting("false")}, false},
+ } {
+ var out bytes.Buffer
+ if err := tmpl.ExecuteTemplate(&out, "_powered_by", tc.data); err != nil {
+ t.Fatalf("%s: %v", name, err)
+ }
+ got := strings.Contains(out.String(), `Powered by
-
+ {{ template "_powered_by" . }}
diff --git a/themes/default/templates/wizard_settings.html b/themes/default/templates/wizard_settings.html
index 0d1edbe..c322313 100644
--- a/themes/default/templates/wizard_settings.html
+++ b/themes/default/templates/wizard_settings.html
@@ -125,7 +125,7 @@ Socials: (leave blank if you don't want to display)
Save Settings
-
+ {{ template "_powered_by" . }}
diff --git a/tools/migrate.go b/tools/migrate.go
index 5a93e5f..65036b8 100644
--- a/tools/migrate.go
+++ b/tools/migrate.go
@@ -328,6 +328,7 @@ func seedDefaultSettings(db *gorm.DB) {
{Key: "favicon", Type: "file", Value: "/img/favicon.ico"},
{Key: "custom_header_code", Type: "textarea", Value: ""},
{Key: "custom_footer_code", Type: "textarea", Value: ""},
+ {Key: "show_powered_by", Type: "checkbox", Value: "true"},
{Key: "theme", Type: "text", Value: "default"},
{Key: "robots_tag", Type: "text", Value: "index, follow"},
{Key: "site_url", Type: "text", Value: "https://www.example.com"},