- Plan 02: Added key_link clarifying CmsPageService.render populates PageRenderContext.components before template rendering. Updated Task 2/3 actions to emphasize component initialization flow. - Plan 03: Split into 03 (storage + library core) and 03b (image processing + routes) to reduce scope from 9 files to 7+4 files. Estimated context reduced from ~65% to ~45% each. - Plan 03b: New plan for ImageProcessor and MediaRoutes. Added specific curl command with -F flags and expected JSON response. - Plan 05: Added plugin integration test (MenuPluginIntegrationSpec) demonstrating custom menu item type registration, resolution, and template rendering per CONT-09 requirement. - Plan 06: Reframed must_haves truths from implementation details to user-observable outcomes (e.g., 'Developer edits template file, browser refresh shows changes without server restart') - Roadmap: Updated Phase 9 from 6 to 7 plans.
10 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 | 02 | execute | 2 |
|
|
true |
|
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.
<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/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 Task 1: Create PageRenderContext and placeholder/put tags summercms/src/content/cms/PageRenderContext.scala summercms/src/content/cms/pebble/PlaceholderTag.scala summercms/src/content/cms/pebble/PutTag.scala **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.
./mill summercms.compile succeeds with new Pebble tags
PlaceholderTag and PutTag allow layouts to define placeholders with defaults that pages can override. PageRenderContext stores render state.
CRITICAL: ComponentTag depends on PageRenderContext.components being pre-populated. The components map is populated by CmsPageService.render (Task 3) BEFORE the template is rendered. ComponentTag only looks up by alias - it does NOT initialize components.
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.
./mill summercms.compile succeeds
ComponentTag parses component alias and properties
PartialTag resolves partial files correctly
ComponentTag embeds components by alias with property overrides. PartialTag includes partial templates with variable passing. Both integrate with Pebble rendering.
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 with COMPONENT INITIALIZATION:
1. **Initialize components from page config (CRITICAL - populates PageRenderContext.components):**
- Parse component definitions from page settings (INI format)
- For each `[componentClass alias]` section:
a. Look up component class from ComponentRegistry (from Phase 3)
b. Create ComponentInstance with properties from section
c. Run component lifecycle (init, onRun)
d. Store in components Map keyed by alias
- This Map is passed to PageRenderContext BEFORE any template rendering
2. First pass - render page content:
- Create PageRenderContext with:
- `components`: the Map populated in step 1
- `placeholderContent`: empty mutable.Map
- Render page.markup with Pebble
- This collects {% put %} content into placeholderContent
- {% component 'alias' %} tags can now resolve from pre-populated components Map
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 initializes components from page config into PageRenderContext.components Map, then 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' %}
{% endput %} {% component 'demo' extraProp='override' %} {% partial 'shared-block' title='Hello' %} ``` 3. Create test layout: ``` <html> {% placeholder 'sidebar' %} {% endplaceholder %}<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
- CmsPageService.render initializes components from page config BEFORE template rendering
- PageRenderContext.components is populated when ComponentTag executes
- Two-pass rendering correctly resolves all template composition </success_criteria>