--- 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/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/BlogPostService.scala - plugins/golem15/blog/controllers/Categories.scala - plugins/golem15/blog/components/Posts.scala - plugins/golem15/blog/components/Post.scala - plugins/golem15/blog/components/Categories.scala - plugins/golem15/blog/components/RelatedPosts.scala - plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql - plugins/golem15/blog/resources/controllers/categories/fields.yaml - plugins/golem15/blog/resources/views/posts/default.peb - plugins/golem15/blog/resources/views/posts/item.peb - plugins/golem15/blog/resources/views/post/default.peb autonomous: true must_haves: truths: - "Blog posts can be assigned to categories" - "Categories are hierarchical (parent/child)" - "Blog posts can have multiple tags" - "Frontend displays post listings with pagination" - "Frontend displays individual post pages" - "Related posts shown based on 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/components/Posts.scala" provides: "Frontend post listing component" exports: ["PostsComponent", "onLoadMore"] - path: "plugins/golem15/blog/components/Post.scala" provides: "Single post display component" exports: ["PostComponent"] - 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/components/Posts.scala" to: "BlogPostService" via: "post listing" pattern: "postService\\.listFrontend" - 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\\." --- Complete the Blog plugin with categories, tags, and frontend listing components. Purpose: Enable content organization and frontend display, demonstrating hierarchical data structures and frontend component patterns. Output: Complete Blog plugin with: - Hierarchical categories using nested set model - Tags for free-form labeling - Frontend Posts component with pagination/infinite scroll - Frontend Post component for single post display - Related posts 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 3 component patterns for HTMX 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 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. Task 3: Frontend listing components plugins/golem15/blog/components/Posts.scala plugins/golem15/blog/components/Post.scala plugins/golem15/blog/components/Categories.scala plugins/golem15/blog/components/RelatedPosts.scala plugins/golem15/blog/resources/views/posts/default.peb plugins/golem15/blog/resources/views/posts/item.peb plugins/golem15/blog/resources/views/post/default.peb plugins/golem15/blog/resources/views/categories/default.peb plugins/golem15/blog/resources/views/relatedposts/default.peb plugins/golem15/blog/Plugin.scala (modify) Create frontend components for blog display: **Posts.scala (research Pattern 8):** - Extend SummerComponent trait - ComponentDetails: name "Posts", description "Blog post listing" - Property schema: - pageNumber: String, URL param for pagination, default "{{ :page }}" - categoryFilter: String, filter by category slug - postsPerPage: String, default "10", validation 1-100 - noPostsMessage: String, default "No posts found." - sortOrder: Dropdown with BlogPost.allowedSortingOptions - throwNotFound: Checkbox, 404 on empty results - Page variables: posts (PaginatedResult), category (Option), noPostsMessage - onRun lifecycle: - Load category if categoryFilter set - Build PostListOptions from properties - Call postService.listFrontend or listByCategory - Handle empty results per throwNotFound - Set page context variables - onLoadMore HTMX handler: - Parse page number from form - Load next page of posts - Return items partial - Trigger "postsExhausted" when no more **Post.scala component:** - ComponentDetails: name "Post", description "Single post display" - Property schema: - slug: String, URL param for post slug, default "{{ :slug }}" - notFoundMessage: String - Page variables: post (BlogPostWithRelations) - onRun lifecycle: - Get slug from URL - Call postService.findBySlug - 404 if not found - Set page context: post, author, categories, tags, readingTime - Set OpenGraph meta tags for social sharing **Categories.scala component:** - ComponentDetails: name "Categories", description "Category listing/tree" - Property schema: - displayMode: Dropdown (tree/flat), default "tree" - showPostCount: Checkbox, default true - includeEmpty: Checkbox, show categories with no posts - Page variables: categories (tree or flat list) - onRun: Load category tree, filter empty if needed **RelatedPosts.scala component:** - ComponentDetails: name "RelatedPosts", description "Related posts by category/tag" - Property schema: - limit: String, max posts to show, default "3" - postSlug: String, current post slug (from context) - Page variables: relatedPosts (List) - onRun: Call postService.getRelatedPosts, set context **Templates:** **posts/default.peb (research code example):** ```html
{% if posts.items is empty %}
{{ noPostsMessage }}
{% else %}
{% for post in posts.items %} {% include "posts/item" %} {% endfor %}
{% if posts.hasNext %}
{% endif %} {% endif %}
``` **posts/item.peb:** - Article element with post data - Featured image (lazy load) - Title as link to post page - Excerpt or auto-generated summary - Author name, published date, reading time - Category and tag links **post/default.peb:** - Full post display - OpenGraph meta tags in head - Title, featured image, author, date - Content HTML - Categories and tags - Share buttons (copy link, Twitter, Facebook) - Include RelatedPosts component placeholder **categories/default.peb:** - Tree or flat list based on displayMode - Category name as link - Post count if showPostCount **relatedposts/default.peb:** - Grid of related post cards - Image, title, excerpt snippet **Register all components in Plugin.scala boot method**
./mill summercms.compile succeeds Components registered in Plugin.scala Templates have valid Pebble syntax HTMX attributes properly formatted Posts component displays paginated listings with infinite scroll. Post component displays single post with meta tags. Categories component shows category tree/list. RelatedPosts component shows related content. All templates render with HTMX interactions.
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 component loads and paginates 5. Post component displays with OpenGraph meta 6. 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 - Frontend /blog shows post listing with load more - Frontend /blog/:slug shows full post with related posts - Category pages filter posts by category and children - Tag pages filter posts by tag After completion, create `.planning/phases/10-core-plugins/10-04-SUMMARY.md`