diff --git a/docs/superpowers/specs/2026-03-18-categories-design.md b/docs/superpowers/specs/2026-03-18-categories-design.md index eb5a004..9c10abb 100644 --- a/docs/superpowers/specs/2026-03-18-categories-design.md +++ b/docs/superpowers/specs/2026-03-18-categories-design.md @@ -17,6 +17,7 @@ CREATE TABLE categories ( slug TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE ); CREATE INDEX idx_categories_site ON categories (site_id); @@ -44,11 +45,13 @@ type Category struct { Slug string `json:"slug"` Description string `json:"description"` CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_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. +- DB provider must use the existing `timeToInt()`/`time.Unix()` helpers for timestamp conversion, consistent with how posts are handled. ## Admin UI @@ -66,7 +69,7 @@ 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. +- A multi-select checkbox list of all available categories (sorted alphabetically by name), 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`. @@ -114,8 +117,8 @@ New file: `sql/queries/categories.sql` - `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 +- `SelectPostsOfCategory` — published, non-deleted posts in a category (`state = 0 AND deleted_at = 0`), ordered by `published_at` desc +- `CountPostsOfCategory` — count of published posts per category (same `state = 0 AND deleted_at = 0` filter) - `InsertCategory` / `UpdateCategory` / `DeleteCategory` — CRUD - `InsertPostCategory` / `DeletePostCategory` — manage the join table - `DeletePostCategoriesByPost` — clear all categories for a post (delete-then-reinsert on save) @@ -128,25 +131,28 @@ New file: `sql/queries/categories.sql` - `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)` +- `CreateCategory(ctx, params) (*Category, error)` — auto-generates slug from name. If the slug collides with an existing one for the same site, return a validation error. +- `UpdateCategory(ctx, params) (*Category, error)` — same slug collision check on update. - `DeleteCategory(ctx, id) error` — deletes category and post associations, queues site rebuild +All mutation methods verify site ownership (same pattern as post service authorization checks). + ### Changes to `services/posts` -- `UpdatePost` — after saving the post, deletes existing `post_categories` rows and re-inserts for the selected category IDs +- `UpdatePost` — after saving the post, deletes existing `post_categories` rows and re-inserts for the selected category IDs. The post save and category reassignment must run within a single database transaction to ensure atomicity. - `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 + - `Categories []CategoryWithCount` — category list with post counts and description excerpts for the index page + - `PostIterByCategory func(ctx context.Context, categoryID int64) iter.Seq[models.Maybe[*models.Post]]` — iterator for posts in a specific 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` +- New templates: `tmplNameCategoryList`, `tmplNameCategorySingle` (must be added to the `ParseFS` call in `sitebuilder.New()`) +- `postSingleData` gains a `Categories []Category` field so post templates can render category links ### Rebuild Triggers @@ -155,3 +161,9 @@ 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.). + +## Design Decisions + +- **Hard delete for categories** — unlike posts which use soft-delete, categories are hard-deleted. They are simpler entities and don't need a trash/restore workflow. +- **No sort_order column** — categories are sorted alphabetically by name. Manual ordering can be added later if needed. +- **Existing microblog-crosspost feed** — kept as-is. Per-category feeds are a separate, additive feature.