aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
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 /cmd
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 'cmd')
-rw-r--r--cmd/blogspace/seed.go40
1 files changed, 33 insertions, 7 deletions
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 `<p>` 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 `<p>` 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
+}