aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/handlers_blog.go
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-15 23:12:51 +0300
committergrm <grm@eyesin.space>2026-09-15 23:12:51 +0300
commit2c85ca1a46ddf4652f8fffc53d6bcfbe32b2b4ff (patch)
treeecd5efd574b7d7463e4b2c4361c1994c4f28f561 /internal/web/handlers_blog.go
parent55732bdfc25933bab26220ccd82fe5e172d9e673 (diff)
downloadblogspace-2c85ca1a46ddf4652f8fffc53d6bcfbe32b2b4ff.tar.gz
blogspace-2c85ca1a46ddf4652f8fffc53d6bcfbe32b2b4ff.tar.bz2
blogspace-2c85ca1a46ddf4652f8fffc53d6bcfbe32b2b4ff.zip
Add post tags, with a tag page and two side-column modules
Posts can now carry tags, set on the post form as a checklist of the blog's existing tags plus a comma-separated box for new ones (no JS). A tag is a name and a unique slug, so "Go" and "go" are one tag and Greek tags get readable URLs; tags no post uses any more are deleted. On the blog, tags appear under the post date and link to /tag/<slug>, which lists the published posts from every page, paginated like a page. Two new layout modules show them: a Tags list (with counts, by use) and a Tag cloud (alphabetical, sized by use). Both are only fetched when a visible module needs them. The article loop and pager move to a shared postlist partial; while there, the pager stops adding a trailing slash — /news/?p=2 was a 404 because a {page} wildcard never matches one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/web/handlers_blog.go')
-rw-r--r--internal/web/handlers_blog.go52
1 files changed, 47 insertions, 5 deletions
diff --git a/internal/web/handlers_blog.go b/internal/web/handlers_blog.go
index f2e7713..c6030f2 100644
--- a/internal/web/handlers_blog.go
+++ b/internal/web/handlers_blog.go
@@ -51,10 +51,26 @@ func (s *Server) blogView(r *http.Request) (map[string]any, error) {
}
v["recent"] = recent
}
+ if layout.NeedsTags() {
+ counts, err := blogStore(r).TagCounts(ctx)
+ if err != nil {
+ return nil, err
+ }
+ v["tags"], v["tagcloud"] = topTags(counts), cloudSizes(counts)
+ }
v["notices"] = placeNotices(sections, layout)
return v, nil
}
+// 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"))
+ if n < 1 {
+ n = 1
+ }
+ return n
+}
+
func (s *Server) handleBlogHome(w http.ResponseWriter, r *http.Request) {
page, err := blogStore(r).HomePage(r.Context())
if err != nil {
@@ -83,10 +99,7 @@ func (s *Server) renderPage(w http.ResponseWriter, r *http.Request, page *store.
s.serverError(w, err)
return
}
- n, _ := strconv.Atoi(r.URL.Query().Get("p"))
- if n < 1 {
- n = 1
- }
+ n := pageNum(r)
posts, total, err := blogStore(r).PublishedPosts(r.Context(), page.ID, postsPerPage, (n-1)*postsPerPage)
if err != nil {
s.serverError(w, err)
@@ -94,13 +107,42 @@ func (s *Server) renderPage(w http.ResponseWriter, r *http.Request, page *store.
}
last := (total + postsPerPage - 1) / postsPerPage
v["page"], v["posts"], v["pageNum"], v["lastPage"] = page, posts, n, last
+ // The pager appends ?p=N to base: no trailing slash, which /{page} would not match.
v["base"] = "/" + page.Slug
if page.IsHome {
- v["base"] = ""
+ v["base"] = "/"
}
s.render(w, r, "blog/page.html", v)
}
+// handleBlogTag lists the published posts carrying a tag, from every page of
+// the blog. A tag that is only on hidden posts is as good as absent.
+func (s *Server) handleBlogTag(w http.ResponseWriter, r *http.Request) {
+ tag, err := blogStore(r).TagBySlug(r.Context(), r.PathValue("tag"))
+ if err != nil {
+ s.blogNotFound(w, r)
+ return
+ }
+ n := pageNum(r)
+ posts, total, err := blogStore(r).PublishedPostsByTag(r.Context(), tag.ID, postsPerPage, (n-1)*postsPerPage)
+ if err != nil {
+ s.serverError(w, err)
+ return
+ }
+ if total == 0 {
+ s.blogNotFound(w, r)
+ return
+ }
+ v, err := s.blogView(r)
+ if err != nil {
+ s.serverError(w, err)
+ return
+ }
+ last := (total + postsPerPage - 1) / postsPerPage
+ v["tag"], v["posts"], v["pageNum"], v["lastPage"], v["base"] = tag, posts, n, last, "/tag/"+tag.Slug
+ s.render(w, r, "blog/tag.html", v)
+}
+
func (s *Server) handleBlogPost(w http.ResponseWriter, r *http.Request) {
page, err := blogStore(r).PageBySlug(r.Context(), r.PathValue("page"))
if err != nil {