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
This commit is contained in:
421
.planning/phases/10-core-plugins/10-03-PLAN.md
Normal file
421
.planning/phases/10-core-plugins/10-03-PLAN.md
Normal file
@@ -0,0 +1,421 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user