---
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\\."
---

<objective>
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
</objective>

<execution_context>
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
@/home/jin/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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
</context>

<tasks>

<task type="auto">
  <name>Task 1: Category and tag models with services</name>
  <files>
    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
  </files>
  <action>
    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
  </action>
  <verify>
    ./mill summercms.compile succeeds
    NestedTree rebuild produces valid left/right sequence
    SQL migration syntax valid
  </verify>
  <done>
    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.
  </done>
</task>

<task type="auto">
  <name>Task 2: Update BlogPostService for relations</name>
  <files>
    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
  </files>
  <action>
    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**
  </action>
  <verify>
    ./mill summercms.compile succeeds
    Related posts algorithm returns posts with shared categories/tags
    Category tree drag-drop works in admin
  </verify>
  <done>
    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.
  </done>
</task>

<task type="auto">
  <name>Task 3: Frontend listing components</name>
  <files>
    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)
  </files>
  <action>
    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
    <div class="blog-posts" id="posts-{{ __SELF__ }}">
      {% if posts.items is empty %}
        <div class="no-posts">{{ noPostsMessage }}</div>
      {% else %}
        <div class="posts-list">
          {% for post in posts.items %}
            {% include "posts/item" %}
          {% endfor %}
        </div>

        {% if posts.hasNext %}
          <div class="load-more">
            <button
              hx-post="{{ componentHandler('onLoadMore') }}"
              hx-vals='{"page": {{ posts.currentPage + 1 }}}'
              hx-target="#posts-{{ __SELF__ }} .posts-list"
              hx-swap="beforeend"
              hx-indicator=".htmx-indicator">
              Load More
              <span class="htmx-indicator">Loading...</span>
            </button>
          </div>
        {% endif %}
      {% endif %}
    </div>
    ```

    **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**
  </action>
  <verify>
    ./mill summercms.compile succeeds
    Components registered in Plugin.scala
    Templates have valid Pebble syntax
    HTMX attributes properly formatted
  </verify>
  <done>
    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.
  </done>
</task>

</tasks>

<verification>
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
</verification>

<success_criteria>
- 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
</success_criteria>

<output>
After completion, create `.planning/phases/10-core-plugins/10-04-SUMMARY.md`
</output>
