docs(09): create phase plan

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
This commit is contained in:
Jakub Zych
2026-02-05 15:33:51 +01:00
parent a12cde5c0c
commit dca89e10cd
7 changed files with 2425 additions and 7 deletions

View File

@@ -0,0 +1,275 @@
---
phase: 09-content-management
plan: 02
type: execute
wave: 2
depends_on: ["09-01"]
files_modified:
- summercms/src/content/cms/pebble/ComponentTag.scala
- summercms/src/content/cms/pebble/PlaceholderTag.scala
- summercms/src/content/cms/pebble/PartialTag.scala
- summercms/src/content/cms/pebble/PutTag.scala
- summercms/src/content/cms/pebble/CmsPebbleExtension.scala
- summercms/src/content/cms/PageRenderContext.scala
- summercms/src/content/cms/CmsPageService.scala
autonomous: true
must_haves:
truths:
- "Components can be embedded in CMS pages via {% component 'alias' %} syntax"
- "Components receive properties from page config and inline overrides"
- "Layouts support {% placeholder 'name' %}default{% endplaceholder %} blocks"
- "Pages can fill placeholders with {% put 'name' %}content{% endput %}"
- "Partials render via {% partial 'name' %} with variable passing"
artifacts:
- path: "summercms/src/content/cms/pebble/ComponentTag.scala"
provides: "{% component %} tag implementation"
contains: "class ComponentTag extends TokenParser"
- path: "summercms/src/content/cms/pebble/PlaceholderTag.scala"
provides: "{% placeholder %} tag with defaults"
contains: "class PlaceholderTag extends TokenParser"
- path: "summercms/src/content/cms/pebble/CmsPebbleExtension.scala"
provides: "Extension registering all CMS tags"
contains: "class CmsPebbleExtension extends Extension"
key_links:
- from: "ComponentTag"
to: "PageRenderContext.components"
via: "resolves component by alias from page context"
pattern: "pageContext\\.components\\.get"
- from: "PlaceholderTag"
to: "PageRenderContext.placeholderContent"
via: "checks for put content before rendering default"
pattern: "placeholderContent\\.get"
---
<objective>
Implement component embedding and placeholder system for CMS pages
Purpose: Enable pages to embed dynamic components via Twig-like syntax and use layouts with placeholders that pages can fill. This completes the template composition system per CONTEXT.md decisions.
Output: Custom Pebble tags (component, placeholder, put, partial), CmsPebbleExtension registering all tags, updated CmsPageService render pipeline.
</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/phases/09-content-management/09-RESEARCH.md
@.planning/phases/09-content-management/09-01-SUMMARY.md
@.planning/phases/03-component-system/03-RESEARCH.md (Component system patterns)
@summercms/src/content/cms/CmsPage.scala
@summercms/src/content/cms/CmsPageService.scala
</context>
<tasks>
<task type="auto">
<name>Task 1: Create PageRenderContext and placeholder/put tags</name>
<files>
summercms/src/content/cms/PageRenderContext.scala
summercms/src/content/cms/pebble/PlaceholderTag.scala
summercms/src/content/cms/pebble/PutTag.scala
</files>
<action>
**PageRenderContext.scala:**
Create render context passed through Pebble evaluation:
```scala
case class PageRenderContext(
page: CmsPage,
components: Map[String, ComponentInstance], // alias -> initialized component
placeholderContent: mutable.Map[String, String], // name -> rendered content
runtime: Runtime[Any] // for ZIO effect execution in sync context
)
```
**PlaceholderTag.scala:**
Implement Pebble TokenParser for `{% placeholder 'name' %}default{% endplaceholder %}`:
- Parse placeholder name expression
- Parse body until `endplaceholder`
- Create PlaceholderNode that:
- Evaluates name from context
- Checks PageRenderContext.placeholderContent for that name
- If content exists, write it
- Otherwise, render default body
Per CONTEXT.md: Flat layouts only, placeholders with defaults.
**PutTag.scala:**
Implement `{% put 'name' %}content{% endput %}`:
- Parse placeholder name expression
- Parse body until `endput`
- Create PutNode that:
- Renders body to string
- Stores in PageRenderContext.placeholderContent[name]
- Writes nothing to output (content stored for later use)
Two-pass rendering needed: First pass collects `put` content, second pass renders layout with placeholders resolved.
</action>
<verify>
`./mill summercms.compile` succeeds with new Pebble tags
</verify>
<done>
PlaceholderTag and PutTag allow layouts to define placeholders with defaults that pages can override. PageRenderContext stores render state.
</done>
</task>
<task type="auto">
<name>Task 2: Implement component and partial tags</name>
<files>
summercms/src/content/cms/pebble/ComponentTag.scala
summercms/src/content/cms/pebble/PartialTag.scala
</files>
<action>
**ComponentTag.scala:**
Implement `{% component 'alias' prop1='value' %}`:
- Parse required alias expression (string)
- Parse optional key=value property pairs until EXECUTE_END
- Create ComponentNode that:
- Gets PageRenderContext from evaluation context
- Looks up component by alias in pageContext.components
- If not found, throw PebbleException with line number
- Override component properties with tag-provided values
- Call component.render() using pageContext.runtime.unsafeRun
- Write rendered HTML to output
Component lookup: Page's INI section defines components like:
```
[blogPosts posts]
postsPerPage = 5
```
Where `blogPosts` is component class, `posts` is alias, properties follow.
**PartialTag.scala:**
Implement `{% partial 'name' var1='value' %}`:
- Parse partial name expression
- Parse optional variable assignments
- Create PartialNode that:
- Resolves partial from theme (partials/{name}.htm)
- Or from plugin namespace (plugin::partialName)
- Renders partial template with provided variables merged into context
- Write output
Per CONTEXT.md: `plugin::partial` for plugin partials, plain name for theme partials.
</action>
<verify>
`./mill summercms.compile` succeeds
ComponentTag parses component alias and properties
PartialTag resolves partial files correctly
</verify>
<done>
ComponentTag embeds components by alias with property overrides. PartialTag includes partial templates with variable passing. Both integrate with Pebble rendering.
</done>
</task>
<task type="auto">
<name>Task 3: Create CmsPebbleExtension and update render pipeline</name>
<files>
summercms/src/content/cms/pebble/CmsPebbleExtension.scala
summercms/src/content/cms/CmsPageService.scala
</files>
<action>
**CmsPebbleExtension.scala:**
Create Pebble Extension registering all CMS tags:
```scala
class CmsPebbleExtension extends Extension:
override def getTokenParsers: java.util.List[TokenParser] =
java.util.Arrays.asList(
new ComponentTag(),
new PlaceholderTag(),
new PutTag(),
new PartialTag()
)
override def getGlobalVariables: java.util.Map[String, Object] = null
override def getFilters: java.util.Map[String, Filter] = null
// ... other Extension methods return null/empty
```
**Update CmsPageService.scala render method:**
Implement two-pass rendering:
1. Initialize components from page config:
- Parse component definitions from page settings
- For each `[componentClass alias]` section, create ComponentInstance
- Run component lifecycle (init, onRun)
2. First pass - render page content:
- Create PageRenderContext with mutable.Map for placeholderContent
- Render page.markup with Pebble
- This collects {% put %} content into placeholderContent
3. Second pass - render layout:
- Get layout from page.config.layout
- Add pageContent variable with rendered page HTML
- Render layout.markup
- {% placeholder %} tags now have access to collected content
- {% page %} tag outputs pageContent variable
Add `{% page %}` as simple Pebble output: `{{ pageContent }}` or custom PageTag.
Note: Must configure PebbleEngine with CmsPebbleExtension when building the engine layer.
</action>
<verify>
`./mill summercms.compile` succeeds
End-to-end test: Create sample page with component, partial, and placeholder
Render should produce composed HTML with all elements
</verify>
<done>
CmsPebbleExtension registers all CMS tags with Pebble. CmsPageService.render uses two-pass rendering: first to collect put content, then to render layout with placeholders resolved.
</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. `./mill summercms.compile` - all code compiles
2. Create test page with all features:
```
url = "/test"
layout = "default"
[myComponent demo]
property = "value"
==
{% put 'sidebar' %}
<aside>Custom sidebar</aside>
{% endput %}
<main>
{% component 'demo' extraProp='override' %}
{% partial 'shared-block' title='Hello' %}
</main>
```
3. Create test layout:
```
<!DOCTYPE html>
<html>
<body>
{% placeholder 'sidebar' %}
<aside>Default sidebar</aside>
{% endplaceholder %}
<div class="content">
{% page %}
</div>
</body>
</html>
```
4. Render produces correct composed HTML with custom sidebar
</verification>
<success_criteria>
- {% component 'alias' %} embeds components with property passing
- {% placeholder 'name' %}default{% endplaceholder %} in layouts with page-fillable content
- {% put 'name' %}content{% endput %} in pages fills layout placeholders
- {% partial 'name' %} includes partials with variables
- Two-pass rendering correctly resolves all template composition
</success_criteria>
<output>
After completion, create `.planning/phases/09-content-management/09-02-SUMMARY.md`
</output>