From 847e8e76d063ea4be36175fb7677f70e55b9917c Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 18 Mar 2026 21:11:18 +1100 Subject: [PATCH] Add categories feature design spec Co-Authored-By: Claude Opus 4.6 --- .../specs/2026-03-18-categories-design.md | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-18-categories-design.md diff --git a/docs/superpowers/specs/2026-03-18-categories-design.md b/docs/superpowers/specs/2026-03-18-categories-design.md new file mode 100644 index 0000000..eb5a004 --- /dev/null +++ b/docs/superpowers/specs/2026-03-18-categories-design.md @@ -0,0 +1,157 @@ +# Categories Feature Design + +## Overview + +Add flat, many-to-many categories to Weiro. Categories are managed via a dedicated admin page and assigned to posts on the post edit form. On the published static site, categories appear as labels on posts, archive pages per category, a category index page, and per-category RSS/JSON feeds. Categories with no published posts are hidden from the published site. + +## Data Model + +### New Tables (migration `04_categories.up.sql`) + +```sql +CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_id INTEGER NOT NULL, + guid TEXT NOT NULL, + name TEXT NOT NULL, + slug TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + created_at INTEGER NOT NULL, + FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE +); +CREATE INDEX idx_categories_site ON categories (site_id); +CREATE UNIQUE INDEX idx_categories_guid ON categories (guid); +CREATE UNIQUE INDEX idx_categories_site_slug ON categories (site_id, slug); + +CREATE TABLE post_categories ( + post_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + PRIMARY KEY (post_id, category_id), + FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE +); +CREATE INDEX idx_post_categories_category ON post_categories (category_id); +``` + +### New Go Model (`models/categories.go`) + +```go +type Category struct { + ID int64 `json:"id"` + SiteID int64 `json:"site_id"` + GUID string `json:"guid"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + CreatedAt time.Time `json:"created_at"` +} +``` + +- `slug` is auto-generated from `name` (e.g. "Go Programming" -> `go-programming`), editable by the user. +- `description` is Markdown, rendered on the category archive page. Defaults to empty string. + +## Admin UI + +### Category Management Page + +Route: `/sites/:siteID/categories` + +- Lists all categories for the site showing name, slug, and post count. +- "New category" button navigates to a create/edit form. +- Edit form fields: Name, Slug (auto-generated but editable), Description (Markdown textarea). +- Delete button with confirmation. Deletes the category and its post associations; does not delete the posts. + +Handler: `CategoriesHandler` (new, in `handlers/categories.go`). +Templates: `views/categories/index.html`, `views/categories/edit.html`. + +### Post Edit Form Changes + +- A multi-select checkbox list of all available categories, displayed in a **right sidebar** alongside the main title/body editing area on the left. +- Selected category IDs sent with the form submission. +- `CreatePostParams` gains `CategoryIDs []int64`. + +### Post List (Admin) + +- Category names shown as small labels next to each post title. + +## Static Site Output + +### Category Index Page (`/categories/`) + +Lists all categories that have at least one published post. For each category: + +- Category name as a clickable link to the archive page +- Post count +- First sentence/line of the description as a brief excerpt + +### Category Archive Pages (`/categories//`) + +- Category name as heading +- Full Markdown description rendered below the heading +- List of published posts in the category, ordered by `published_at` descending + +### Post Pages + +Each post page displays its category names as clickable links to the corresponding category archive pages. + +### Feeds + +Per-category feeds: +- `/categories//feed.xml` (RSS) +- `/categories//feed.json` (JSON Feed) + +Main site feeds (`/feed.xml`, `/feed.json`) gain category metadata on each post entry. + +### Empty Category Handling + +Categories with no published posts are hidden from the published site: no index entry, no archive page, no feed generated. They remain visible and manageable in the admin UI. + +## SQL Queries + +New file: `sql/queries/categories.sql` + +- `SelectCategoriesOfSite` — all categories for a site, ordered by name +- `SelectCategory` — single category by ID +- `SelectCategoryByGUID` — single category by GUID +- `SelectCategoriesOfPost` — categories for a given post (via join table) +- `SelectPostsOfCategory` — published, non-deleted posts in a category, ordered by `published_at` desc +- `CountPostsOfCategory` — count of published posts per category +- `InsertCategory` / `UpdateCategory` / `DeleteCategory` — CRUD +- `InsertPostCategory` / `DeletePostCategory` — manage the join table +- `DeletePostCategoriesByPost` — clear all categories for a post (delete-then-reinsert on save) + +## Service Layer + +### New `services/categories` Package + +`Service` struct with methods: + +- `ListCategories(ctx) ([]Category, error)` — all categories for the current site (from context) +- `GetCategory(ctx, id) (*Category, error)` +- `CreateCategory(ctx, params) (*Category, error)` — auto-generates slug from name +- `UpdateCategory(ctx, params) (*Category, error)` +- `DeleteCategory(ctx, id) error` — deletes category and post associations, queues site rebuild + +### Changes to `services/posts` + +- `UpdatePost` — after saving the post, deletes existing `post_categories` rows and re-inserts for the selected category IDs +- `GetPost` / `ListPosts` — loads each post's categories for admin display + +### Changes to Publishing Pipeline + +- `pubmodel.Site` gains new fields: + - Category list (with post counts and description excerpts for the index page) + - A function to iterate published posts by category +- `sitebuilder.Builder.BuildSite` gains additional goroutines for: + - Rendering the category index page + - Rendering each category archive page + - Rendering per-category feeds +- New templates: `tmplNameCategoryList`, `tmplNameCategorySingle` + +### Rebuild Triggers + +Saving or deleting a category queues a site rebuild, same as post state changes. + +## DB Provider + +`providers/db/` gains wrapper methods for all new sqlc queries, following the same pattern as existing post methods (e.g. `SaveCategory`, `SelectCategoriesOfPost`, etc.).