From 2c85ca1a46ddf4652f8fffc53d6bcfbe32b2b4ff Mon Sep 17 00:00:00 2001 From: grm Date: Tue, 15 Sep 2026 23:12:51 +0300 Subject: Add post tags, with a tag page and two side-column modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/, 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 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- cmd/blogspace/seed.go | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) (limited to 'cmd') diff --git a/cmd/blogspace/seed.go b/cmd/blogspace/seed.go index 05c54f8..14c378f 100644 --- a/cmd/blogspace/seed.go +++ b/cmd/blogspace/seed.go @@ -97,6 +97,8 @@ func seed(ctx context.Context, cfg *config.Config, st *store.Store) error { {Area: "footer", Kind: "text", Body: "© Alice — made with Blogspace"}, {Area: "right", Kind: "recent", Title: "Recent posts", Count: 5}, {Area: "right", Kind: "archive", Title: "Archive"}, + {Area: "right", Kind: "tags", Title: "Tags", Count: 10}, + {Area: "left", Kind: "tagcloud", Title: "Tag cloud"}, } { if _, err := bs.CreateModule(ctx, &m); err != nil { return err @@ -107,19 +109,34 @@ func seed(ctx context.Context, cfg *config.Config, st *store.Store) error { for i := 0; i < 10; i++ { body := fmt.Sprintf("Filler post number %d, here to demonstrate pagination.", i+1) post := &store.Post{PageID: home.ID, Title: fmt.Sprintf("Filler post %d", i+1), Slug: fmt.Sprintf("filler-%d", i+1), BodyMD: body, BodyHTML: markdown.Render(body), Published: true} - if _, err := bs.CreatePost(ctx, post); err != nil { + created, err := bs.CreatePost(ctx, post) + if err != nil { + return err + } + tags := []string{"filler"} // uneven use, so the tag cloud has some spread + if i%3 == 0 { + tags = append(tags, "Go") + } + if err := bs.SetPostTags(ctx, created.ID, seedTags(tags...)); err != nil { return err } } - posts := []struct{ page, title, body string }{ - {"home", "Welcome to my corner of the web", "This is the **first post**. It lives on the home page.\n\nThings I plan to write about:\n\n- tomatoes\n- Go\n- the occasional recipe\n\n![gradient](/media/" + bg.ID.String() + ")\n\nThat image above was uploaded through the dashboard, and here is a file to download: [notes.txt](/media/" + notes.ID.String() + ")."}, - {"home", "Why I still like plain HTML", "No frameworks, no build step. Just `

` tags and a stylesheet.\n\n> Simplicity is prerequisite for reliability. — Dijkstra\n\n```go\nfunc main() {\n\tfmt.Println(\"hello\")\n}\n```"}, - {"news", "Site is up", "The blog is live. Expect sporadic updates."}, - {"news", "Tomato season", "First ripe tomato of the year! Table for the record:\n\n| Variety | Days |\n|---|---|\n| Cherry | 62 |\n| Beefsteak | 85 |"}, + posts := []struct { + page, title, body string + tags []string + }{ + {"home", "Welcome to my corner of the web", "This is the **first post**. It lives on the home page.\n\nThings I plan to write about:\n\n- tomatoes\n- Go\n- the occasional recipe\n\n![gradient](/media/" + bg.ID.String() + ")\n\nThat image above was uploaded through the dashboard, and here is a file to download: [notes.txt](/media/" + notes.ID.String() + ").", []string{"Go", "tomatoes", "meta"}}, + {"home", "Why I still like plain HTML", "No frameworks, no build step. Just `

` tags and a stylesheet.\n\n> Simplicity is prerequisite for reliability. — Dijkstra\n\n```go\nfunc main() {\n\tfmt.Println(\"hello\")\n}\n```", []string{"Go", "web"}}, + {"news", "Site is up", "The blog is live. Expect sporadic updates.", []string{"meta"}}, + {"news", "Tomato season", "First ripe tomato of the year! Table for the record:\n\n| Variety | Days |\n|---|---|\n| Cherry | 62 |\n| Beefsteak | 85 |", []string{"tomatoes", "garden", "Συνταγές"}}, } for _, p := range posts { post := &store.Post{PageID: pages[p.page].ID, Title: p.title, Slug: slug.Make(p.title), BodyMD: p.body, BodyHTML: markdown.Render(p.body), Published: true} - if _, err := bs.CreatePost(ctx, post); err != nil { + created, err := bs.CreatePost(ctx, post) + if err != nil { + return err + } + if err := bs.SetPostTags(ctx, created.ID, seedTags(p.tags...)); err != nil { return err } } @@ -143,3 +160,12 @@ func seed(ctx context.Context, cfg *config.Config, st *store.Store) error { log.Printf("seed: superadmin %q (password %q in dev), blogger alice / alicealice at %s", cfg.SuperadminUsername, "admin", cfg.BlogURL("alice")) return nil } + +// seedTags builds tags the way the post form does: the slug is made from the name. +func seedTags(names ...string) []store.Tag { + out := make([]store.Tag, 0, len(names)) + for _, n := range names { + out = append(out, store.Tag{Name: n, Slug: slug.Make(n)}) + } + return out +} -- cgit v1.2.3