fix(10): revise plans based on checker feedback

- Plan 10-02: Added explicit UserMailService wiring in Account.scala
  onRegister handler for activation emails when activateMode == User
- Plan 10-04: Split into backend-only (models, services, admin controller)
  reducing from 16 to 15 files, estimated context ~50%
- Plan 10-05: New plan for frontend components (Posts, Post, Categories,
  RelatedPosts), Wave 3 depends on 10-04
- Updated ROADMAP.md to reflect 5 plans for Phase 10
This commit is contained in:
Jakub Zych
2026-02-05 16:13:31 +01:00
parent 4d8d5719d5
commit b05e85cd55
4 changed files with 338 additions and 186 deletions

View File

@@ -195,13 +195,14 @@ Plans:
6. Blog posts can be organized into categories 6. Blog posts can be organized into categories
7. Blog posts can be tagged with multiple tags 7. Blog posts can be tagged with multiple tags
8. Frontend displays blog post listings via components 8. Frontend displays blog post listings via components
**Plans**: 4 plans **Plans**: 5 plans
Plans: Plans:
- [ ] 10-01-PLAN.md - User plugin models, services, and authentication components - [ ] 10-01-PLAN.md - User plugin models, services, and authentication components
- [ ] 10-02-PLAN.md - User profile management and password reset flow - [ ] 10-02-PLAN.md - User profile management and password reset flow
- [ ] 10-03-PLAN.md - Blog post model, services, and admin controller with TinyMCE - [ ] 10-03-PLAN.md - Blog post model, services, and admin controller with TinyMCE
- [ ] 10-04-PLAN.md - Blog categories, tags, and frontend listing components - [ ] 10-04-PLAN.md - Blog categories, tags, and admin management
- [ ] 10-05-PLAN.md - Blog frontend listing components
## Progress ## Progress
@@ -219,7 +220,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10
| 7. Admin Forms & Lists | 0/3 | Planned | - | | 7. Admin Forms & Lists | 0/3 | Planned | - |
| 8. Admin Dashboard | 0/4 | Planned | - | | 8. Admin Dashboard | 0/4 | Planned | - |
| 9. Content Management | 0/7 | Planned | - | | 9. Content Management | 0/7 | Planned | - |
| 10. Core Plugins | 0/4 | Planned | - | | 10. Core Plugins | 0/5 | Planned | - |
--- ---
*Roadmap created: 2026-02-04* *Roadmap created: 2026-02-04*

View File

@@ -6,6 +6,7 @@ wave: 2
depends_on: ["10-01"] depends_on: ["10-01"]
files_modified: files_modified:
- plugins/golem15/user/components/ResetPassword.scala - plugins/golem15/user/components/ResetPassword.scala
- plugins/golem15/user/components/Account.scala
- plugins/golem15/user/services/FrontendUserService.scala - plugins/golem15/user/services/FrontendUserService.scala
- plugins/golem15/user/services/UserMailService.scala - plugins/golem15/user/services/UserMailService.scala
- plugins/golem15/user/resources/views/account/profile.peb - plugins/golem15/user/resources/views/account/profile.peb
@@ -155,6 +156,7 @@ Output: Working profile and reset features:
<name>Task 2: Password reset flow and email service</name> <name>Task 2: Password reset flow and email service</name>
<files> <files>
plugins/golem15/user/components/ResetPassword.scala plugins/golem15/user/components/ResetPassword.scala
plugins/golem15/user/components/Account.scala (modify)
plugins/golem15/user/services/UserMailService.scala plugins/golem15/user/services/UserMailService.scala
plugins/golem15/user/services/FrontendUserService.scala (modify) plugins/golem15/user/services/FrontendUserService.scala (modify)
plugins/golem15/user/resources/views/resetpassword/request.peb plugins/golem15/user/resources/views/resetpassword/request.peb
@@ -175,6 +177,13 @@ Output: Working profile and reset features:
- Send via configured SMTP or email service - Send via configured SMTP or email service
- MailError ADT: SendFailed, TemplateError, ConfigurationError - MailError ADT: SendFailed, TemplateError, ConfigurationError
**Account.scala modification (CRITICAL - wire activation email):**
- Modify the onRegister handler created in Plan 01 to integrate UserMailService:
- Inject UserMailService dependency into Account component
- After successful userService.register call, check UserSettings.activateMode
- If activateMode == ActivateMode.User, call mailService.sendActivationEmail(user, activationCode)
- This completes the key_link from Account.scala to UserMailService
**FrontendUserService.scala additions:** **FrontendUserService.scala additions:**
- requestPasswordReset(email: String): Unit - requestPasswordReset(email: String): Unit
- Find user by email (don't reveal if not found - timing attack) - Find user by email (don't reveal if not found - timing attack)
@@ -230,13 +239,14 @@ Output: Working profile and reset features:
<verify> <verify>
./mill summercms.compile succeeds ./mill summercms.compile succeeds
ResetPassword component registered in Plugin.scala ResetPassword component registered in Plugin.scala
Account.scala onRegister calls mailService.sendActivationEmail when activateMode == User
Email templates have valid Pebble syntax Email templates have valid Pebble syntax
Reset code generation uses SecureRandom Reset code generation uses SecureRandom
</verify> </verify>
<done> <done>
Password reset request sends email (or stubs if no SMTP). Password reset request sends email (or stubs if no SMTP).
Reset link with token allows setting new password. Reset link with token allows setting new password.
Activation email template ready for registration flow. Activation email sent during registration when activateMode is User.
No email enumeration possible (generic success messages). No email enumeration possible (generic success messages).
</done> </done>
</task> </task>
@@ -247,9 +257,10 @@ Output: Working profile and reset features:
After all tasks complete: After all tasks complete:
1. User plugin compiles: `./mill summercms.compile` 1. User plugin compiles: `./mill summercms.compile`
2. Account component has all handlers: onSignin, onRegister, onActivate, onUpdate, onUploadAvatar 2. Account component has all handlers: onSignin, onRegister, onActivate, onUpdate, onUploadAvatar
3. ResetPassword component handles: onRequest, onReset 3. Account.scala onRegister integrates UserMailService for activation emails
4. Email service can send activation and reset emails 4. ResetPassword component handles: onRequest, onReset
5. Templates render valid forms with HTMX 5. Email service can send activation and reset emails
6. Templates render valid forms with HTMX
</verification> </verification>
<success_criteria> <success_criteria>
@@ -257,6 +268,7 @@ After all tasks complete:
- Profile update saves name/surname changes - Profile update saves name/surname changes
- Password change requires current password confirmation - Password change requires current password confirmation
- Avatar upload stores image and updates user record - Avatar upload stores image and updates user record
- Registration with activateMode=User triggers activation email via UserMailService
- Password reset request always shows success (no enumeration) - Password reset request always shows success (no enumeration)
- Reset link with valid code allows new password entry - Reset link with valid code allows new password entry
- User automatically logged in after successful reset - User automatically logged in after successful reset

View File

@@ -7,21 +7,21 @@ depends_on: ["10-03"]
files_modified: files_modified:
- plugins/golem15/blog/models/Category.scala - plugins/golem15/blog/models/Category.scala
- plugins/golem15/blog/models/Tag.scala - plugins/golem15/blog/models/Tag.scala
- plugins/golem15/blog/models/PostCategory.scala
- plugins/golem15/blog/models/PostTag.scala
- plugins/golem15/blog/repositories/CategoryRepository.scala - plugins/golem15/blog/repositories/CategoryRepository.scala
- plugins/golem15/blog/repositories/TagRepository.scala - plugins/golem15/blog/repositories/TagRepository.scala
- plugins/golem15/blog/services/CategoryService.scala - plugins/golem15/blog/services/CategoryService.scala
- plugins/golem15/blog/services/TagService.scala - plugins/golem15/blog/services/TagService.scala
- plugins/golem15/blog/services/NestedTree.scala
- plugins/golem15/blog/services/BlogPostService.scala - plugins/golem15/blog/services/BlogPostService.scala
- plugins/golem15/blog/repositories/BlogPostRepository.scala
- plugins/golem15/blog/controllers/Posts.scala
- plugins/golem15/blog/controllers/Categories.scala - plugins/golem15/blog/controllers/Categories.scala
- plugins/golem15/blog/components/Posts.scala
- plugins/golem15/blog/components/Post.scala
- plugins/golem15/blog/components/Categories.scala
- plugins/golem15/blog/components/RelatedPosts.scala
- plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql - plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql
- plugins/golem15/blog/resources/controllers/posts/fields.yaml
- plugins/golem15/blog/resources/controllers/categories/fields.yaml - plugins/golem15/blog/resources/controllers/categories/fields.yaml
- plugins/golem15/blog/resources/views/posts/default.peb - plugins/golem15/blog/resources/controllers/categories/columns.yaml
- plugins/golem15/blog/resources/views/posts/item.peb
- plugins/golem15/blog/resources/views/post/default.peb
autonomous: true autonomous: true
must_haves: must_haves:
@@ -29,9 +29,8 @@ must_haves:
- "Blog posts can be assigned to categories" - "Blog posts can be assigned to categories"
- "Categories are hierarchical (parent/child)" - "Categories are hierarchical (parent/child)"
- "Blog posts can have multiple tags" - "Blog posts can have multiple tags"
- "Frontend displays post listings with pagination" - "Admin can manage categories in tree view"
- "Frontend displays individual post pages" - "Related posts calculated by shared categories/tags"
- "Related posts shown based on shared categories/tags"
artifacts: artifacts:
- path: "plugins/golem15/blog/models/Category.scala" - path: "plugins/golem15/blog/models/Category.scala"
provides: "Hierarchical category model with nested set" provides: "Hierarchical category model with nested set"
@@ -39,20 +38,16 @@ must_haves:
- path: "plugins/golem15/blog/models/Tag.scala" - path: "plugins/golem15/blog/models/Tag.scala"
provides: "Tag model for free-form labeling" provides: "Tag model for free-form labeling"
contains: "case class Tag" contains: "case class Tag"
- path: "plugins/golem15/blog/components/Posts.scala" - path: "plugins/golem15/blog/services/CategoryService.scala"
provides: "Frontend post listing component" provides: "Category CRUD with tree operations"
exports: ["PostsComponent", "onLoadMore"] exports: ["CategoryService", "getTree", "getAllChildrenAndSelf"]
- path: "plugins/golem15/blog/components/Post.scala" - path: "plugins/golem15/blog/controllers/Categories.scala"
provides: "Single post display component" provides: "Admin controller for category management"
exports: ["PostComponent"] exports: ["CategoriesController"]
- path: "plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql" - path: "plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql"
provides: "Categories, tags, and pivot tables" provides: "Categories, tags, and pivot tables"
contains: "CREATE TABLE blog_categories" contains: "CREATE TABLE blog_categories"
key_links: key_links:
- from: "plugins/golem15/blog/components/Posts.scala"
to: "BlogPostService"
via: "post listing"
pattern: "postService\\.listFrontend"
- from: "plugins/golem15/blog/services/BlogPostService.scala" - from: "plugins/golem15/blog/services/BlogPostService.scala"
to: "CategoryService" to: "CategoryService"
via: "category filtering" via: "category filtering"
@@ -61,19 +56,23 @@ must_haves:
to: "NestedTree" to: "NestedTree"
via: "tree operations" via: "tree operations"
pattern: "NestedTree\\." pattern: "NestedTree\\."
- from: "plugins/golem15/blog/controllers/Categories.scala"
to: "CategoryService"
via: "CRUD operations"
pattern: "categoryService\\."
--- ---
<objective> <objective>
Complete the Blog plugin with categories, tags, and frontend listing components. Add categories and tags to the Blog plugin with admin management.
Purpose: Enable content organization and frontend display, demonstrating hierarchical data structures and frontend component patterns. Purpose: Enable content organization with hierarchical categories and free-form tags, providing the data layer for frontend components.
Output: Complete Blog plugin with: Output: Blog plugin extended with:
- Hierarchical categories using nested set model - Hierarchical categories using nested set model
- Tags for free-form labeling - Tags for free-form labeling
- Frontend Posts component with pagination/infinite scroll - Admin controller for category management
- Frontend Post component for single post display - BlogPostService updated with category/tag relations
- Related posts based on shared categories/tags - Related posts algorithm based on shared categories/tags
</objective> </objective>
<execution_context> <execution_context>
@@ -91,7 +90,7 @@ Output: Complete Blog plugin with:
# Prior plan in this phase # Prior plan in this phase
@.planning/phases/10-core-plugins/10-03-SUMMARY.md @.planning/phases/10-core-plugins/10-03-SUMMARY.md
# Reference: Phase 3 component patterns for HTMX # Reference: Phase 7 admin controller patterns
</context> </context>
<tasks> <tasks>
@@ -209,7 +208,7 @@ Output: Complete Blog plugin with:
</task> </task>
<task type="auto"> <task type="auto">
<name>Task 2: Update BlogPostService for relations</name> <name>Task 2: Update BlogPostService and admin for relations</name>
<files> <files>
plugins/golem15/blog/services/BlogPostService.scala (modify) plugins/golem15/blog/services/BlogPostService.scala (modify)
plugins/golem15/blog/repositories/BlogPostRepository.scala (modify) plugins/golem15/blog/repositories/BlogPostRepository.scala (modify)
@@ -340,150 +339,6 @@ Output: Complete Blog plugin with:
</done> </done>
</task> </task>
<task type="auto">
<name>Task 3: Frontend listing components</name>
<files>
plugins/golem15/blog/components/Posts.scala
plugins/golem15/blog/components/Post.scala
plugins/golem15/blog/components/Categories.scala
plugins/golem15/blog/components/RelatedPosts.scala
plugins/golem15/blog/resources/views/posts/default.peb
plugins/golem15/blog/resources/views/posts/item.peb
plugins/golem15/blog/resources/views/post/default.peb
plugins/golem15/blog/resources/views/categories/default.peb
plugins/golem15/blog/resources/views/relatedposts/default.peb
plugins/golem15/blog/Plugin.scala (modify)
</files>
<action>
Create frontend components for blog display:
**Posts.scala (research Pattern 8):**
- Extend SummerComponent trait
- ComponentDetails: name "Posts", description "Blog post listing"
- Property schema:
- pageNumber: String, URL param for pagination, default "{{ :page }}"
- categoryFilter: String, filter by category slug
- postsPerPage: String, default "10", validation 1-100
- noPostsMessage: String, default "No posts found."
- sortOrder: Dropdown with BlogPost.allowedSortingOptions
- throwNotFound: Checkbox, 404 on empty results
- Page variables: posts (PaginatedResult), category (Option), noPostsMessage
- onRun lifecycle:
- Load category if categoryFilter set
- Build PostListOptions from properties
- Call postService.listFrontend or listByCategory
- Handle empty results per throwNotFound
- Set page context variables
- onLoadMore HTMX handler:
- Parse page number from form
- Load next page of posts
- Return items partial
- Trigger "postsExhausted" when no more
**Post.scala component:**
- ComponentDetails: name "Post", description "Single post display"
- Property schema:
- slug: String, URL param for post slug, default "{{ :slug }}"
- notFoundMessage: String
- Page variables: post (BlogPostWithRelations)
- onRun lifecycle:
- Get slug from URL
- Call postService.findBySlug
- 404 if not found
- Set page context: post, author, categories, tags, readingTime
- Set OpenGraph meta tags for social sharing
**Categories.scala component:**
- ComponentDetails: name "Categories", description "Category listing/tree"
- Property schema:
- displayMode: Dropdown (tree/flat), default "tree"
- showPostCount: Checkbox, default true
- includeEmpty: Checkbox, show categories with no posts
- Page variables: categories (tree or flat list)
- onRun: Load category tree, filter empty if needed
**RelatedPosts.scala component:**
- ComponentDetails: name "RelatedPosts", description "Related posts by category/tag"
- Property schema:
- limit: String, max posts to show, default "3"
- postSlug: String, current post slug (from context)
- Page variables: relatedPosts (List)
- onRun: Call postService.getRelatedPosts, set context
**Templates:**
**posts/default.peb (research code example):**
```html
<div class="blog-posts" id="posts-{{ __SELF__ }}">
{% if posts.items is empty %}
<div class="no-posts">{{ noPostsMessage }}</div>
{% else %}
<div class="posts-list">
{% for post in posts.items %}
{% include "posts/item" %}
{% endfor %}
</div>
{% if posts.hasNext %}
<div class="load-more">
<button
hx-post="{{ componentHandler('onLoadMore') }}"
hx-vals='{"page": {{ posts.currentPage + 1 }}}'
hx-target="#posts-{{ __SELF__ }} .posts-list"
hx-swap="beforeend"
hx-indicator=".htmx-indicator">
Load More
<span class="htmx-indicator">Loading...</span>
</button>
</div>
{% endif %}
{% endif %}
</div>
```
**posts/item.peb:**
- Article element with post data
- Featured image (lazy load)
- Title as link to post page
- Excerpt or auto-generated summary
- Author name, published date, reading time
- Category and tag links
**post/default.peb:**
- Full post display
- OpenGraph meta tags in head
- Title, featured image, author, date
- Content HTML
- Categories and tags
- Share buttons (copy link, Twitter, Facebook)
- Include RelatedPosts component placeholder
**categories/default.peb:**
- Tree or flat list based on displayMode
- Category name as link
- Post count if showPostCount
**relatedposts/default.peb:**
- Grid of related post cards
- Image, title, excerpt snippet
**Register all components in Plugin.scala boot method**
</action>
<verify>
./mill summercms.compile succeeds
Components registered in Plugin.scala
Templates have valid Pebble syntax
HTMX attributes properly formatted
</verify>
<done>
Posts component displays paginated listings with infinite scroll.
Post component displays single post with meta tags.
Categories component shows category tree/list.
RelatedPosts component shows related content.
All templates render with HTMX interactions.
</done>
</task>
</tasks> </tasks>
<verification> <verification>
@@ -491,9 +346,8 @@ After all tasks complete:
1. Blog plugin compiles: `./mill summercms.compile` 1. Blog plugin compiles: `./mill summercms.compile`
2. Migration creates all tables: blog_categories, blog_tags, pivot tables 2. Migration creates all tables: blog_categories, blog_tags, pivot tables
3. Category nested tree rebuilds correctly on insert/delete 3. Category nested tree rebuilds correctly on insert/delete
4. Posts component loads and paginates 4. Posts admin form shows category tree selector and tag input
5. Post component displays with OpenGraph meta 5. Related posts algorithm returns relevant content
6. Related posts algorithm returns relevant content
</verification> </verification>
<success_criteria> <success_criteria>
@@ -501,10 +355,8 @@ After all tasks complete:
- Nested set left/right values correct after operations - Nested set left/right values correct after operations
- Tags can be created on-the-fly when typing - Tags can be created on-the-fly when typing
- Post admin form shows category tree selector and tag input - Post admin form shows category tree selector and tag input
- Frontend /blog shows post listing with load more - Admin can manage categories with tree view and drag-drop
- Frontend /blog/:slug shows full post with related posts - Related posts calculated by shared categories/tags
- Category pages filter posts by category and children
- Tag pages filter posts by tag
</success_criteria> </success_criteria>
<output> <output>

View File

@@ -0,0 +1,287 @@
---
phase: 10-core-plugins
plan: 05
type: execute
wave: 3
depends_on: ["10-04"]
files_modified:
- plugins/golem15/blog/components/Posts.scala
- plugins/golem15/blog/components/Post.scala
- plugins/golem15/blog/components/Categories.scala
- plugins/golem15/blog/components/RelatedPosts.scala
- plugins/golem15/blog/resources/views/posts/default.peb
- plugins/golem15/blog/resources/views/posts/item.peb
- plugins/golem15/blog/resources/views/post/default.peb
- plugins/golem15/blog/resources/views/categories/default.peb
- plugins/golem15/blog/resources/views/relatedposts/default.peb
- plugins/golem15/blog/Plugin.scala
autonomous: true
must_haves:
truths:
- "Frontend displays post listings with pagination"
- "Frontend displays individual post pages"
- "Related posts shown based on shared categories/tags"
- "Category tree/list component available for navigation"
- "Infinite scroll works via HTMX"
artifacts:
- path: "plugins/golem15/blog/components/Posts.scala"
provides: "Frontend post listing component"
exports: ["PostsComponent", "onLoadMore"]
- path: "plugins/golem15/blog/components/Post.scala"
provides: "Single post display component"
exports: ["PostComponent"]
- path: "plugins/golem15/blog/components/Categories.scala"
provides: "Category listing/tree component"
exports: ["CategoriesComponent"]
- path: "plugins/golem15/blog/components/RelatedPosts.scala"
provides: "Related posts component"
exports: ["RelatedPostsComponent"]
- path: "plugins/golem15/blog/resources/views/posts/default.peb"
provides: "Post listing template"
contains: "hx-post"
key_links:
- from: "plugins/golem15/blog/components/Posts.scala"
to: "BlogPostService"
via: "post listing"
pattern: "postService\\.listFrontend"
- from: "plugins/golem15/blog/components/Post.scala"
to: "BlogPostService"
via: "single post lookup"
pattern: "postService\\.findBySlug"
- from: "plugins/golem15/blog/components/RelatedPosts.scala"
to: "BlogPostService"
via: "related posts query"
pattern: "postService\\.getRelatedPosts"
---
<objective>
Create frontend components for blog display with pagination and HTMX interactions.
Purpose: Enable visitors to browse and read blog content, demonstrating frontend component patterns with HTMX.
Output: Complete Blog frontend with:
- Posts component with pagination/infinite scroll
- Post component for single post display with OpenGraph meta
- Categories component for navigation
- RelatedPosts component for content discovery
</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
# Prior plans in this phase
@.planning/phases/10-core-plugins/10-03-SUMMARY.md
@.planning/phases/10-core-plugins/10-04-SUMMARY.md
# Reference: Phase 3 component patterns for HTMX
</context>
<tasks>
<task type="auto">
<name>Task 1: Posts and Post listing components</name>
<files>
plugins/golem15/blog/components/Posts.scala
plugins/golem15/blog/components/Post.scala
plugins/golem15/blog/resources/views/posts/default.peb
plugins/golem15/blog/resources/views/posts/item.peb
plugins/golem15/blog/resources/views/post/default.peb
</files>
<action>
Create frontend components for blog display:
**Posts.scala (research Pattern 8):**
- Extend SummerComponent trait
- ComponentDetails: name "Posts", description "Blog post listing"
- Property schema:
- pageNumber: String, URL param for pagination, default "{{ :page }}"
- categoryFilter: String, filter by category slug
- postsPerPage: String, default "10", validation 1-100
- noPostsMessage: String, default "No posts found."
- sortOrder: Dropdown with BlogPost.allowedSortingOptions
- throwNotFound: Checkbox, 404 on empty results
- Page variables: posts (PaginatedResult), category (Option), noPostsMessage
- onRun lifecycle:
- Load category if categoryFilter set
- Build PostListOptions from properties
- Call postService.listFrontend or listByCategory
- Handle empty results per throwNotFound
- Set page context variables
- onLoadMore HTMX handler:
- Parse page number from form
- Load next page of posts
- Return items partial
- Trigger "postsExhausted" when no more
**Post.scala component:**
- ComponentDetails: name "Post", description "Single post display"
- Property schema:
- slug: String, URL param for post slug, default "{{ :slug }}"
- notFoundMessage: String
- Page variables: post (BlogPostWithRelations)
- onRun lifecycle:
- Get slug from URL
- Call postService.findBySlug
- 404 if not found
- Set page context: post, author, categories, tags, readingTime
- Set OpenGraph meta tags for social sharing
**Templates:**
**posts/default.peb (research code example):**
```html
<div class="blog-posts" id="posts-{{ __SELF__ }}">
{% if posts.items is empty %}
<div class="no-posts">{{ noPostsMessage }}</div>
{% else %}
<div class="posts-list">
{% for post in posts.items %}
{% include "posts/item" %}
{% endfor %}
</div>
{% if posts.hasNext %}
<div class="load-more">
<button
hx-post="{{ componentHandler('onLoadMore') }}"
hx-vals='{"page": {{ posts.currentPage + 1 }}}'
hx-target="#posts-{{ __SELF__ }} .posts-list"
hx-swap="beforeend"
hx-indicator=".htmx-indicator">
Load More
<span class="htmx-indicator">Loading...</span>
</button>
</div>
{% endif %}
{% endif %}
</div>
```
**posts/item.peb:**
- Article element with post data
- Featured image (lazy load)
- Title as link to post page
- Excerpt or auto-generated summary
- Author name, published date, reading time
- Category and tag links
**post/default.peb:**
- Full post display
- OpenGraph meta tags in head
- Title, featured image, author, date
- Content HTML
- Categories and tags
- Share buttons (copy link, Twitter, Facebook)
- Include RelatedPosts component placeholder
</action>
<verify>
./mill summercms.compile succeeds
Posts component has onLoadMore HTMX handler
Post component sets OpenGraph meta tags
Templates have valid Pebble syntax
</verify>
<done>
Posts component displays paginated listings with infinite scroll.
Post component displays single post with meta tags.
Templates render with HTMX interactions.
</done>
</task>
<task type="auto">
<name>Task 2: Categories and RelatedPosts components</name>
<files>
plugins/golem15/blog/components/Categories.scala
plugins/golem15/blog/components/RelatedPosts.scala
plugins/golem15/blog/resources/views/categories/default.peb
plugins/golem15/blog/resources/views/relatedposts/default.peb
plugins/golem15/blog/Plugin.scala (modify)
</files>
<action>
Create category navigation and related posts components:
**Categories.scala component:**
- ComponentDetails: name "Categories", description "Category listing/tree"
- Property schema:
- displayMode: Dropdown (tree/flat), default "tree"
- showPostCount: Checkbox, default true
- includeEmpty: Checkbox, show categories with no posts
- Page variables: categories (tree or flat list)
- onRun: Load category tree, filter empty if needed
**RelatedPosts.scala component:**
- ComponentDetails: name "RelatedPosts", description "Related posts by category/tag"
- Property schema:
- limit: String, max posts to show, default "3"
- postSlug: String, current post slug (from context)
- Page variables: relatedPosts (List)
- onRun: Call postService.getRelatedPosts, set context
**Templates:**
**categories/default.peb:**
- Tree or flat list based on displayMode
- Category name as link
- Post count if showPostCount
- Recursive include for tree mode
**relatedposts/default.peb:**
- Grid of related post cards
- Image, title, excerpt snippet
- Link to full post
**Register all components in Plugin.scala boot method:**
- Posts component
- Post component
- Categories component
- RelatedPosts component
</action>
<verify>
./mill summercms.compile succeeds
Components registered in Plugin.scala
Templates have valid Pebble syntax
HTMX attributes properly formatted
</verify>
<done>
Categories component shows category tree/list.
RelatedPosts component shows related content.
All components registered in Plugin.scala.
Blog frontend fully functional.
</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. Blog plugin compiles: `./mill summercms.compile`
2. All four components registered in Plugin.scala
3. Posts component loads and paginates
4. Post component displays with OpenGraph meta
5. Categories component renders tree structure
6. RelatedPosts component shows related content
</verification>
<success_criteria>
- Frontend /blog shows post listing with load more
- Frontend /blog/:slug shows full post with related posts
- Category pages filter posts by category and children
- Tag pages filter posts by tag
- Infinite scroll works via HTMX onLoadMore handler
- OpenGraph meta tags present for social sharing
- Category tree/list renders correctly
- Related posts show content with shared categories/tags
</success_criteria>
<output>
After completion, create `.planning/phases/10-core-plugins/10-05-SUMMARY.md`
</output>