--- phase: 10-core-plugins plan: 04 type: execute wave: 2 depends_on: ["10-03"] files_modified: - plugins/golem15/blog/models/Category.scala - plugins/golem15/blog/models/Tag.scala - plugins/golem15/blog/models/PostCategory.scala - plugins/golem15/blog/models/PostTag.scala - plugins/golem15/blog/repositories/CategoryRepository.scala - plugins/golem15/blog/repositories/TagRepository.scala - plugins/golem15/blog/services/CategoryService.scala - plugins/golem15/blog/services/TagService.scala - plugins/golem15/blog/services/NestedTree.scala - plugins/golem15/blog/services/BlogPostService.scala - plugins/golem15/blog/repositories/BlogPostRepository.scala - plugins/golem15/blog/controllers/Posts.scala - plugins/golem15/blog/controllers/Categories.scala - plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql - plugins/golem15/blog/resources/controllers/posts/fields.yaml - plugins/golem15/blog/resources/controllers/categories/fields.yaml - plugins/golem15/blog/resources/controllers/categories/columns.yaml autonomous: true must_haves: truths: - "Blog posts can be assigned to categories" - "Categories are hierarchical (parent/child)" - "Blog posts can have multiple tags" - "Admin can manage categories in tree view" - "Related posts calculated by shared categories/tags" artifacts: - path: "plugins/golem15/blog/models/Category.scala" provides: "Hierarchical category model with nested set" contains: "case class Category" - path: "plugins/golem15/blog/models/Tag.scala" provides: "Tag model for free-form labeling" contains: "case class Tag" - path: "plugins/golem15/blog/services/CategoryService.scala" provides: "Category CRUD with tree operations" exports: ["CategoryService", "getTree", "getAllChildrenAndSelf"] - path: "plugins/golem15/blog/controllers/Categories.scala" provides: "Admin controller for category management" exports: ["CategoriesController"] - path: "plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql" provides: "Categories, tags, and pivot tables" contains: "CREATE TABLE blog_categories" key_links: - from: "plugins/golem15/blog/services/BlogPostService.scala" to: "CategoryService" via: "category filtering" pattern: "categoryService\\.getAllChildrenAndSelf" - from: "plugins/golem15/blog/services/CategoryService.scala" to: "NestedTree" via: "tree operations" pattern: "NestedTree\\." - from: "plugins/golem15/blog/controllers/Categories.scala" to: "CategoryService" via: "CRUD operations" pattern: "categoryService\\." --- Add categories and tags to the Blog plugin with admin management. Purpose: Enable content organization with hierarchical categories and free-form tags, providing the data layer for frontend components. Output: Blog plugin extended with: - Hierarchical categories using nested set model - Tags for free-form labeling - Admin controller for category management - BlogPostService updated with category/tag relations - Related posts algorithm based on shared categories/tags @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/10-core-plugins/10-CONTEXT.md @.planning/phases/10-core-plugins/10-RESEARCH.md # Prior plan in this phase @.planning/phases/10-core-plugins/10-03-SUMMARY.md # Reference: Phase 7 admin controller patterns Task 1: Category and tag models with services plugins/golem15/blog/models/Category.scala plugins/golem15/blog/models/Tag.scala plugins/golem15/blog/models/PostCategory.scala plugins/golem15/blog/models/PostTag.scala plugins/golem15/blog/repositories/CategoryRepository.scala plugins/golem15/blog/repositories/TagRepository.scala plugins/golem15/blog/services/CategoryService.scala plugins/golem15/blog/services/TagService.scala plugins/golem15/blog/services/NestedTree.scala plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql Create category and tag models with nested tree support: **Category.scala (research Pattern 6):** - Case class with fields: id, name, slug (unique), description (Option), parentId (Option), nestLeft (Int), nestRight (Int), nestDepth (Int), published, publishedAt, createdAt, updatedAt - Quill table mapping to `blog_categories` - CategoryTreeNode case class: category, children (List), postCount **Tag.scala (research Pattern 7):** - Case class: id, name, slug (unique), createdAt, updatedAt - Quill table mapping to `blog_tags` - TagWithCount case class: tag, postCount **PostCategory.scala & PostTag.scala:** - Pivot case classes for many-to-many relationships - PostCategory: postId, categoryId - PostTag: postId, tagId **NestedTree.scala (utility object):** - Functions for nested set operations: - rebuild(categories: List[Category]): List[Category] - Recalculate left/right - descendants(category, all): List[Category] - Get all children - ancestors(category, all): List[Category] - Get all parents - moveNode(category, newParent, all): List[Category] - Move category in tree - insertNode(category, parent, all): List[Category] - Add new category - deleteNode(category, all): List[Category] - Remove category and adjust tree **CategoryRepository.scala:** - Trait: - findById(id): Option[Category] - findBySlug(slug): Option[Category] - listAll: List[Category] - create(category): Category - update(category): Category - delete(id): Unit - getPostCount(categoryId): Int - getNestedPostCount(categoryId): Int (includes children) - Live implementation using QuillContext **TagRepository.scala:** - Trait: - findById(id): Option[Tag] - findBySlug(slug): Option[Tag] - findByName(name): Option[Tag] - listAll: List[Tag] - create(tag): Tag - listForPost(postId): List[Tag] - getPopular(limit): List[TagWithCount] - Live implementation **CategoryService.scala:** - Trait: - create(data): Category (auto-generate slug, rebuild tree) - update(id, data): Category (rebuild tree if parent changed) - delete(id): Unit (rebuild tree after) - findBySlug(slug): Option[Category] - listAll: List[Category] - getTree: List[CategoryTreeNode] - getChildren(parentId): List[Category] - getAllChildrenAndSelf(categoryId): List[Category] (for nested queries) - getPostCount(categoryId): Int - getNestedPostCount(categoryId): Int - Live implementation with NestedTree for tree operations **TagService.scala:** - Trait: - findOrCreate(name): Tag (create with auto-slug if not exists) - findBySlug(slug): Option[Tag] - listAll: List[Tag] - listForPost(postId): List[Tag] - getPopularTags(limit): List[TagWithCount] - syncPostTags(postId, tagNames): List[Tag] (update pivot table) - Live implementation **V10_2_2__blog_categories_tags.sql:** - CREATE TABLE blog_categories (all fields, nested set indexes) - CREATE TABLE blog_tags (id, name, slug unique, timestamps) - CREATE TABLE blog_posts_categories (post_id, category_id, composite PK, FKs) - CREATE TABLE blog_posts_tags (post_id, tag_id, composite PK, FKs) - ALTER TABLE blog_posts ADD CONSTRAINT fk_main_category REFERENCES blog_categories - Indexes on slugs, nested set columns, pivot table lookups ./mill summercms.compile succeeds NestedTree rebuild produces valid left/right sequence SQL migration syntax valid Category model with nested set tree support. Tag model for free-form labeling. Services handle CRUD with automatic tree maintenance. Pivot tables link posts to categories and tags. Task 2: Update BlogPostService and admin for relations plugins/golem15/blog/services/BlogPostService.scala (modify) plugins/golem15/blog/repositories/BlogPostRepository.scala (modify) plugins/golem15/blog/controllers/Posts.scala (modify) plugins/golem15/blog/controllers/Categories.scala plugins/golem15/blog/resources/controllers/posts/fields.yaml (modify) plugins/golem15/blog/resources/controllers/categories/fields.yaml plugins/golem15/blog/resources/controllers/categories/columns.yaml Integrate categories and tags with blog posts: **BlogPostRepository.scala additions:** - findBySlugWithRelations(slug): Option[BlogPostWithRelations] - listFrontendWithRelations(options): PaginatedResult[BlogPostWithRelations] - listByCategory(categoryId, options): PaginatedResult[BlogPostWithRelations] - listByTag(tagSlug, options): PaginatedResult[BlogPostWithRelations] - getRelatedPosts(postId, limit): List[BlogPostWithRelations] - Join queries to load categories, tags, author in single query **BlogPostService.scala modifications:** - BlogPostWithRelations case class: post: BlogPost, categories: List[Category], tags: List[Tag], featuredImages: List[MediaItem], author: Option[BackendUser], readingTime: Int - Update create/update to handle categories and tags: - PostCreateData/UpdateData add: categoryIds (List[Long]), tagNames (List[String]) - On save: Sync pivot tables via services - Set mainCategoryId if categories provided - Update listFrontend to use listFrontendWithRelations - Add listByCategory(categoryId, options) - Add listByTag(tagSlug, options) - Add getRelatedPosts(postId, limit): - Score = 2 * shared_categories + 1 * shared_tags - Exclude current post - Return top N by score **Posts.scala controller modifications:** - Update store/update actions to handle categories and tags from form - Add category/tag selection to form context **Categories.scala controller (new):** - Admin controller for category management - Actions: index, create, store, edit, update, delete, reorder (tree) - HTMX handler for drag-drop tree reordering **fields.yaml modifications (Posts):** - Add categories tab fields: ```yaml _categories: type: relation label: Categories nameFrom: name list: true tree: true tab: Categories _tags: type: taglist label: Tags mode: array separator: ',' tab: Tags ``` **categories/fields.yaml:** ```yaml fields: name: type: text label: Name required: true slug: type: text label: Slug preset: field: name type: slug parent_id: type: relation label: Parent Category emptyOption: -- None -- nameFrom: name description: type: textarea label: Description size: small published: type: checkbox label: Published ``` **categories/columns.yaml:** ```yaml columns: name: label: Name sortable: true tree: true slug: label: Slug published: label: Published type: switch post_count: label: Posts type: number ``` **Register Categories controller in Plugin.scala** ./mill summercms.compile succeeds Related posts algorithm returns posts with shared categories/tags Category tree drag-drop works in admin Posts can be assigned categories and tags. Admin can manage categories in tree view. Related posts calculated by shared categories/tags. Post queries include relations efficiently. After all tasks complete: 1. Blog plugin compiles: `./mill summercms.compile` 2. Migration creates all tables: blog_categories, blog_tags, pivot tables 3. Category nested tree rebuilds correctly on insert/delete 4. Posts admin form shows category tree selector and tag input 5. Related posts algorithm returns relevant content - Categories form hierarchical tree structure - Nested set left/right values correct after operations - Tags can be created on-the-fly when typing - Post admin form shows category tree selector and tag input - Admin can manage categories with tree view and drag-drop - Related posts calculated by shared categories/tags After completion, create `.planning/phases/10-core-plugins/10-04-SUMMARY.md`