Phase 09: Content Management - 6 plan(s) in 3 wave(s) - Wave 1: 09-01 (CMS pages/layouts), 09-03 (media library) - parallel - Wave 2: 09-02 (component embedding), 09-04 (revisions), 09-05 (menus) - Wave 3: 09-06 (hot reload) - Ready for execution
9.3 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09-content-management | 01 | execute | 1 |
|
true |
|
Purpose: Enable admin to create CMS pages stored as .htm files (WinterCMS compound format), specify layouts for structure, and have pages accessible via URL patterns with parameter extraction. This is the foundation of the content management system.
Output: CmsPage/CmsLayout/CmsPartial models, CmsRouter for URL matching, CmsPageService for CRUD and rendering, database migration for page metadata caching.
<execution_context> @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/09-content-management/09-RESEARCH.md @.planning/phases/04-theme-engine/04-RESEARCH.md (Pebble template setup) @summercms/src/db/QuillContext.scala @build.mill Task 1: Add dependencies and create CMS content models build.mill summercms/src/content/cms/CmsPage.scala summercms/src/content/cms/CmsLayout.scala summercms/src/content/cms/CmsPartial.scala summercms/src/content/cms/ContentState.scala Add Pebble dependencies to build.mill (if not already present from Phase 4): - io.pebbletemplates:pebble:4.1.1 - com.sfxcode.templating::pebble-scala:1.0.2Create CMS content models in summercms/src/content/cms/:
ContentState.scala:
- Enum with Draft, Published values
- Companion object with fromString/toString
CmsPage.scala:
- CmsPageConfig case class: url, layout (Option), title (Option), description (Option), isHidden (Boolean), state (ContentState), components (Map[String, ComponentConfig])
- CmsPage case class: fileName, config, markup (String), code (Option[String]), themePath (Path), mtime (Instant)
- Companion object with parse(path: Path, themePath: Path): IO[ParseError, CmsPage]
- Parse WinterCMS compound format: INI-style settings, then
==, then Twig markup, optional second==for code section - Use regex pattern:
(?s)(.*?)^==\s*$(.*?)(?:^==\s*$(.*?))?
CmsLayout.scala:
- CmsLayout case class: fileName, markup (String), themePath (Path), mtime (Instant)
- Parse simple .htm files (no settings section, pure Twig)
CmsPartial.scala:
- CmsPartial case class: fileName, markup (String), themePath (Path), mtime (Instant)
- Same format as layouts
Note: Per CONTEXT.md decision, flat layouts only (no inheritance). Pages declare layout via layout = "default" in settings.
./mill summercms.compile succeeds with new models
CmsPage, CmsLayout, CmsPartial, ContentState models exist and compile. CmsPage.parse can load WinterCMS-format .htm files.
RouteSegment.scala:
- Sealed trait RouteSegment with:
- Literal(value: String)
- Parameter(name: String, optional: Boolean, default: Option[String], regex: Option[String])
CmsRouter.scala:
-
Trait CmsRouter with:
- findByUrl(url: String): IO[RouterError, Option[(CmsPage, Map[String, String])]]
- pageUrl(page: CmsPage, params: Map[String, String]): IO[RouterError, String]
-
Case class Route(pattern: String, pageFile: String, segments: List[RouteSegment])
-
Live implementation:
- Build route map from all pages in theme on construction
- parsePattern: Parse URL patterns supporting:
- /blog/:slug - required parameter
- /blog/:page? - optional parameter
- /blog/:page?1 - optional with default
- /blog/:id|^[0-9]+$ - with regex validation
- matchUrl: Try routes in order of specificity (more literal segments = higher priority)
- Extract parameters into Map[String, String]
-
Specificity calculation: Count literal segments, subtract optional params
Router should handle:
- Trailing slash normalization
- URL decoding
- Regex validation failure returns None (not error) Unit test or REPL verification:
- parsePattern("/blog/:slug") returns [Literal("blog"), Parameter("slug", false, None, None)]
- parsePattern("/posts/:page?1") extracts optional param with default
- Route matching extracts correct parameters CmsRouter can parse WinterCMS URL patterns, match incoming URLs to pages, and extract route parameters. Routes sorted by specificity for correct matching.
CREATE INDEX idx_cms_page_metadata_url ON cms_page_metadata(url_pattern); CREATE INDEX idx_cms_page_metadata_state ON cms_page_metadata(state);
**CmsPageService.scala:**
Create service trait and live implementation:
- trait CmsPageService:
- load(fileName: String): IO[PageError, CmsPage]
- list: IO[PageError, List[CmsPageSummary]] (from metadata table)
- render(page: CmsPage, context: Map[String, Any]): IO[PageError, String]
- save(page: CmsPage): IO[PageError, CmsPage]
- delete(fileName: String): IO[PageError, Unit]
- syncMetadata(themePath: Path): IO[PageError, Unit] (scan files, update DB)
- CmsPageSummary case class: fileName, url, title, state, mtime
- Live implementation needs:
- themePath: Path (from config)
- PebbleEngine for rendering
- Quill context for metadata queries
- render method:
1. Load layout if specified
2. Render page markup with Pebble, passing context
3. If layout exists, render layout with pageContent variable set to rendered page
4. Return final HTML
Note: Primary source of truth is filesystem. Database only caches metadata for listing.
</action>
<verify>
`./mill summercms.compile` succeeds
Migration file exists at correct path
CmsPageService.load can parse a sample .htm file (create test file manually if needed)
</verify>
<done>
CmsPageService provides CRUD operations for CMS pages. Pages load from filesystem, render with Pebble through layouts. Metadata cached in database for admin list views.
</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. `./mill summercms.compile` - all code compiles
2. Migration V009 ready for Flyway
3. Sample page file can be parsed: create `themes/test/pages/home.htm` with:
url = "/" layout = "default" title = "Home"
Welcome
``` 4. CmsRouter.parsePattern correctly handles WinterCMS URL patterns 5. CmsPageService.render produces HTML with layout wrapping<success_criteria>
- CMS pages load from .htm files in WinterCMS compound format
- Pages specify layouts which wrap rendered content
- URL router matches patterns with parameter extraction
- Page metadata cached in database for admin queries
- All code compiles with
./mill summercms.compile</success_criteria>