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.4 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.
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:
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' %}
{% 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
- Two-pass rendering correctly resolves all template composition </success_criteria>