aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/layout.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/layout.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/layout.go')
-rw-r--r--internal/web/layout.go41
1 files changed, 29 insertions, 12 deletions
diff --git a/internal/web/layout.go b/internal/web/layout.go
index ad55e82..b8069a0 100644
--- a/internal/web/layout.go
+++ b/internal/web/layout.go
@@ -13,8 +13,8 @@ import (
// blocks; the side columns take the small boxes; the footer its own set.
var moduleKinds = map[string][]string{
"header": {"title", "logo", "menu"},
- "left": {"menu", "recent", "archive", "logo", "html"},
- "right": {"menu", "recent", "archive", "logo", "html"},
+ "left": {"menu", "recent", "archive", "tags", "tagcloud", "logo", "html"},
+ "right": {"menu", "recent", "archive", "tags", "tagcloud", "logo", "html"},
"above": {"html"},
"below": {"html"},
"footer": {"text", "rss", "sitemap", "html"},
@@ -22,15 +22,17 @@ var moduleKinds = map[string][]string{
// Friendly names for the dashboard (English; translate with t / s.tr at the point of use).
var moduleNames = map[string]string{
- "title": "Blog title & tagline",
- "logo": "Logo",
- "menu": "Menu",
- "archive": "Archive (posts by month)",
- "recent": "Recent posts",
- "html": "Custom HTML",
- "rss": "RSS link",
- "text": "Footer text",
- "sitemap": "Site map",
+ "title": "Blog title & tagline",
+ "logo": "Logo",
+ "menu": "Menu",
+ "archive": "Archive (posts by month)",
+ "recent": "Recent posts",
+ "tags": "Tags",
+ "tagcloud": "Tag cloud",
+ "html": "Custom HTML",
+ "rss": "RSS link",
+ "text": "Footer text",
+ "sitemap": "Site map",
}
var areaNames = map[string]string{
@@ -53,7 +55,7 @@ func allowedKind(area, kind string) bool {
// moduleHasSettings says whether the module has an edit page.
func moduleHasSettings(kind string) bool {
switch kind {
- case "html", "text", "recent", "archive", "sitemap":
+ case "html", "text", "recent", "archive", "sitemap", "tags", "tagcloud":
return true
}
return false
@@ -64,6 +66,11 @@ func moduleSummary(lang string, m store.Module) string {
switch m.Kind {
case "recent":
return i18n.Tf(lang, "%d posts", m.Count) + headingNote(lang, m)
+ case "tags":
+ if m.Count == 0 {
+ return i18n.T(lang, "all tags") + headingNote(lang, m)
+ }
+ return i18n.Tf(lang, "%d tags", m.Count) + headingNote(lang, m)
case "html", "text":
body := strings.Join(strings.Fields(m.Body), " ")
if body == "" {
@@ -160,6 +167,16 @@ func (l Layout) NeedsArchive() bool {
return false
}
+// NeedsTags says whether any visible module shows the tags.
+func (l Layout) NeedsTags() bool {
+ for _, m := range l.all() {
+ if m.Kind == "tags" || m.Kind == "tagcloud" {
+ return true
+ }
+ }
+ return false
+}
+
// MaxRecent is the longest "recent posts" list any visible module wants (0 = none).
func (l Layout) MaxRecent() int {
n := 0