From ac125593efe689fabd1e48a7023249c8973698b8 Mon Sep 17 00:00:00 2001 From: grm Date: Fri, 18 Sep 2026 11:22:25 +0300 Subject: Add switches to keep announcements off a page or off post pages Announcements showed on every public page. Now each page has a "Show announcements on this page" checkbox (pages.show_notices, on by default) that covers the page and its posts, and the Design tab's Content section has "Show announcements on post pages" (theme.post_notices, on by default) for bloggers who want notices only on listings, not while reading a post. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- internal/web/handlers_blog.go | 12 ++++++++++++ internal/web/handlers_pages.go | 3 ++- internal/web/templates/dashboard/design.html | 1 + internal/web/templates/dashboard/page_form.html | 1 + internal/web/templates/dashboard/sections.html | 2 +- internal/web/theme.go | 4 +++- internal/web/web_test.go | 16 ++++++++++++++++ 7 files changed, 36 insertions(+), 3 deletions(-) (limited to 'internal/web') diff --git a/internal/web/handlers_blog.go b/internal/web/handlers_blog.go index c26adfa..c1afedb 100644 --- a/internal/web/handlers_blog.go +++ b/internal/web/handlers_blog.go @@ -63,6 +63,10 @@ func (s *Server) blogView(r *http.Request) (map[string]any, error) { return v, nil } +// noNotices stands in for the placed announcements on pages that hide them +// (typed, so the layout's index lookups still work). +var noNotices = map[string][]store.Section{} + // pageNum reads the ?p= of a paginated listing (1 when absent or silly). func pageNum(r *http.Request) int { n, _ := strconv.Atoi(r.URL.Query().Get("p")) @@ -109,6 +113,9 @@ func (s *Server) renderPage(w http.ResponseWriter, r *http.Request, page *store. last := (total + per - 1) / per fillExcerpts(v["theme"].(Theme), posts) v["page"], v["posts"], v["pageNum"], v["lastPage"] = page, posts, n, last + if !page.ShowNotices { + v["notices"] = noNotices + } // The pager appends ?p=N to base: no trailing slash, which /{page} would not match. v["base"] = "/" + page.Slug if page.IsHome { @@ -195,6 +202,11 @@ func (s *Server) handleBlogPost(w http.ResponseWriter, r *http.Request) { return } v["page"], v["post"] = page, post + // A post shows the announcements only when its page does and the theme + // wants them on posts at all. + if !page.ShowNotices || !v["theme"].(Theme).PostNotices { + v["notices"] = noNotices + } s.render(w, r, "blog/post.html", v) } diff --git a/internal/web/handlers_pages.go b/internal/web/handlers_pages.go index 8dc0f5f..b085b82 100644 --- a/internal/web/handlers_pages.go +++ b/internal/web/handlers_pages.go @@ -39,7 +39,7 @@ func (s *Server) loadPage(w http.ResponseWriter, r *http.Request) *store.Page { } func (s *Server) handlePageForm(w http.ResponseWriter, r *http.Request) { - p := &store.Page{ShowInNav: true} + p := &store.Page{ShowInNav: true, ShowNotices: true} if r.PathValue("id") != "" { if p = s.loadPage(w, r); p == nil { return @@ -62,6 +62,7 @@ func (s *Server) handlePageSave(w http.ResponseWriter, r *http.Request) { p.OutroMD = strings.ReplaceAll(r.FormValue("outro"), "\r\n", "\n") p.Format = pick(r.FormValue("format"), store.FormatMarkdown, store.FormatHTML) p.ShowInNav = r.FormValue("show_in_nav") == "on" + p.ShowNotices = r.FormValue("show_notices") == "on" autoSlug := p.Slug == "" if autoSlug { p.Slug = slug.Make(p.Title) diff --git a/internal/web/templates/dashboard/design.html b/internal/web/templates/dashboard/design.html index fea1b92..476e311 100644 --- a/internal/web/templates/dashboard/design.html +++ b/internal/web/templates/dashboard/design.html @@ -188,6 +188,7 @@

{{t "Posts"}}

+
+

{{if .Data.page.ID}}{{t "View page"}} ↗{{end}} diff --git a/internal/web/templates/dashboard/sections.html b/internal/web/templates/dashboard/sections.html index 9f0e314..2767921 100644 --- a/internal/web/templates/dashboard/sections.html +++ b/internal/web/templates/dashboard/sections.html @@ -4,7 +4,7 @@

{{t "Announcements"}}

-

{{t "Notices shown on every page and post of your blog — the next meeting, this month's book, a holiday closure. Hide one instead of deleting it if you will need it again."}}

+

{{t "Notices shown on every page and post of your blog — the next meeting, this month's book, a holiday closure. Hide one instead of deleting it if you will need it again. A page can opt out on its form, and the Design tab can keep them off post pages."}}

{{if .Data.sections}} diff --git a/internal/web/theme.go b/internal/web/theme.go index 676a01e..0437f51 100644 --- a/internal/web/theme.go +++ b/internal/web/theme.go @@ -55,6 +55,7 @@ type Theme struct { PostImage string `json:"post_image"` // where the post page shows the featured image: top | bottom | list (listings only) TitleRule string `json:"title_rule"` // none | line | dots | dots2 | stripes, drawn under page and post titles in the link colour TagHash bool `json:"tag_hash"` // tags are listed as #name + PostNotices bool `json:"post_notices"` // announcements are shown on post pages too, not only on listings FooterBg string `json:"footer_bg"` FooterColor string `json:"footer_color"` @@ -108,7 +109,7 @@ func DefaultTheme() Theme { Font: "sans", HeadingFont: "same", FontSize: "normal", HeaderImageHeight: "medium", HeaderAlign: "left", HeaderBg: "#2f3a4a", HeaderText: "#ffffff", TitleSize: "normal", HeaderRule: "none", NavAlign: "left", NavStyle: "normal", NavBg: "#ffffff", NavText: "#222222", - ShowDates: true, DateFormat: "long", PostsPerPage: postsPerPageChoices[0], ListStyle: "full", PostImage: "top", TitleRule: "none", + ShowDates: true, DateFormat: "long", PostsPerPage: postsPerPageChoices[0], ListStyle: "full", PostImage: "top", TitleRule: "none", PostNotices: true, FooterBg: "#2f3a4a", FooterColor: "#d0d5dc", FooterAlign: "center", LogoSize: "medium", HeaderOn: true, LeftOn: true, RightOn: true, FooterOn: true, LeftWidth: 25, RightWidth: 25, PageAlign: "center", @@ -269,6 +270,7 @@ func ThemeFromForm(cur Theme, f url.Values) Theme { t.PostImage = get("post_image") t.TitleRule = get("title_rule") t.TagHash = on("tag_hash") + t.PostNotices = on("post_notices") t.FooterBg = get("footer_bg") t.FooterColor = get("footer_color") t.FooterAlign = get("footer_align") diff --git a/internal/web/web_test.go b/internal/web/web_test.go index a7c189f..34ec057 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -138,6 +138,22 @@ func TestThemeNormalizeAndCSS(t *testing.T) { } } +// Announcements on post pages are on by default (the look blogs had before +// the switch) and, like every checkbox, off when the box is unticked; stored +// themes from before the option get the default too. +func TestPostNotices(t *testing.T) { + if !DefaultTheme().PostNotices || !ParseTheme([]byte(`{"show_dates":false}`)).PostNotices { + t.Error("post notices should default to on") + } + th := ThemeFromForm(DefaultTheme(), url.Values{"post_notices": {"on"}}) + if !th.PostNotices { + t.Errorf("ticked: %+v", th) + } + if th = ThemeFromForm(th, url.Values{"show_dates": {"on"}}); th.PostNotices { + t.Errorf("unticked: %+v", th) + } +} + // The rule under titles is a body class blog.css draws from, coloured by the // theme CSS; the tag hash is a plain flag the templates read. func TestTitleRuleAndTagHash(t *testing.T) { -- cgit v1.2.3