Files
summercms-initial-research/.planning/phases/10-core-plugins/10-03-PLAN.md
Jakub Zych 4d8d5719d5 docs(10): create phase plan
Phase 10: Core Plugins
- 4 plans in 2 waves
- Wave 1: 10-01 (User auth), 10-03 (Blog posts) - parallel
- Wave 2: 10-02 (User profiles), 10-04 (Blog categories/tags) - sequential
- Ready for execution
2026-02-05 16:07:32 +01:00

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
10-core-plugins 03 execute 1
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
true
truths artifacts key_links
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
path provides contains
plugins/golem15/blog/models/BlogPost.scala Blog post domain model case class BlogPost
path provides exports
plugins/golem15/blog/services/BlogPostService.scala Blog post CRUD operations
BlogPostService
create
update
findBySlug
path provides exports
plugins/golem15/blog/controllers/Posts.scala Admin controller for post management
PostsController
path provides contains
plugins/golem15/blog/resources/controllers/posts/fields.yaml YAML-driven form definition content:
path provides contains
plugins/golem15/blog/resources/db/migration/V10_2_1__blog_posts.sql Blog posts table schema CREATE TABLE blog_posts
from to via pattern
plugins/golem15/blog/controllers/Posts.scala BlogPostService ZIO service injection ZIO.service[BlogPostService]
from to via pattern
plugins/golem15/blog/services/BlogPostService.scala BlogPostRepository data access postRepo.
from to via pattern
plugins/golem15/blog/services/ContentService.scala commonmark Markdown parsing 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

<execution_context> @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md </execution_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

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 <!-- 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
./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

<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>
After completion, create `.planning/phases/10-core-plugins/10-03-SUMMARY.md`