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
14 KiB
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 |
|
true |
|
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.mdReference 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>