diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index dbf8a0f..2367eae 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -195,13 +195,13 @@ Plans:
6. Blog posts can be organized into categories
7. Blog posts can be tagged with multiple tags
8. Frontend displays blog post listings via components
-**Plans**: TBD
+**Plans**: 4 plans
Plans:
-- [ ] 10-01: User plugin - registration and authentication
-- [ ] 10-02: User plugin - profiles and password reset
-- [ ] 10-03: Blog plugin - posts and editor
-- [ ] 10-04: Blog plugin - categories, tags, listing components
+- [ ] 10-01-PLAN.md - User plugin models, services, and authentication components
+- [ ] 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-04-PLAN.md - Blog categories, tags, and frontend listing components
## Progress
@@ -219,7 +219,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10
| 7. Admin Forms & Lists | 0/3 | Planned | - |
| 8. Admin Dashboard | 0/4 | Planned | - |
| 9. Content Management | 0/7 | Planned | - |
-| 10. Core Plugins | 0/4 | Not started | - |
+| 10. Core Plugins | 0/4 | Planned | - |
---
*Roadmap created: 2026-02-04*
diff --git a/.planning/phases/10-core-plugins/10-01-PLAN.md b/.planning/phases/10-core-plugins/10-01-PLAN.md
new file mode 100644
index 0000000..f406e8c
--- /dev/null
+++ b/.planning/phases/10-core-plugins/10-01-PLAN.md
@@ -0,0 +1,333 @@
+---
+phase: 10-core-plugins
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - plugins/golem15/user/plugin.yaml
+ - plugins/golem15/user/Plugin.scala
+ - plugins/golem15/user/models/FrontendUser.scala
+ - plugins/golem15/user/models/UserGroup.scala
+ - plugins/golem15/user/models/UserThrottle.scala
+ - plugins/golem15/user/models/UserSettings.scala
+ - plugins/golem15/user/services/FrontendUserService.scala
+ - plugins/golem15/user/services/FrontendAuthService.scala
+ - plugins/golem15/user/services/ThrottleService.scala
+ - plugins/golem15/user/components/Session.scala
+ - plugins/golem15/user/components/Account.scala
+ - plugins/golem15/user/resources/db/migration/V10_1_1__user_plugin.sql
+ - plugins/golem15/user/resources/views/account/login.peb
+ - plugins/golem15/user/resources/views/account/register.peb
+autonomous: true
+
+must_haves:
+ truths:
+ - "Frontend visitor can register with email, password, name, surname"
+ - "Registered user receives activation email if activation mode is user"
+ - "User can log in with email and password"
+ - "User can log out and session is terminated"
+ - "Session component restricts page access based on security mode"
+ - "Login throttling prevents brute force attacks"
+ artifacts:
+ - path: "plugins/golem15/user/models/FrontendUser.scala"
+ provides: "Frontend user domain model with validation"
+ contains: "case class FrontendUser"
+ - path: "plugins/golem15/user/services/FrontendAuthService.scala"
+ provides: "Authentication logic with JWT session tokens"
+ exports: ["FrontendAuthService", "login", "logout", "getCurrentUser"]
+ - path: "plugins/golem15/user/components/Account.scala"
+ provides: "Registration and login HTMX handlers"
+ exports: ["AccountComponent", "onSignin", "onRegister"]
+ - path: "plugins/golem15/user/components/Session.scala"
+ provides: "Page access control component"
+ exports: ["SessionComponent", "onLogout"]
+ - path: "plugins/golem15/user/resources/db/migration/V10_1_1__user_plugin.sql"
+ provides: "Database schema for frontend users"
+ contains: "CREATE TABLE frontend_users"
+ key_links:
+ - from: "plugins/golem15/user/components/Account.scala"
+ to: "FrontendAuthService"
+ via: "ZIO service injection"
+ pattern: "ZIO\\.service\\[FrontendAuthService\\]"
+ - from: "plugins/golem15/user/services/FrontendAuthService.scala"
+ to: "FrontendUserRepository"
+ via: "repository lookup"
+ pattern: "userRepo\\.findByEmail"
+ - from: "plugins/golem15/user/components/Session.scala"
+ to: "pageContext"
+ via: "user injection"
+ pattern: "pageContext\\.set\\(\"user\""
+---
+
+
+Create the User plugin foundation with registration and authentication functionality.
+
+Purpose: Establish frontend user management as the first core plugin, demonstrating the complete plugin pattern with models, services, components, and migrations.
+
+Output: Working User plugin with:
+- FrontendUser model with validation rules
+- Registration flow with configurable activation
+- Login/logout with JWT cookie sessions
+- Session component for page access control
+- Login throttling for security
+
+
+
+@/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 3: Component system with HTMX handlers
+# Phase 6: Authentication patterns (Argon2id, JWT)
+
+
+
+
+
+ Task 1: User plugin models and database schema
+
+ plugins/golem15/user/plugin.yaml
+ plugins/golem15/user/Plugin.scala
+ plugins/golem15/user/models/FrontendUser.scala
+ plugins/golem15/user/models/UserGroup.scala
+ plugins/golem15/user/models/UserThrottle.scala
+ plugins/golem15/user/models/UserSettings.scala
+ plugins/golem15/user/resources/db/migration/V10_1_1__user_plugin.sql
+
+
+ Create User plugin structure following Phase 2 plugin patterns:
+
+ **plugin.yaml:**
+ - name: Golem15.User
+ - description: Frontend user authentication and management
+ - author: Golem15
+ - version: 1.0.0
+ - require: [] (no plugin dependencies)
+
+ **Plugin.scala:**
+ - Extend SummerPlugin trait
+ - Register components: Session, Account (ResetPassword in plan 02)
+ - Register settings model
+ - Boot method for any initialization
+
+ **FrontendUser.scala:**
+ - Case class with fields from research Pattern 1:
+ id, email, passwordHash, name, surname, username (Option), avatarPath (Option),
+ isActivated, activationCode, activatedAt, persistCode, resetPasswordCode,
+ lastLogin, lastSeen, createdIpAddress, lastIpAddress, isGuest, isSuperuser,
+ deletedAt (soft delete), createdAt, updatedAt
+ - Validation rules object: email (required, unique, 6-255), password (required, 8-255),
+ name (required, 2-100), surname (required, 2-100)
+ - Quill table mapping to `frontend_users`
+
+ **UserGroup.scala:**
+ - Case class: id, name, code (unique), description, createdAt, updatedAt
+ - Quill table mapping to `user_groups`
+ - UsersGroups pivot case class for many-to-many
+
+ **UserThrottle.scala:**
+ - Case class: id, userId (Option), ipAddress (Option), attempts, lastAttemptAt,
+ isSuspended, suspendedAt, isBanned, bannedAt
+ - Quill table mapping to `user_throttle`
+
+ **UserSettings.scala:**
+ - Sealed traits: ActivateMode (Auto, User, Admin), RememberLogin (Always, Never, Ask)
+ - Case class UserPluginSettings with fields from research Pattern 2:
+ allowRegistration, requireActivation, activateMode, useThrottle,
+ useRegisterThrottle, blockPersistence, rememberLogin, minPasswordLength
+ - Default values matching WinterCMS
+
+ **V10_1_1__user_plugin.sql:**
+ - CREATE TABLE frontend_users (all fields with proper types, indexes)
+ - CREATE TABLE user_groups (id, name, code unique, description, timestamps)
+ - CREATE TABLE users_groups (user_id, group_id, composite PK)
+ - CREATE TABLE user_throttle (id, user_id nullable, ip_address inet, attempts, timestamps, ban flags)
+ - Indexes on email, username, persist_code, throttle lookups
+
+
+ ./mill summercms.compile succeeds with new models
+ SQL migration syntax valid (no syntax errors in IDE)
+
+
+ FrontendUser, UserGroup, UserThrottle, UserSettings models exist with Quill mappings.
+ Migration creates all required tables with proper indexes.
+
+
+
+
+ Task 2: Authentication and user services
+
+ plugins/golem15/user/repositories/FrontendUserRepository.scala
+ plugins/golem15/user/services/FrontendUserService.scala
+ plugins/golem15/user/services/FrontendAuthService.scala
+ plugins/golem15/user/services/ThrottleService.scala
+
+
+ Create service layer for user operations:
+
+ **FrontendUserRepository.scala:**
+ - Trait with ZIO effects following Phase 1 repository pattern
+ - Methods: findById, findByEmail, findByPersistCode, create, update, delete (soft)
+ - Live implementation using QuillContext
+ - Error handling with RepositoryError ADT
+
+ **FrontendUserService.scala:**
+ - Trait following research Pattern 1
+ - Methods:
+ - findByEmail(email): Option[FrontendUser]
+ - findById(id): Option[FrontendUser]
+ - register(data: RegistrationData): FrontendUser (handles activation code generation)
+ - activate(userId, code): FrontendUser
+ - updateProfile(userId, data: ProfileUpdate): FrontendUser
+ - touchLastSeen(userId): Unit
+ - touchIpAddress(userId, ip): Unit
+ - RegistrationData case class: email, password, name, surname, ipAddress
+ - ProfileUpdate case class: name, surname, newPassword (Option)
+ - Use Password4j for hashing (Argon2id, matching Phase 6 patterns)
+ - Live implementation with ZLayer
+
+ **FrontendAuthService.scala:**
+ - Trait following research Pattern 10
+ - Methods:
+ - login(user, remember): String (returns JWT token)
+ - logout: Unit (clears session, rotates persistCode)
+ - getCurrentUser: Option[FrontendUser]
+ - isAuthenticated: Boolean
+ - verifyPassword(userId, password): Boolean
+ - validateAndSetSession(token): Unit (for middleware)
+ - JWT token generation using jwt-scala (15min session, 7day remember)
+ - Cookie configuration helper (httpOnly, secure in prod, SameSite Lax)
+ - Session state via Ref[Option[FrontendUser]]
+ - Live implementation with ZLayer
+
+ **ThrottleService.scala:**
+ - Trait for login/registration throttling
+ - Methods:
+ - isThrottled(ip): Boolean
+ - recordAttempt(ip, success): Unit
+ - clearAttempts(ip): Unit
+ - isBanned(ip): Boolean
+ - Configurable thresholds (5 attempts, 15 min lockout)
+ - Live implementation with ZLayer
+
+
+ ./mill summercms.compile succeeds
+ All service traits have Live implementations
+ ZLayers compose correctly (no missing dependencies)
+
+
+ FrontendUserRepository provides data access.
+ FrontendUserService handles registration with Argon2id password hashing.
+ FrontendAuthService manages JWT sessions with cookie helpers.
+ ThrottleService prevents brute force attacks.
+
+
+
+
+ Task 3: Session and Account components with templates
+
+ plugins/golem15/user/components/Session.scala
+ plugins/golem15/user/components/Account.scala
+ plugins/golem15/user/resources/views/session/default.peb
+ plugins/golem15/user/resources/views/account/login.peb
+ plugins/golem15/user/resources/views/account/register.peb
+ plugins/golem15/user/resources/views/account/success.peb
+
+
+ Create frontend components following Phase 3 component patterns:
+
+ **Session.scala (research Pattern 3):**
+ - Extend SummerComponent trait
+ - ComponentDetails: name "Session", description "User session and access control"
+ - Property schema:
+ - security: Dropdown (all/user/guest), default "all"
+ - allowedUserGroups: Set, optional group filtering
+ - redirect: Dropdown, page to redirect unauthorized users
+ - onRun lifecycle:
+ - Check security mode against current auth state
+ - If unauthorized and redirect set, fail with ComponentRedirect
+ - If unauthorized and no redirect, fail with AccessDenied
+ - Inject "user" into pageContext (null if guest)
+ - Touch lastSeen for authenticated users
+ - HTMX handler onLogout:
+ - Call authService.logout
+ - Return HtmxResponse with HX-Redirect to configured page
+ - Trigger "userLoggedOut" event
+
+ **Account.scala (research Pattern 4):**
+ - Extend SummerComponent trait
+ - ComponentDetails: name "Account", description "Registration, login, profile"
+ - Property schema:
+ - redirect: Dropdown, page after login/register
+ - paramCode: String, activation code URL param name, default "code"
+ - requirePassword: Checkbox, require current password for profile update
+ - HTMX handlers:
+ - onSignin: Parse login/password from form, validate length, call authService.authenticate,
+ handle errors (InvalidCredentials, AccountBanned, NotActivated) with generic message,
+ record IP, return HX-Redirect on success
+ - onRegister: Check allowRegistration setting, check throttle, parse form data,
+ call userService.register, send activation email if mode is User,
+ auto-login if auto-activated, return HX-Redirect on success
+ - onActivate: Get code from URL param, call userService.activate, auto-login, redirect
+
+ **Templates (Pebble):**
+ - session/default.peb: Empty by default (component injects user context only)
+ - account/login.peb:
+ - Form with hx-post to onSignin handler
+ - Email input, password input, remember checkbox (if settings allow)
+ - Error display area with hx-swap="innerHTML"
+ - CSRF token hidden field
+ - account/register.peb:
+ - Form with hx-post to onRegister handler
+ - Email, password, password_confirmation, name, surname inputs
+ - Error display area
+ - CSRF token hidden field
+ - account/success.peb:
+ - Success message partial for HTMX responses
+
+
+ ./mill summercms.compile succeeds
+ Templates have valid Pebble syntax (no unclosed tags)
+ Components registered in Plugin.scala boot method
+
+
+ Session component controls page access with security modes.
+ Account component handles login and registration via HTMX.
+ Templates render forms with CSRF protection and error handling.
+ User plugin fully functional for registration and authentication flows.
+
+
+
+
+
+
+After all tasks complete:
+1. User plugin compiles: `./mill summercms.compile`
+2. Migration valid: Check SQL syntax for frontend_users, user_groups, users_groups, user_throttle
+3. Component registration: Plugin.scala registers Session and Account components
+4. Service wiring: All ZLayers compose without missing dependencies
+5. Template syntax: Pebble templates parse without errors
+
+
+
+- Frontend visitor can submit registration form (model, service, component exist)
+- Registration creates FrontendUser with hashed password
+- Login authenticates user and sets JWT cookie
+- Session component restricts page access based on security mode
+- Logout clears session and rotates persist code
+- Throttle service tracks failed login attempts
+
+
+
diff --git a/.planning/phases/10-core-plugins/10-02-PLAN.md b/.planning/phases/10-core-plugins/10-02-PLAN.md
new file mode 100644
index 0000000..c0bbd5a
--- /dev/null
+++ b/.planning/phases/10-core-plugins/10-02-PLAN.md
@@ -0,0 +1,267 @@
+---
+phase: 10-core-plugins
+plan: 02
+type: execute
+wave: 2
+depends_on: ["10-01"]
+files_modified:
+ - plugins/golem15/user/components/ResetPassword.scala
+ - plugins/golem15/user/services/FrontendUserService.scala
+ - plugins/golem15/user/services/UserMailService.scala
+ - plugins/golem15/user/resources/views/account/profile.peb
+ - plugins/golem15/user/resources/views/account/avatar.peb
+ - plugins/golem15/user/resources/views/resetpassword/request.peb
+ - plugins/golem15/user/resources/views/resetpassword/reset.peb
+ - plugins/golem15/user/resources/views/mail/activation.peb
+ - plugins/golem15/user/resources/views/mail/reset.peb
+autonomous: true
+
+must_haves:
+ truths:
+ - "Logged-in user can view and edit their profile"
+ - "User can change password with current password verification"
+ - "User can upload and change avatar"
+ - "Visitor can request password reset via email"
+ - "User can reset password using email link with token"
+ - "Activation email sent when activation mode is user"
+ artifacts:
+ - path: "plugins/golem15/user/components/ResetPassword.scala"
+ provides: "Password reset flow component"
+ exports: ["ResetPasswordComponent", "onRequest", "onReset"]
+ - path: "plugins/golem15/user/services/UserMailService.scala"
+ provides: "Email sending for activation and reset"
+ exports: ["UserMailService", "sendActivationEmail", "sendResetEmail"]
+ - path: "plugins/golem15/user/resources/views/account/profile.peb"
+ provides: "Profile edit form template"
+ contains: "hx-post"
+ key_links:
+ - from: "plugins/golem15/user/components/Account.scala"
+ to: "UserMailService"
+ via: "activation email on register"
+ pattern: "mailService\\.sendActivationEmail"
+ - from: "plugins/golem15/user/components/ResetPassword.scala"
+ to: "FrontendUserService"
+ via: "password reset"
+ pattern: "userService\\.resetPassword"
+ - from: "plugins/golem15/user/services/FrontendUserService.scala"
+ to: "MediaLibrary"
+ via: "avatar upload"
+ pattern: "mediaLibrary\\.upload"
+---
+
+
+Complete the User plugin with profile management and password reset functionality.
+
+Purpose: Enable users to manage their accounts and recover access, completing the frontend user experience.
+
+Output: Working profile and reset features:
+- Profile edit form with name/surname/password change
+- Avatar upload using media library
+- Password reset request and completion flow
+- Email templates for activation and reset
+
+
+
+@/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
+
+# Prior plan in this phase
+@.planning/phases/10-core-plugins/10-01-SUMMARY.md
+
+# Reference: Phase 9 media library for avatar upload
+
+
+
+
+
+ Task 1: Profile management with avatar upload
+
+ plugins/golem15/user/components/Account.scala (modify)
+ plugins/golem15/user/services/FrontendUserService.scala (modify)
+ plugins/golem15/user/resources/views/account/profile.peb
+ plugins/golem15/user/resources/views/account/avatar.peb
+
+
+ Add profile management to Account component:
+
+ **Account.scala additions:**
+ - Add onUpdate HTMX handler (research Pattern 4):
+ - Get current user from authService, fail if not authenticated
+ - If requirePassword property set, validate current password
+ - Parse ProfileUpdate from form (name, surname, newPassword optional)
+ - Validate new password length if provided (min from settings)
+ - Call userService.updateProfile
+ - If password changed, re-authenticate user (new JWT)
+ - Return success partial with "Profile updated" message
+ - Add onUploadAvatar HTMX handler:
+ - Get current user, fail if not authenticated
+ - Get uploaded file from multipart form
+ - Validate file type (image/jpeg, image/png, image/gif, image/webp)
+ - Validate file size (max 2MB from settings or default)
+ - Call userService.updateAvatar
+ - Return updated avatar partial with new image
+
+ **FrontendUserService.scala additions:**
+ - Add updateAvatar(userId, file: UploadedFile): FrontendUser
+ - Use MediaLibrary from Phase 9 to store file
+ - Generate path: users/{userId}/avatar.{ext}
+ - Update user.avatarPath
+ - Delete old avatar if exists
+ - Modify updateProfile to handle password change:
+ - If newPassword provided, hash with Argon2id
+ - Update passwordHash field
+ - Rotate persistCode to invalidate other sessions
+
+ **profile.peb template:**
+ - Form with hx-post to onUpdate handler, hx-target for response
+ - Name input (pre-filled from user)
+ - Surname input (pre-filled from user)
+ - Current password input (if requirePassword property)
+ - New password input (optional)
+ - Confirm new password input
+ - Submit button
+ - Error/success display area
+ - CSRF token
+
+ **avatar.peb template:**
+ - Current avatar display (or placeholder)
+ - Form with hx-post to onUploadAvatar, hx-encoding="multipart/form-data"
+ - File input with accept="image/*"
+ - Upload button
+ - Preview area that updates on success
+
+
+ ./mill summercms.compile succeeds
+ Account component has onUpdate and onUploadAvatar handlers
+ Templates have valid Pebble syntax
+
+
+ Users can edit profile (name, surname, password).
+ Password change requires current password if configured.
+ Avatar upload stores image via media library.
+ Templates render forms with proper HTMX attributes.
+
+
+
+
+ Task 2: Password reset flow and email service
+
+ plugins/golem15/user/components/ResetPassword.scala
+ plugins/golem15/user/services/UserMailService.scala
+ plugins/golem15/user/services/FrontendUserService.scala (modify)
+ plugins/golem15/user/resources/views/resetpassword/request.peb
+ plugins/golem15/user/resources/views/resetpassword/reset.peb
+ plugins/golem15/user/resources/views/mail/activation.peb
+ plugins/golem15/user/resources/views/mail/reset.peb
+
+
+ Create password reset component and email service:
+
+ **UserMailService.scala:**
+ - Trait with ZIO effects:
+ - sendActivationEmail(user: FrontendUser, code: String): IO[MailError, Unit]
+ - sendResetEmail(user: FrontendUser, code: String): IO[MailError, Unit]
+ - Live implementation using ZIO email service (or stub for now):
+ - Load Pebble template for email body
+ - Include activation/reset URL with code
+ - Send via configured SMTP or email service
+ - MailError ADT: SendFailed, TemplateError, ConfigurationError
+
+ **FrontendUserService.scala additions:**
+ - requestPasswordReset(email: String): Unit
+ - Find user by email (don't reveal if not found - timing attack)
+ - Generate secure random reset code (32 bytes hex)
+ - Set resetPasswordCode on user with expiry tracking
+ - Call mailService.sendResetEmail
+ - Always succeed (don't reveal user existence)
+ - resetPassword(code: String, newPassword: String): FrontendUser
+ - Find user by resetPasswordCode
+ - Validate code not expired (24 hours)
+ - Hash new password with Argon2id
+ - Clear resetPasswordCode
+ - Rotate persistCode (invalidate other sessions)
+ - Return updated user
+
+ **ResetPassword.scala component:**
+ - Extend SummerComponent trait
+ - ComponentDetails: name "ResetPassword", description "Password recovery flow"
+ - Property schema:
+ - paramCode: String, URL parameter for reset code, default "code"
+ - redirect: Dropdown, page after successful reset
+ - HTMX handlers:
+ - onRequest: Parse email from form, call userService.requestPasswordReset,
+ always show success message (no email enumeration)
+ - onReset: Get code from URL param, parse new password + confirmation,
+ validate password length and match, call userService.resetPassword,
+ auto-login user, redirect to configured page
+
+ **Templates:**
+ - resetpassword/request.peb:
+ - Form with hx-post to onRequest
+ - Email input
+ - Submit button "Send Reset Link"
+ - Success message: "If an account exists, you will receive an email"
+ - resetpassword/reset.peb:
+ - Form with hx-post to onReset
+ - Hidden input with reset code from URL
+ - New password input
+ - Confirm password input
+ - Submit button "Reset Password"
+ - Error display for invalid/expired code
+ - mail/activation.peb (Pebble email template):
+ - Subject: "Activate your account"
+ - Body: Welcome message, activation link with code
+ - Plain text fallback
+ - mail/reset.peb (Pebble email template):
+ - Subject: "Reset your password"
+ - Body: Reset link with code, expiry notice (24 hours)
+ - Plain text fallback
+
+ **Register ResetPassword component in Plugin.scala**
+
+
+ ./mill summercms.compile succeeds
+ ResetPassword component registered in Plugin.scala
+ Email templates have valid Pebble syntax
+ Reset code generation uses SecureRandom
+
+
+ Password reset request sends email (or stubs if no SMTP).
+ Reset link with token allows setting new password.
+ Activation email template ready for registration flow.
+ No email enumeration possible (generic success messages).
+
+
+
+
+
+
+After all tasks complete:
+1. User plugin compiles: `./mill summercms.compile`
+2. Account component has all handlers: onSignin, onRegister, onActivate, onUpdate, onUploadAvatar
+3. ResetPassword component handles: onRequest, onReset
+4. Email service can send activation and reset emails
+5. Templates render valid forms with HTMX
+
+
+
+- Logged-in user can view profile form pre-filled with their data
+- Profile update saves name/surname changes
+- Password change requires current password confirmation
+- Avatar upload stores image and updates user record
+- Password reset request always shows success (no enumeration)
+- Reset link with valid code allows new password entry
+- User automatically logged in after successful reset
+
+
+
diff --git a/.planning/phases/10-core-plugins/10-03-PLAN.md b/.planning/phases/10-core-plugins/10-03-PLAN.md
new file mode 100644
index 0000000..925ad18
--- /dev/null
+++ b/.planning/phases/10-core-plugins/10-03-PLAN.md
@@ -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"
+---
+
+
+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
+
+
+
diff --git a/.planning/phases/10-core-plugins/10-04-PLAN.md b/.planning/phases/10-core-plugins/10-04-PLAN.md
new file mode 100644
index 0000000..c7020e8
--- /dev/null
+++ b/.planning/phases/10-core-plugins/10-04-PLAN.md
@@ -0,0 +1,512 @@
+---
+phase: 10-core-plugins
+plan: 04
+type: execute
+wave: 2
+depends_on: ["10-03"]
+files_modified:
+ - plugins/golem15/blog/models/Category.scala
+ - plugins/golem15/blog/models/Tag.scala
+ - plugins/golem15/blog/repositories/CategoryRepository.scala
+ - plugins/golem15/blog/repositories/TagRepository.scala
+ - plugins/golem15/blog/services/CategoryService.scala
+ - plugins/golem15/blog/services/TagService.scala
+ - plugins/golem15/blog/services/BlogPostService.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/controllers/categories/fields.yaml
+ - plugins/golem15/blog/resources/views/posts/default.peb
+ - plugins/golem15/blog/resources/views/posts/item.peb
+ - plugins/golem15/blog/resources/views/post/default.peb
+autonomous: true
+
+must_haves:
+ truths:
+ - "Blog posts can be assigned to categories"
+ - "Categories are hierarchical (parent/child)"
+ - "Blog posts can have multiple tags"
+ - "Frontend displays post listings with pagination"
+ - "Frontend displays individual post pages"
+ - "Related posts shown based on shared categories/tags"
+ artifacts:
+ - path: "plugins/golem15/blog/models/Category.scala"
+ provides: "Hierarchical category model with nested set"
+ contains: "case class Category"
+ - path: "plugins/golem15/blog/models/Tag.scala"
+ provides: "Tag model for free-form labeling"
+ contains: "case class Tag"
+ - 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/resources/db/migration/V10_2_2__blog_categories_tags.sql"
+ provides: "Categories, tags, and pivot tables"
+ contains: "CREATE TABLE blog_categories"
+ key_links:
+ - from: "plugins/golem15/blog/components/Posts.scala"
+ to: "BlogPostService"
+ via: "post listing"
+ pattern: "postService\\.listFrontend"
+ - from: "plugins/golem15/blog/services/BlogPostService.scala"
+ to: "CategoryService"
+ via: "category filtering"
+ pattern: "categoryService\\.getAllChildrenAndSelf"
+ - from: "plugins/golem15/blog/services/CategoryService.scala"
+ to: "NestedTree"
+ via: "tree operations"
+ pattern: "NestedTree\\."
+---
+
+
+Complete the Blog plugin with categories, tags, and frontend listing components.
+
+Purpose: Enable content organization and frontend display, demonstrating hierarchical data structures and frontend component patterns.
+
+Output: Complete Blog plugin with:
+- Hierarchical categories using nested set model
+- Tags for free-form labeling
+- Frontend Posts component with pagination/infinite scroll
+- Frontend Post component for single post display
+- Related posts based on shared categories/tags
+
+
+
+@/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
+
+# Prior plan in this phase
+@.planning/phases/10-core-plugins/10-03-SUMMARY.md
+
+# Reference: Phase 3 component patterns for HTMX
+
+
+
+
+
+ Task 1: Category and tag models with services
+
+ plugins/golem15/blog/models/Category.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/TagRepository.scala
+ plugins/golem15/blog/services/CategoryService.scala
+ plugins/golem15/blog/services/TagService.scala
+ plugins/golem15/blog/services/NestedTree.scala
+ plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql
+
+
+ Create category and tag models with nested tree support:
+
+ **Category.scala (research Pattern 6):**
+ - Case class with fields:
+ id, name, slug (unique), description (Option), parentId (Option),
+ nestLeft (Int), nestRight (Int), nestDepth (Int),
+ published, publishedAt, createdAt, updatedAt
+ - Quill table mapping to `blog_categories`
+ - CategoryTreeNode case class: category, children (List), postCount
+
+ **Tag.scala (research Pattern 7):**
+ - Case class: id, name, slug (unique), createdAt, updatedAt
+ - Quill table mapping to `blog_tags`
+ - TagWithCount case class: tag, postCount
+
+ **PostCategory.scala & PostTag.scala:**
+ - Pivot case classes for many-to-many relationships
+ - PostCategory: postId, categoryId
+ - PostTag: postId, tagId
+
+ **NestedTree.scala (utility object):**
+ - Functions for nested set operations:
+ - rebuild(categories: List[Category]): List[Category] - Recalculate left/right
+ - descendants(category, all): List[Category] - Get all children
+ - ancestors(category, all): List[Category] - Get all parents
+ - moveNode(category, newParent, all): List[Category] - Move category in tree
+ - insertNode(category, parent, all): List[Category] - Add new category
+ - deleteNode(category, all): List[Category] - Remove category and adjust tree
+
+ **CategoryRepository.scala:**
+ - Trait:
+ - findById(id): Option[Category]
+ - findBySlug(slug): Option[Category]
+ - listAll: List[Category]
+ - create(category): Category
+ - update(category): Category
+ - delete(id): Unit
+ - getPostCount(categoryId): Int
+ - getNestedPostCount(categoryId): Int (includes children)
+ - Live implementation using QuillContext
+
+ **TagRepository.scala:**
+ - Trait:
+ - findById(id): Option[Tag]
+ - findBySlug(slug): Option[Tag]
+ - findByName(name): Option[Tag]
+ - listAll: List[Tag]
+ - create(tag): Tag
+ - listForPost(postId): List[Tag]
+ - getPopular(limit): List[TagWithCount]
+ - Live implementation
+
+ **CategoryService.scala:**
+ - Trait:
+ - create(data): Category (auto-generate slug, rebuild tree)
+ - update(id, data): Category (rebuild tree if parent changed)
+ - delete(id): Unit (rebuild tree after)
+ - findBySlug(slug): Option[Category]
+ - listAll: List[Category]
+ - getTree: List[CategoryTreeNode]
+ - getChildren(parentId): List[Category]
+ - getAllChildrenAndSelf(categoryId): List[Category] (for nested queries)
+ - getPostCount(categoryId): Int
+ - getNestedPostCount(categoryId): Int
+ - Live implementation with NestedTree for tree operations
+
+ **TagService.scala:**
+ - Trait:
+ - findOrCreate(name): Tag (create with auto-slug if not exists)
+ - findBySlug(slug): Option[Tag]
+ - listAll: List[Tag]
+ - listForPost(postId): List[Tag]
+ - getPopularTags(limit): List[TagWithCount]
+ - syncPostTags(postId, tagNames): List[Tag] (update pivot table)
+ - Live implementation
+
+ **V10_2_2__blog_categories_tags.sql:**
+ - CREATE TABLE blog_categories (all fields, nested set indexes)
+ - CREATE TABLE blog_tags (id, name, slug unique, timestamps)
+ - CREATE TABLE blog_posts_categories (post_id, category_id, composite PK, FKs)
+ - CREATE TABLE blog_posts_tags (post_id, tag_id, composite PK, FKs)
+ - ALTER TABLE blog_posts ADD CONSTRAINT fk_main_category REFERENCES blog_categories
+ - Indexes on slugs, nested set columns, pivot table lookups
+
+
+ ./mill summercms.compile succeeds
+ NestedTree rebuild produces valid left/right sequence
+ SQL migration syntax valid
+
+
+ Category model with nested set tree support.
+ Tag model for free-form labeling.
+ Services handle CRUD with automatic tree maintenance.
+ Pivot tables link posts to categories and tags.
+
+
+
+
+ Task 2: Update BlogPostService for relations
+
+ plugins/golem15/blog/services/BlogPostService.scala (modify)
+ plugins/golem15/blog/repositories/BlogPostRepository.scala (modify)
+ plugins/golem15/blog/controllers/Posts.scala (modify)
+ plugins/golem15/blog/controllers/Categories.scala
+ plugins/golem15/blog/resources/controllers/posts/fields.yaml (modify)
+ plugins/golem15/blog/resources/controllers/categories/fields.yaml
+ plugins/golem15/blog/resources/controllers/categories/columns.yaml
+
+
+ Integrate categories and tags with blog posts:
+
+ **BlogPostRepository.scala additions:**
+ - findBySlugWithRelations(slug): Option[BlogPostWithRelations]
+ - listFrontendWithRelations(options): PaginatedResult[BlogPostWithRelations]
+ - listByCategory(categoryId, options): PaginatedResult[BlogPostWithRelations]
+ - listByTag(tagSlug, options): PaginatedResult[BlogPostWithRelations]
+ - getRelatedPosts(postId, limit): List[BlogPostWithRelations]
+ - Join queries to load categories, tags, author in single query
+
+ **BlogPostService.scala modifications:**
+ - BlogPostWithRelations case class:
+ post: BlogPost, categories: List[Category], tags: List[Tag],
+ featuredImages: List[MediaItem], author: Option[BackendUser],
+ readingTime: Int
+ - Update create/update to handle categories and tags:
+ - PostCreateData/UpdateData add: categoryIds (List[Long]), tagNames (List[String])
+ - On save: Sync pivot tables via services
+ - Set mainCategoryId if categories provided
+ - Update listFrontend to use listFrontendWithRelations
+ - Add listByCategory(categoryId, options)
+ - Add listByTag(tagSlug, options)
+ - Add getRelatedPosts(postId, limit):
+ - Score = 2 * shared_categories + 1 * shared_tags
+ - Exclude current post
+ - Return top N by score
+
+ **Posts.scala controller modifications:**
+ - Update store/update actions to handle categories and tags from form
+ - Add category/tag selection to form context
+
+ **Categories.scala controller (new):**
+ - Admin controller for category management
+ - Actions: index, create, store, edit, update, delete, reorder (tree)
+ - HTMX handler for drag-drop tree reordering
+
+ **fields.yaml modifications (Posts):**
+ - Add categories tab fields:
+ ```yaml
+ _categories:
+ type: relation
+ label: Categories
+ nameFrom: name
+ list: true
+ tree: true
+ tab: Categories
+ _tags:
+ type: taglist
+ label: Tags
+ mode: array
+ separator: ','
+ tab: Tags
+ ```
+
+ **categories/fields.yaml:**
+ ```yaml
+ fields:
+ name:
+ type: text
+ label: Name
+ required: true
+
+ slug:
+ type: text
+ label: Slug
+ preset:
+ field: name
+ type: slug
+
+ parent_id:
+ type: relation
+ label: Parent Category
+ emptyOption: -- None --
+ nameFrom: name
+
+ description:
+ type: textarea
+ label: Description
+ size: small
+
+ published:
+ type: checkbox
+ label: Published
+ ```
+
+ **categories/columns.yaml:**
+ ```yaml
+ columns:
+ name:
+ label: Name
+ sortable: true
+ tree: true
+
+ slug:
+ label: Slug
+
+ published:
+ label: Published
+ type: switch
+
+ post_count:
+ label: Posts
+ type: number
+ ```
+
+ **Register Categories controller in Plugin.scala**
+
+
+ ./mill summercms.compile succeeds
+ Related posts algorithm returns posts with shared categories/tags
+ Category tree drag-drop works in admin
+
+
+ Posts can be assigned categories and tags.
+ Admin can manage categories in tree view.
+ Related posts calculated by shared categories/tags.
+ Post queries include relations efficiently.
+
+
+
+
+ Task 3: Frontend listing components
+
+ 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)
+
+
+ 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
+
+ {% if posts.items is empty %}
+
{{ noPostsMessage }}
+ {% else %}
+
+ {% for post in posts.items %}
+ {% include "posts/item" %}
+ {% endfor %}
+
+
+ {% if posts.hasNext %}
+
+
+
+ {% endif %}
+ {% endif %}
+
+ ```
+
+ **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**
+
+
+ ./mill summercms.compile succeeds
+ Components registered in Plugin.scala
+ Templates have valid Pebble syntax
+ HTMX attributes properly formatted
+
+
+ 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.
+
+
+
+
+
+
+After all tasks complete:
+1. Blog plugin compiles: `./mill summercms.compile`
+2. Migration creates all tables: blog_categories, blog_tags, pivot tables
+3. Category nested tree rebuilds correctly on insert/delete
+4. Posts component loads and paginates
+5. Post component displays with OpenGraph meta
+6. Related posts algorithm returns relevant content
+
+
+
+- Categories form hierarchical tree structure
+- Nested set left/right values correct after operations
+- Tags can be created on-the-fly when typing
+- Post admin form shows category tree selector and tag input
+- 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
+
+
+