--- 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" --- 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 @/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 # 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 Task 1: Blog post model and database schema 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 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 ./mill summercms.compile succeeds with new dependencies SQL migration syntax valid BlogPost model has Quill mappings Blog plugin structure created with manifest. BlogPost model ready with all fields. Migration creates blog_posts table. Dependencies added for slug generation and Markdown. Task 2: Post service with content processing plugins/golem15/blog/repositories/BlogPostRepository.scala plugins/golem15/blog/services/BlogPostService.scala plugins/golem15/blog/services/ContentService.scala 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 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 ./mill summercms.compile succeeds ContentService renders Markdown correctly BlogPostService generates slugs from titles Sanitization strips dangerous HTML 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. Task 3: Admin controller with TinyMCE editor 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 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** ./mill summercms.compile succeeds npm install succeeds with TinyMCE YAML files parse correctly Controller registered in plugin 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. 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 - 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 After completion, create `.planning/phases/10-core-plugins/10-03-SUMMARY.md`