---
phase: 10-core-plugins
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/golem15/blog/plugin.yaml
  - plugins/golem15/blog/Plugin.scala
  - plugins/golem15/blog/models/BlogPost.scala
  - plugins/golem15/blog/models/BlogSettings.scala
  - plugins/golem15/blog/repositories/BlogPostRepository.scala
  - plugins/golem15/blog/services/BlogPostService.scala
  - plugins/golem15/blog/services/ContentService.scala
  - plugins/golem15/blog/controllers/Posts.scala
  - plugins/golem15/blog/resources/db/migration/V10_2_1__blog_posts.sql
  - plugins/golem15/blog/resources/controllers/posts/fields.yaml
  - plugins/golem15/blog/resources/controllers/posts/columns.yaml
  - plugins/golem15/blog/resources/views/posts/form.peb
  - build.mill
  - package.json
autonomous: true

must_haves:
  truths:
    - "Admin can create a new blog post"
    - "Admin can edit existing blog posts"
    - "Blog post has title, slug, content with WYSIWYG editor"
    - "Blog post content supports both Markdown and HTML"
    - "Blog posts have published/draft state with scheduled publishing"
    - "Blog posts can be marked as featured"
  artifacts:
    - path: "plugins/golem15/blog/models/BlogPost.scala"
      provides: "Blog post domain model"
      contains: "case class BlogPost"
    - path: "plugins/golem15/blog/services/BlogPostService.scala"
      provides: "Blog post CRUD operations"
      exports: ["BlogPostService", "create", "update", "findBySlug"]
    - path: "plugins/golem15/blog/controllers/Posts.scala"
      provides: "Admin controller for post management"
      exports: ["PostsController"]
    - path: "plugins/golem15/blog/resources/controllers/posts/fields.yaml"
      provides: "YAML-driven form definition"
      contains: "content:"
    - path: "plugins/golem15/blog/resources/db/migration/V10_2_1__blog_posts.sql"
      provides: "Blog posts table schema"
      contains: "CREATE TABLE blog_posts"
  key_links:
    - from: "plugins/golem15/blog/controllers/Posts.scala"
      to: "BlogPostService"
      via: "ZIO service injection"
      pattern: "ZIO\\.service\\[BlogPostService\\]"
    - from: "plugins/golem15/blog/services/BlogPostService.scala"
      to: "BlogPostRepository"
      via: "data access"
      pattern: "postRepo\\."
    - from: "plugins/golem15/blog/services/ContentService.scala"
      to: "commonmark"
      via: "Markdown parsing"
      pattern: "Parser\\.builder"
---

<objective>
Create the Blog plugin foundation with post management and WYSIWYG editor integration.

Purpose: Establish blog content management as the second core plugin, demonstrating admin CRUD patterns with YAML-driven forms.

Output: Working Blog plugin with:
- BlogPost model with content, publishing state, and metadata
- Admin controller for post CRUD
- TinyMCE WYSIWYG editor integration
- Markdown and HTML content support
- Draft/published state with scheduled publishing
</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

# Reference patterns from prior phases
# Phase 2: Plugin manifest and lifecycle patterns
# Phase 7: Admin forms with YAML definitions
# Phase 9: Media library for image uploads in editor
</context>

<tasks>

<task type="auto">
  <name>Task 1: Blog post model and database schema</name>
  <files>
    plugins/golem15/blog/plugin.yaml
    plugins/golem15/blog/Plugin.scala
    plugins/golem15/blog/models/BlogPost.scala
    plugins/golem15/blog/models/BlogSettings.scala
    plugins/golem15/blog/resources/db/migration/V10_2_1__blog_posts.sql
    build.mill
  </files>
  <action>
    Create Blog plugin structure and post model:

    **build.mill additions:**
    - Add slugify library: mvn"com.github.slugify:slugify:3.0.7"
    - Add commonmark: mvn"org.commonmark:commonmark:0.22.0"

    **plugin.yaml:**
    - name: Golem15.Blog
    - description: Blog posts with categories and tags
    - author: Golem15
    - version: 1.0.0
    - require: [] (no plugin dependencies for posts)

    **Plugin.scala:**
    - Extend SummerPlugin trait
    - Register controllers: Posts (Categories in plan 04)
    - Register components: Posts, Post (in plan 04)
    - Register settings model
    - Boot method for initialization

    **BlogPost.scala (research Pattern 5):**
    - Case class with fields:
      id, authorId (BackendUser reference), title, slug (unique), excerpt (Option),
      content (raw Markdown/HTML), contentHtml (rendered), charactersCount,
      published (Boolean), publishedAt (Option[Instant]), isFeatured,
      mainCategoryId (Option, for plan 04), metadata (Map[String, Json]),
      createdAt, updatedAt
    - Validation rules: title required, slug required + unique + pattern, content required
    - allowedSortingOptions map (title asc/desc, created_at, published_at, random)
    - Quill table mapping to `blog_posts`

    **BlogSettings.scala:**
    - Case class BlogPluginSettings:
      postsPerPage (Int, default 10), defaultSorting (String),
      urlPattern (String, e.g., "/blog/:slug"), enableComments (Boolean, false),
      excerptLength (Int, default 200)
    - Settings YAML schema definition

    **V10_2_1__blog_posts.sql:**
    - CREATE TABLE blog_posts with all fields from research:
      id BIGSERIAL PRIMARY KEY
      author_id BIGINT NOT NULL REFERENCES backend_users(id)
      title VARCHAR(255) NOT NULL
      slug VARCHAR(255) NOT NULL UNIQUE
      excerpt TEXT
      content TEXT NOT NULL
      content_html TEXT NOT NULL
      characters_count INT DEFAULT 0
      published BOOLEAN DEFAULT false
      published_at TIMESTAMPTZ
      is_featured BOOLEAN DEFAULT false
      main_category_id BIGINT (FK added in plan 04)
      metadata JSONB DEFAULT '{}'
      created_at, updated_at TIMESTAMPTZ
    - Indexes on slug, published+published_at, author_id, is_featured
  </action>
  <verify>
    ./mill summercms.compile succeeds with new dependencies
    SQL migration syntax valid
    BlogPost model has Quill mappings
  </verify>
  <done>
    Blog plugin structure created with manifest.
    BlogPost model ready with all fields.
    Migration creates blog_posts table.
    Dependencies added for slug generation and Markdown.
  </done>
</task>

<task type="auto">
  <name>Task 2: Post service with content processing</name>
  <files>
    plugins/golem15/blog/repositories/BlogPostRepository.scala
    plugins/golem15/blog/services/BlogPostService.scala
    plugins/golem15/blog/services/ContentService.scala
  </files>
  <action>
    Create service layer for post operations:

    **BlogPostRepository.scala:**
    - Trait with ZIO effects following repository pattern:
      - findById(id): Option[BlogPost]
      - findBySlug(slug): Option[BlogPost]
      - create(post): BlogPost
      - update(post): BlogPost
      - delete(id): Unit (hard delete or soft)
      - listPublished(options): PaginatedResult[BlogPost]
      - listAll(options): PaginatedResult[BlogPost] (admin)
      - countByAuthor(authorId): Int
    - Live implementation using QuillContext
    - Published filter: published = true AND published_at <= NOW()

    **ContentService.scala:**
    - Trait for content processing:
      - renderMarkdown(content): String (Markdown to HTML)
      - sanitizeHtml(html): String (XSS protection)
      - generateExcerpt(contentHtml, maxLength): String
      - calculateReadingTime(contentHtml): Int (minutes)
      - countCharacters(content): Int
    - Live implementation:
      - Use CommonMark parser for Markdown (research Pattern 5)
      - Use Jsoup with Safelist.relaxed() for sanitization
      - Excerpt: Split on <!-- more --> marker or truncate to maxLength
      - Reading time: Strip HTML, count words, divide by 200 WPM, round up

    **BlogPostService.scala (research Pattern 5):**
    - Trait:
      - create(data: PostCreateData): BlogPost
      - update(id, data: PostUpdateData): BlogPost
      - delete(id): Unit
      - findBySlug(slug): Option[BlogPostWithRelations]
      - listFrontend(options: PostListOptions): PaginatedResult[BlogPostWithRelations]
      - publish(id): BlogPost (sets published=true, publishedAt=now if null)
      - unpublish(id): BlogPost (sets published=false)
    - PostCreateData case class: title, slug (Option - auto-generate if empty),
      content, excerpt (Option), published, publishedAt (Option), isFeatured, authorId
    - PostUpdateData case class: title, slug, content, excerpt, published, publishedAt, isFeatured
    - PostListOptions from research: page, perPage, sort, published, exceptPost, exceptCategories, search
    - Before save:
      - If slug empty, generate from title using Slugify
      - Render content to contentHtml using ContentService
      - Calculate charactersCount
      - Sanitize HTML content
    - BlogPostWithRelations case class for joins (categories, tags, author in plan 04)
    - Live implementation with ZLayer
  </action>
  <verify>
    ./mill summercms.compile succeeds
    ContentService renders Markdown correctly
    BlogPostService generates slugs from titles
    Sanitization strips dangerous HTML
  </verify>
  <done>
    BlogPostRepository provides data access.
    ContentService handles Markdown rendering and XSS sanitization.
    BlogPostService manages CRUD with auto-slug and content processing.
    Reading time and character count auto-calculated.
  </done>
</task>

<task type="auto">
  <name>Task 3: Admin controller with TinyMCE editor</name>
  <files>
    plugins/golem15/blog/controllers/Posts.scala
    plugins/golem15/blog/resources/controllers/posts/fields.yaml
    plugins/golem15/blog/resources/controllers/posts/columns.yaml
    plugins/golem15/blog/resources/views/posts/form.peb
    plugins/golem15/blog/resources/views/posts/list.peb
    package.json
  </files>
  <action>
    Create admin controller for post management:

    **package.json additions:**
    - Add TinyMCE: "tinymce": "^7.0.0"

    **Posts.scala controller:**
    - Extend AdminController (Phase 7 pattern)
    - Configure: modelClass = BlogPost, listUrl = "/admin/blog/posts", formUrl = "/admin/blog/posts/:id/edit"
    - Actions:
      - index: List posts using BlogPostService.listAll, render list.peb
      - create: New post form
      - store: Parse form, call BlogPostService.create, redirect to edit
      - edit: Load post by ID, render form.peb
      - update: Parse form, call BlogPostService.update, return success response
      - delete: Call BlogPostService.delete, redirect to list
      - publish/unpublish: Toggle published state
    - HTMX handlers for inline actions (delete confirmation, publish toggle)

    **fields.yaml (Phase 7 form definition):**
    ```yaml
    fields:
      title:
        type: text
        label: Title
        required: true
        span: full

      slug:
        type: text
        label: Slug
        span: auto
        comment: "Leave blank to auto-generate from title"
        preset:
          field: title
          type: slug

      published:
        type: checkbox
        label: Published
        span: auto

      published_at:
        type: datepicker
        label: Publish Date
        mode: datetime
        span: auto
        dependsOn: published

      is_featured:
        type: checkbox
        label: Featured Post
        span: auto

      content:
        type: richeditor
        label: Content
        size: huge
        span: full

      excerpt:
        type: textarea
        label: Excerpt
        size: small
        span: full
        comment: "Leave blank to auto-generate from content"

    tabs:
      fields:
        tab: Content
      _categories:
        tab: Categories
      _tags:
        tab: Tags
    ```

    **columns.yaml:**
    ```yaml
    columns:
      title:
        label: Title
        sortable: true

      slug:
        label: Slug
        sortable: true

      author:
        label: Author
        relation: author
        select: name

      published:
        label: Status
        type: switch

      published_at:
        label: Published
        type: datetime
        sortable: true

      updated_at:
        label: Updated
        type: datetime
        sortable: true
    ```

    **form.peb template:**
    - Form structure following Phase 7 patterns
    - Include TinyMCE initialization script for content field:
      ```javascript
      initBlogEditor('#Form-field-Post-content');
      ```
    - Reference research Pattern 9 for TinyMCE config
    - Image upload handler pointing to media library endpoint
    - Save button with HTMX submit
    - Publish/Unpublish button

    **list.peb template:**
    - List structure following Phase 7 patterns
    - Columns from columns.yaml
    - Row actions: Edit, Delete, Publish/Unpublish
    - Bulk actions: Delete selected, Publish/Unpublish selected
    - Filters: published, author, date range

    **Register controller in Plugin.scala**
  </action>
  <verify>
    ./mill summercms.compile succeeds
    npm install succeeds with TinyMCE
    YAML files parse correctly
    Controller registered in plugin
  </verify>
  <done>
    Admin can access /admin/blog/posts to list posts.
    Create/edit forms render with TinyMCE editor.
    Posts can be saved, published, unpublished, deleted.
    List shows posts with sorting and filtering.
  </done>
</task>

</tasks>

<verification>
After all tasks complete:
1. Blog plugin compiles: `./mill summercms.compile`
2. Dependencies installed: `npm install` includes TinyMCE
3. Migration creates blog_posts table: Check SQL syntax
4. Controller routes registered: Posts controller in Plugin.scala
5. Form fields render TinyMCE: richeditor type maps to TinyMCE
</verification>

<success_criteria>
- Admin can navigate to /admin/blog/posts and see post list
- Create post form shows title, slug, content editor, excerpt, publish options
- TinyMCE editor loads for content field with image upload
- Saving post generates slug from title if empty
- Content rendered to HTML with Markdown support
- Published posts have published_at timestamp
- Featured flag can be toggled
</success_criteria>

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